diff --git a/scripts/notebooks/README.md b/scripts/notebooks/README.md new file mode 100644 index 000000000..5df71a4d0 --- /dev/null +++ b/scripts/notebooks/README.md @@ -0,0 +1,42 @@ +# MAS Notebooks + +This directory contains Jupyter notebooks for migrating and managing merch cards. + +## Prerequisites + +### Environment Setup + +**Deno Runtime**: These notebooks require Deno to run JavaScript/TypeScript code. + +## Notebooks + +### 1. `eds_export.ipynb` - EDS Content Export + +**Purpose**: Exports merch card content from Adobe's Edge Delivery Services (EDS/Helix) system. + +**Key Features**: + +- Supports both Markdown (`.md`) and HTML (`.plain.html`) formats +- Download a folder to local + +**Output**: + +- `${repo}_filelist.txt` - List of all discovered file paths +- `./html/` directory - Downloaded HTML content files +- `./md/` directory - Downloaded MD content files + +### 2. `aem_import.ipynb` - AEM ODIN Import + +**Purpose**: Parses exported HTML content and imports it as structured data into the ODIN system. + +**Input**: + +- `${repo}_filelist.txt` - List of all file paths to be imported + +### 3. `mas_update.ipynb` - Update M@S cards + +**Purpose**: Batch update existing merch cards + +### 4. `mas_copy.ipynb` - Copy M@S cards + +**Purpose**: Copy merch cards from one surface to the other diff --git a/scripts/notebooks/aem_import.ipynb b/scripts/notebooks/aem_import.ipynb new file mode 100644 index 000000000..b80ebc9d4 --- /dev/null +++ b/scripts/notebooks/aem_import.ipynb @@ -0,0 +1,593 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ac3bb06e", + "metadata": {}, + "source": [ + "* Initialize the libraries and variables\n", + "* Add `AEM_TOKEN` to the `.env` file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06343339", + "metadata": {}, + "outputs": [], + "source": [ + "import { logError } from \"./logError.js\";\n", + "import { reloadEnv } from \"./reloadEnv.js\"\n", + "await reloadEnv();\n", + "\n", + "const token = Deno.env.get(\"AEM_TOKEN\");\n", + "const aemOrigin = 'https://author-p22655-e59433.adobeaemcloud.com';\n", + "const fileListFilename = 'cc_filelist.txt';\n", + "const format = 'html';" + ] + }, + { + "cell_type": "markdown", + "id": "f3654cab", + "metadata": {}, + "source": [ + "* Test if the token is working" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56d56ccf", + "metadata": {}, + "outputs": [], + "source": [ + "const url = `${aemOrigin}/bin/querybuilder.json?path=/content/dam/mas&path.flat=true&type=sling:Folder&p.limit=-1`;\n", + "\n", + "const response = await fetch(url, {\n", + " headers: {\n", + " \"Authorization\": `Bearer ${token}`\n", + " }\n", + "});\n", + "\n", + "if (!response.ok) {\n", + " logError(response);\n", + "}\n", + "\n", + "console.log(response.status);" + ] + }, + { + "cell_type": "markdown", + "id": "9ba43e93", + "metadata": {}, + "source": [ + "* Read the list of file paths" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a42fff8", + "metadata": {}, + "outputs": [], + "source": [ + "const fileList = (await Deno.readTextFile(fileListFilename)).split('\\n');\n", + "console.log(`File count: ${fileList.length}`);" + ] + }, + { + "cell_type": "markdown", + "id": "cc7052cd", + "metadata": {}, + "source": [ + "* Parse the HTMLs or MDs into fields " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6bd580ae", + "metadata": {}, + "outputs": [], + "source": [ + "import { DOMParser } from \"https://deno.land/x/deno_dom@v0.1.38/deno-dom-wasm.ts\";\n", + "\n", + "const getText = (element) => element?.textContent?.trim() || '';\n", + "const getHref = (element) => element?.getAttribute('href') || '';\n", + "\n", + "const extractOsi = (url) => {\n", + " try {\n", + " const urlObj = new URL(url);\n", + " return urlObj.searchParams.get('osi') || '';\n", + " } catch (e) {\n", + " return '';\n", + " }\n", + "};\n", + "\n", + "const ext = {'md': '.md', 'html': '.plain.html'}[format];\n", + "const fileFolder = {'md': './md', 'html': './html'}[format];\n", + "\n", + "const items = [];\n", + "const itemErrors = [];\n", + "\n", + "function processCta(ctaLinks, filePath) {\n", + " const ctas = [];\n", + " let osi = '';\n", + "\n", + " if (ctaLinks.length > 3 || ctaLinks.length === 0) {\n", + " throw new Error(`${filePath}: Got ${ctaLinks.length} CTAs\\n${ctaChild.outerHTML}`);\n", + " }\n", + "\n", + " for (const ctaLink of ctaLinks) {\n", + " const href = getHref(ctaLink);\n", + " const [text, aria] = getText(ctaLink).split('|').map(s => s.trim());\n", + " ctas.push({\n", + " text,\n", + " aria,\n", + " href: href,\n", + " osi: extractOsi(href),\n", + " parentTag: ctaLink.parentElement.tagName,\n", + " });\n", + " }\n", + "\n", + " const osiCtas = ctas.filter(cta => cta.osi);\n", + " if (osiCtas.length > 0) {\n", + " osi = osiCtas[0].osi;\n", + " }\n", + "\n", + " return { ctas, osi };\n", + "}\n", + "\n", + "for (let i = 0; i < fileList.length; i++) {\n", + " const filePath = fileList[i];\n", + " const localPath = `${fileFolder}${filePath}${ext}`;\n", + "\n", + " const htmlContent = await Deno.readTextFile(localPath);\n", + "\n", + " const doc = new DOMParser().parseFromString(htmlContent, 'text/html');\n", + "\n", + " const rows = doc.querySelectorAll('.merch-card.catalog > div > div');\n", + "\n", + " if (rows.length === 0) {\n", + " itemErrors.push(filePath);\n", + " continue;\n", + " }\n", + "\n", + " const result = {\n", + " filePath,\n", + " column1Html: '',\n", + " deviceTypes: [],\n", + " recommendedFor: [],\n", + " mnemonicIcons: [],\n", + " cardTitle: '',\n", + " cardTitleLink: '',\n", + " description: '',\n", + " ctas: [],\n", + " ctasHtml: '',\n", + " osi: '',\n", + " tags: [],\n", + " };\n", + "\n", + " let rowIndex = 0;\n", + "\n", + " if (rows.length > 2) {\n", + " // Row 1: Device types and Recommended for\n", + " const row1 = rows[rowIndex++];\n", + " // Clean up excess whitespaces\n", + " result.column1Html = row1.innerHTML\n", + " .split('\\n')\n", + " .map(line => line.trim())\n", + " .filter(line => line.length > 0)\n", + " .join('');\n", + " const lists = row1.querySelectorAll('ul');\n", + " if (lists.length >= 1) {\n", + " const deviceItems = lists[0].querySelectorAll('li');\n", + " result.deviceTypes = Array.from(deviceItems).map(li => getText(li));\n", + " }\n", + " if (lists.length >= 2) {\n", + " const recItems = lists[1].querySelectorAll('li');\n", + " result.recommendedFor = Array.from(recItems).map(li => getText(li));\n", + " }\n", + " }\n", + "\n", + " if (rows.length >= 2) {\n", + " // Row 2: Main content with 4 or 5 children\n", + " const row2 = rows[rowIndex++];\n", + " const children = Array.from(row2.children);\n", + "\n", + " // Child 1: Mnemonic icons (could be multiple icons, each with its link)\n", + " if (children.length >= 1) {\n", + " const iconLinks = children[0].querySelectorAll('a');\n", + " iconLinks.forEach(iconLink => {\n", + " const text = getText(iconLink);\n", + " const parts = text.split('|').map(s => s.trim());\n", + " result.mnemonicIcons.push({\n", + " icon: parts[0] || '',\n", + " alt: parts[1] || '',\n", + " link: getHref(iconLink)\n", + " });\n", + " });\n", + " }\n", + "\n", + " // Child 2: Title and title link (h3)\n", + " if (children.length >= 2) {\n", + " const titleLink = children[1].querySelector('a');\n", + " if (titleLink) {\n", + " result.cardTitle = getText(titleLink);\n", + " result.cardTitleLink = getHref(titleLink);\n", + " } else {\n", + " result.cardTitle = getText(children[1]);\n", + " }\n", + " }\n", + "\n", + " // Child 3: Description (includes description text and learn more link)\n", + " if (children.length >= 3) {\n", + " const descChild = children[2];\n", + " result.description = descChild.outerHTML;\n", + " }\n", + "\n", + " // Child 4: Footer CTAs (strong and em links)\n", + " if (children.length >= 4) {\n", + " const ctaChild = children[3];\n", + " result.ctasHtml = ctaChild.innerHTML;\n", + " \n", + " const ctaLinks = ctaChild.querySelectorAll('a');\n", + " const { ctas, osi } = processCta(ctaLinks, filePath);\n", + " result.ctas = ctas;\n", + " result.osi = osi;\n", + " }\n", + "\n", + " if (children.length >= 5) {\n", + " // Move line 4 to description\n", + " const learnmore = result.ctas.map(x => `${x.text}`).join(' | ');\n", + " result.description = result.description.replace(new RegExp('

$'), `
${learnmore}

`);\n", + "\n", + " // Process the new CTAs\n", + " const ctaChild = children[4];\n", + " result.ctasHtml = ctaChild.innerHTML;\n", + "\n", + " const ctaLinks = ctaChild.querySelectorAll('a');\n", + " const { ctas, osi } = processCta(ctaLinks, filePath);\n", + " result.ctas = ctas;\n", + " result.osi = osi; \n", + " }\n", + " }\n", + "\n", + " if (rows.length >= 2) {\n", + " // Row 3: Tags/categories\n", + " const row3 = rows[rowIndex++];\n", + " const tagParagraphs = row3.querySelectorAll('p');\n", + " result.tags = Array.from(tagParagraphs).map(p => getText(p));\n", + " }\n", + "\n", + " items.push(result);\n", + "}\n", + "\n", + "console.log(`Read ${items.length} cards`);\n", + "console.log(`Errors: ${itemErrors.length}\\n${itemErrors.join('\\n')}`);" + ] + }, + { + "cell_type": "markdown", + "id": "b5d25b8b", + "metadata": {}, + "source": [ + "* Review the parsed items" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c32e12b2", + "metadata": {}, + "outputs": [], + "source": [ + "console.log(JSON.stringify(items[0], null, 2));" + ] + }, + { + "cell_type": "markdown", + "id": "ae810ec8", + "metadata": {}, + "source": [ + "* Normalize the items for writing to ODIN\n", + "* Some fields need be proccessed first" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1658addf", + "metadata": {}, + "outputs": [], + "source": [ + "const normItems = [];\n", + "for (let i = 0; i < items.length; i++) {\n", + " // Clone the item\n", + " const item = JSON.parse(JSON.stringify(items[i]));\n", + "\n", + " // Update the mnemonic icons links to hlx.page/aem.page to aem.live\n", + "\n", + " item.mnemonicIcons.forEach(x => {\n", + " x.icon = x.icon.replace('hlx.page', 'aem.live').replace('aem.page', 'aem.live');\n", + " });\n", + " \n", + " // Update the card description\n", + " const descDoc = new DOMParser().parseFromString(item.description, 'text/html');\n", + " const descLinks = descDoc.querySelectorAll('a');\n", + " if (descLinks.length > 0) {\n", + " descLinks.forEach(x => {\n", + " const [text, aria] = x.textContent.split('|').map(s => s.trim());\n", + " x.textContent = text;\n", + " x.setAttribute('aria-label', aria);\n", + " });\n", + " }\n", + " item.description = descDoc.body.innerHTML;\n", + "\n", + " // Update the ctas text\n", + " item.ctas.forEach(x => {\n", + " const ctaText = x.text.split('|').map(s => s.trim());\n", + " x.text = ctaText[0];\n", + " x.alt = ctaText[1];\n", + " });\n", + "\n", + " item.ctasHtml = item.ctas.map(x => {\n", + " const attrs = [];\n", + " let className = { 'strong': 'accent', 'em': 'primary-outline' }[x.parentTag.toLowerCase()];\n", + " if (className) {\n", + " attrs.push(`class=\"${className}\"`);\n", + " } else {\n", + " attrs.push(`class=\"primary-link\"`);\n", + " }\n", + " if (x.href) {\n", + " attrs.push(`href=\"${x.href}\"`);\n", + " }\n", + " if (x.aria) {\n", + " attrs.push(`aria-label=\"${x.aria}\"`);\n", + " }\n", + " return `${x.text}`;\n", + " }).join(' ');\n", + " \n", + " normItems.push(item);\n", + "}\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "c49c19c0", + "metadata": {}, + "source": [ + "* Review the normalized items" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb5b74a4", + "metadata": {}, + "outputs": [], + "source": [ + "console.log(JSON.stringify(normItems[0], null, 2));" + ] + }, + { + "cell_type": "markdown", + "id": "36faa114", + "metadata": {}, + "source": [ + "* Write to ODIN\n", + "* Update `parentPath` if needed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4868b27f", + "metadata": {}, + "outputs": [], + "source": [ + "const url = `${aemOrigin}/adobe/sites/cf/fragments`;\n", + "const parentPath = `/content/dam/mas/sandbox/en_US`;\n", + "\n", + "const odinResults = [];\n", + "const odinErrors = [];\n", + "\n", + "//for (let i=0; i icon.icon)\n", + "\n", + " },\n", + " {\n", + " \"name\": \"mnemonicLink\",\n", + " \"type\": \"text\",\n", + " \"multiple\": true,\n", + " \"locked\": false,\n", + " \"values\": result.mnemonicIcons.map(icon => icon.link)\n", + " },\n", + " {\n", + " \"name\": \"shortDescription\",\n", + " \"type\": \"long-text\",\n", + " \"multiple\": false,\n", + " \"locked\": false,\n", + " \"mimeType\": \"text/html\",\n", + " \"values\": [\n", + " result.shortDescription\n", + " ]\n", + " },\n", + " {\n", + " \"name\": \"description\",\n", + " \"type\": \"long-text\",\n", + " \"multiple\": false,\n", + " \"locked\": false,\n", + " \"mimeType\": \"text/html\",\n", + " \"values\": [\n", + " result.description\n", + " ]\n", + " },\n", + " {\n", + " \"name\": \"ctas\",\n", + " \"type\": \"long-text\",\n", + " \"multiple\": false,\n", + " \"locked\": false,\n", + " \"mimeType\": \"text/html\",\n", + " \"values\": [\n", + " result.ctasHtml\n", + " ]\n", + " },\n", + " {\n", + " \"name\": \"osi\",\n", + " \"type\": \"text\",\n", + " \"multiple\": false,\n", + " \"locked\": false,\n", + " \"values\": [\n", + " result.osi\n", + " ]\n", + " },\n", + " ];\n", + "\n", + " const response = await fetch(url, {\n", + " method: 'POST',\n", + " headers: {\n", + " 'Content-Type': 'application/json',\n", + " \"x-api-key\": \"mas-studio\",\n", + " \"Authorization\": `Bearer ${token}`,\n", + " 'x-aem-affinity-type': 'api',\n", + " },\n", + " body: JSON.stringify({\n", + " title,\n", + " name,\n", + " modelId,\n", + " parentPath,\n", + " description,\n", + " fields,\n", + " }),\n", + " })\n", + "\n", + " if (!response.ok) {\n", + " odinErrors.push(response);\n", + " } else {\n", + " const data = await response.json();\n", + " odinResults.push(data);\n", + " }\n", + "}\n", + "\n", + "console.log(`Imported ${odinResults.length} cards`);\n", + "console.log(`Errors: ${odinErrors.length}`);\n" + ] + }, + { + "cell_type": "markdown", + "id": "3f4a36c0", + "metadata": {}, + "source": [ + "* List IDs of imported cards " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abb41f67", + "metadata": {}, + "outputs": [], + "source": [ + "const ids = odinResults.map(x => x.id);\n", + "console.log(ids);" + ] + }, + { + "cell_type": "markdown", + "id": "a4e1a69d", + "metadata": {}, + "source": [ + "* Check the imported card\n", + "* Update `cardId`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6fe224c2", + "metadata": {}, + "outputs": [], + "source": [ + "const cardId = ids[0];\n", + "const url = `${aemOrigin}/adobe/sites/cf/fragments/${cardId}?references=direct-hydrated`;\n", + "\n", + "const response = await fetch(url, {\n", + " headers: {\n", + " \"x-api-key\": \"mas-studio\",\n", + " \"Authorization\": `Bearer ${token}`,\n", + " pragma: 'no-cache',\n", + " 'cache-control': 'no-cache',\n", + " 'x-aem-affinity-type': 'api',\n", + " }\n", + "});\n", + "\n", + "if (!response.ok) {\n", + " logError(response);\n", + "}\n", + "\n", + "const data = await response.json();\n", + "await Deno.writeTextFile('catalog.json', JSON.stringify(data, null, 2));\n", + "console.log(JSON.stringify(data, null, 2));" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Deno", + "language": "typescript", + "name": "deno" + }, + "language_info": { + "codemirror_mode": "typescript", + "file_extension": ".ts", + "mimetype": "text/x.typescript", + "name": "typescript", + "nbconvert_exporter": "script", + "pygments_lexer": "typescript", + "version": "5.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scripts/notebooks/eds_export.ipynb b/scripts/notebooks/eds_export.ipynb new file mode 100644 index 000000000..d1186dc75 --- /dev/null +++ b/scripts/notebooks/eds_export.ipynb @@ -0,0 +1,203 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c682a6ee", + "metadata": {}, + "source": [ + "* Add `EDS_TOKEN` to the `.env` file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c34ac05", + "metadata": {}, + "outputs": [], + "source": [ + "import { logError } from \"./logError.js\";\n", + "import { reloadEnv } from \"./reloadEnv.js\"\n", + "await reloadEnv();\n", + "\n", + "const token = Deno.env.get(\"EDS_TOKEN\");\n", + "\n", + "const org = 'adobecom';\n", + "const repo = 'cc';\n", + "const merchCardPath = '/cc-shared/fragments/merch/products/catalog/merch-card';\n", + "const format = 'html'; // 'md' or 'html'\n" + ] + }, + { + "cell_type": "markdown", + "id": "e3b635ce", + "metadata": {}, + "source": [ + "* Test if the token is working" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7157d132", + "metadata": {}, + "outputs": [], + "source": [ + "const response = await fetch(`https://admin.hlx.page/status/${org}/${repo}/main/`, {\n", + " headers: {\n", + " 'x-auth-token': token\n", + " }\n", + "});\n", + "\n", + "if (!response.ok) {\n", + " logError(response);\n", + "}\n", + "\n", + "console.log(response.status);" + ] + }, + { + "cell_type": "markdown", + "id": "518700eb", + "metadata": {}, + "source": [ + "* Query the list of files with Helix API" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e75782e", + "metadata": {}, + "outputs": [], + "source": [ + "import { scheduler } from \"node:timers/promises\";\n", + "\n", + "const response = await fetch(`https://admin.hlx.page/status/${org}/${repo}/main/*`, {\n", + " method: 'POST',\n", + " headers: {\n", + " 'x-auth-token': token,\n", + " 'Content-Type': 'application/json'\n", + " },\n", + "\n", + " body: JSON.stringify({\n", + " \"select\": [\n", + " 'live'\n", + " ],\n", + " \"paths\": [\n", + " `${merchCardPath}/*`\n", + " ],\n", + " \"pathsOnly\": true\n", + " })\n", + "});\n", + "\n", + "if (!response.ok) {\n", + " logError(response);\n", + "}\n", + "\n", + "const data = await response.json();\n", + "console.log(`Job link: ${data.links.self}`);\n", + "\n", + "const jobLink = data.links.self;\n", + "\n", + "let jobDetails;\n", + "\n", + "let retry = 10;\n", + "while (retry > 0) {\n", + " const responseState = await fetch(jobLink, {\n", + " headers: {\n", + " 'x-auth-token': token\n", + " }\n", + " });\n", + "\n", + " if (!responseState.ok) {\n", + " logError(responseState);\n", + " }\n", + "\n", + " const dataState = await responseState.json();\n", + "\n", + " console.log(`Job state: ${dataState.state}`);\n", + "\n", + " if (dataState.state === 'stopped') {\n", + " jobDetails = dataState.links.details;\n", + " break;\n", + " }\n", + "\n", + " await scheduler.wait(5000)\n", + " retry -= 1;\n", + "}\n", + "\n", + "const responseDetail = await fetch(jobDetails, {\n", + " headers: {\n", + " 'x-auth-token': token\n", + " }\n", + "});\n", + "\n", + "if (!responseDetail.ok) {\n", + " logError(responseDetail);\n", + "}\n", + "\n", + "const dataDetail = await responseDetail.json();\n", + "const fileList = dataDetail.data.resources['live'];\n", + "\n", + "console.log(`page and asset count: ${fileList.length}`);\n", + "\n", + "const fileName = `${repo}_filelist.txt`;\n", + "await Deno.writeTextFile(fileName, fileList.join('\\n'));\n", + "console.log(`\\nOutput saved to: ${fileName}`);\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "964960a6", + "metadata": {}, + "outputs": [], + "source": [ + "import { ensureDir } from \"https://deno.land/std@0.208.0/fs/ensure_dir.ts\";\n", + "import { dirname } from \"https://deno.land/std@0.208.0/path/mod.ts\";\n", + "\n", + "const ext = {'md': '.md', 'html': '.plain.html'}[format];\n", + "\n", + "let successCount = 0;\n", + "let failureCount = 0;\n", + "\n", + "if (ext) {\n", + " for (let i = 0; i < fileList.length; i++) {\n", + " const filePath = fileList[i];\n", + " const response = await fetch(`https://main--${repo}--${org}.aem.live${filePath}${ext}`);\n", + " if (!response.ok) {\n", + " logError(response);\n", + " failureCount++;\n", + " } else {\n", + " const data = await response.text();\n", + " const localPath = `./${format}${filePath}${ext}`;\n", + " await ensureDir(dirname(localPath));\n", + " await Deno.writeTextFile(localPath, data);\n", + " successCount++;\n", + " }\n", + " }\n", + "}\n", + "\n", + "console.log(`\\nResults: ${successCount} succeeded, ${failureCount} failed out of ${fileList.length} total`);\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Deno", + "language": "typescript", + "name": "deno" + }, + "language_info": { + "codemirror_mode": "typescript", + "file_extension": ".ts", + "mimetype": "text/x.typescript", + "name": "typescript", + "nbconvert_exporter": "script", + "pygments_lexer": "typescript", + "version": "5.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scripts/notebooks/logError.js b/scripts/notebooks/logError.js new file mode 100644 index 000000000..cb5d31c4f --- /dev/null +++ b/scripts/notebooks/logError.js @@ -0,0 +1,18 @@ +export async function logError(response) { + console.error(`Request failed with status: ${response.status} ${response.statusText}`); + + console.error('Response Headers:'); + for (const [key, value] of response.headers.entries()) { + console.error(` ${key}: ${value}`); + } + + try { + const errorData = await response.json(); + console.error('Error Response Body:', errorData); + } catch (e) { + const errorText = await response.text(); + console.error('Error Response Body (text):', errorText); + } + + throw new Error(`HTTP ${response.status}: ${response.statusText}`); +} diff --git a/scripts/notebooks/mas_copy.ipynb b/scripts/notebooks/mas_copy.ipynb new file mode 100644 index 000000000..2e828ad1e --- /dev/null +++ b/scripts/notebooks/mas_copy.ipynb @@ -0,0 +1,231 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "50093cc3", + "metadata": {}, + "source": [ + "* Initialize the libraries and variables" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a80c4080", + "metadata": {}, + "outputs": [], + "source": [ + "import { DOMParser } from \"https://deno.land/x/deno_dom@v0.1.38/deno-dom-wasm.ts\";\n", + "import { logError } from \"./logError.js\";\n", + "import { reloadEnv } from \"./reloadEnv.js\"\n", + "await reloadEnv();\n", + "\n", + "const token = Deno.env.get(\"AEM_TOKEN\");\n", + "const aemOrigin = 'https://author-p22655-e59433.adobeaemcloud.com';\n", + "const destination = '/content/dam/mas/acom/en_US/';" + ] + }, + { + "cell_type": "markdown", + "id": "0ce87c5a", + "metadata": {}, + "source": [ + "* Test if the token is working" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb7c757f", + "metadata": {}, + "outputs": [], + "source": [ + "const url = `${aemOrigin}/bin/querybuilder.json?path=/content/dam/mas&path.flat=true&type=sling:Folder&p.limit=-1`;\n", + "\n", + "const response = await fetch(url, {\n", + " headers: {\n", + " \"Authorization\": `Bearer ${token}`\n", + " }\n", + "});\n", + "\n", + "if (!response.ok) {\n", + " logError(response);\n", + "}\n", + "\n", + "console.log(response.status);" + ] + }, + { + "cell_type": "markdown", + "id": "54772fa4", + "metadata": {}, + "source": [ + "* Read card id to be copied from a file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "222b9fe0", + "metadata": {}, + "outputs": [], + "source": [ + "const idListFilename = './copy_idlist.txt';\n", + "const idList = (await Deno.readTextFile(idListFilename)).split('\\n');\n", + "console.log(`Id count: ${idList.length}`);\n", + "console.log(idList.join('\\n'));" + ] + }, + { + "cell_type": "markdown", + "id": "35ab10c9", + "metadata": {}, + "source": [ + "* Fetch all cards to be copied" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01537438", + "metadata": {}, + "outputs": [], + "source": [ + "const cardsToBeCopied = [];\n", + "\n", + "for (const item of idList) {\n", + " const cardId = item;\n", + " const url = `${aemOrigin}/adobe/sites/cf/fragments/${cardId}`;\n", + "\n", + "\n", + " // Get the card\n", + " const response = await fetch(url, {\n", + " headers: {\n", + " \"x-api-key\": \"mas-studio\",\n", + " \"Authorization\": `Bearer ${token}`,\n", + " pragma: 'no-cache',\n", + " 'cache-control': 'no-cache',\n", + " 'x-aem-affinity-type': 'api',\n", + " }\n", + " });\n", + "\n", + " if (!response.ok) {\n", + " logError(response);\n", + " } else {\n", + " const data = await response.json();\n", + " cardsToBeCopied.push(data);\n", + " }\n", + "}\n", + "\n", + "console.log(`Total cards: ${cardsToBeCopied.length}`);\n", + "console.log(JSON.stringify(cardsToBeCopied[0], null, 2));\n" + ] + }, + { + "cell_type": "markdown", + "id": "438c3756", + "metadata": {}, + "source": [ + "* Save to the destination" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ddb54094", + "metadata": {}, + "outputs": [], + "source": [ + "const cardsDone = [];\n", + "const cardsFailed = [];\n", + "\n", + "for (const item of cardsToBeCopied) {\n", + " const cardId = item.id;\n", + " const url = `${aemOrigin}/adobe/sites/cf/fragments/${cardId}`;\n", + "\n", + " console.log(`Getting card ${cardId}`);\n", + "\n", + " // Get the card\n", + " const response = await fetch(url, {\n", + " headers: {\n", + " \"x-api-key\": \"mas-studio\",\n", + " \"Authorization\": `Bearer ${token}`,\n", + " pragma: 'no-cache',\n", + " 'cache-control': 'no-cache',\n", + " 'x-aem-affinity-type': 'api',\n", + " }\n", + " });\n", + "\n", + " if (!response.ok) {\n", + " logError(response);\n", + " cardsFailed.push(item);\n", + " continue;\n", + " }\n", + " const data = await response.json();\n", + "\n", + " // Save to the destination\n", + " const parentPath = destination;\n", + " const name = data.path.replace(/^.*[\\\\\\/]/, '');\n", + "\n", + " console.log(`Saving card ${name} to ${parentPath}`);\n", + "\n", + " const title = data.title;\n", + " const modelId = data.model.id;\n", + " const description = data.description;\n", + " const fields = data.fields;\n", + " \n", + " const urlSave = `${aemOrigin}/adobe/sites/cf/fragments`;\n", + "\n", + " const responseSave = await fetch(urlSave, {\n", + " method: 'POST',\n", + " headers: {\n", + " 'Content-Type': 'application/json',\n", + " \"x-api-key\": \"mas-studio\",\n", + " \"Authorization\": `Bearer ${token}`,\n", + " 'x-aem-affinity-type': 'api',\n", + " },\n", + " body: JSON.stringify({\n", + " title,\n", + " name,\n", + " modelId,\n", + " parentPath,\n", + " description,\n", + " fields,\n", + " }),\n", + " })\n", + "\n", + " if (!responseSave.ok) {\n", + " logError(responseSave);\n", + " cardsFailed.push(item);\n", + " console.log(`Failed to save card ${name} to ${parentPath}`);\n", + " } else {\n", + " console.log(`Saving card ${name} to ${parentPath}`);\n", + " const data = await responseSave.json();\n", + " cardsDone.push(data);\n", + " } \n", + "}\n", + "\n", + "console.log(`Total cards copied: ${cardsDone.length}`);\n", + "console.log(`Total cards failed: ${cardsFailed.length}`);" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Deno", + "language": "typescript", + "name": "deno" + }, + "language_info": { + "codemirror_mode": "typescript", + "file_extension": ".ts", + "mimetype": "text/x.typescript", + "name": "typescript", + "nbconvert_exporter": "script", + "pygments_lexer": "typescript", + "version": "5.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scripts/notebooks/mas_update.ipynb b/scripts/notebooks/mas_update.ipynb new file mode 100644 index 000000000..5015cd520 --- /dev/null +++ b/scripts/notebooks/mas_update.ipynb @@ -0,0 +1,262 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "50093cc3", + "metadata": {}, + "source": [ + "* Initialize the libraries and variables" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a80c4080", + "metadata": {}, + "outputs": [], + "source": [ + "import { DOMParser } from \"https://deno.land/x/deno_dom@v0.1.38/deno-dom-wasm.ts\";\n", + "import { logError } from \"./logError.js\";\n", + "import { reloadEnv } from \"./reloadEnv.js\"\n", + "await reloadEnv();\n", + "\n", + "const token = Deno.env.get(\"AEM_TOKEN\");\n", + "const aemOrigin = 'https://author-p22655-e59433.adobeaemcloud.com';" + ] + }, + { + "cell_type": "markdown", + "id": "54772fa4", + "metadata": {}, + "source": [ + "* Fetch the cards to be proccessed\n", + "* Update the query" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01537438", + "metadata": {}, + "outputs": [], + "source": [ + "const cardPath = '/content/dam/mas/sandbox';\n", + "const createdBy = ''; // with @adobe.com\n", + "\n", + "const query = {\n", + " filter: {\n", + " path: cardPath,\n", + " created: {\n", + " by: [createdBy],\n", + " }\n", + " } \n", + "};\n", + "\n", + "const items = [];\n", + "let cursor = 'start';\n", + "\n", + "while (cursor) {\n", + " let url = `${aemOrigin}/adobe/sites/cf/fragments/search?query=${JSON.stringify(query)}`;\n", + " \n", + " if (cursor !== 'start') {\n", + " url += `&cursor=${cursor}`;\n", + " }\n", + "\n", + " const response = await fetch(url, {\n", + " headers: {\n", + " \"x-api-key\": \"mas-studio\",\n", + " \"Authorization\": `Bearer ${token}`,\n", + " } \n", + " });\n", + "\n", + " if (!response.ok) {\n", + " logError(response);\n", + " }\n", + "\n", + " const data = await response.json();\n", + " \n", + " items.push(...data.items);\n", + " cursor = data.cursor;\n", + "\n", + " console.log(`Fetched ${data.items.length} items`);\n", + "}\n", + "\n", + "console.log(`Total items: ${items.length}`);" + ] + }, + { + "cell_type": "markdown", + "id": "438c3756", + "metadata": {}, + "source": [ + "* Review the items" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab2398ed", + "metadata": {}, + "outputs": [], + "source": [ + "console.log(JSON.stringify(items[0], null, 2));" + ] + }, + { + "cell_type": "markdown", + "id": "99244f3b", + "metadata": {}, + "source": [ + "* Filter and keep cards to be modified\n", + "* Find cards that contain descriptions with links of the `secondary-link` class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "daa3c664", + "metadata": {}, + "outputs": [], + "source": [ + "const toBeModified = [];\n", + "\n", + "for (const item of items) {\n", + " const descHtml = item.fields.filter(f => f.name === 'description')[0].values[0];\n", + " const doc = new DOMParser().parseFromString(descHtml, 'text/html');\n", + " const links = doc.querySelectorAll('a');\n", + " if ([...links].some(x => x.getAttribute('class')?.includes('secondary-link'))) {\n", + " toBeModified.push(item);\n", + " }\n", + "}\n", + "\n", + "console.log(`Total items to be modified: ${toBeModified.length}`);" + ] + }, + { + "cell_type": "markdown", + "id": "14dd8328", + "metadata": {}, + "source": [ + "* Review cards to be updated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b116ec36", + "metadata": {}, + "outputs": [], + "source": [ + "console.log(JSON.stringify(toBeModified[0], null, 2));" + ] + }, + { + "cell_type": "markdown", + "id": "3d524c68", + "metadata": {}, + "source": [ + "* Updated the cards to be modified\n", + "* For each card, read the card first and keep etag and then update the card" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ddb54094", + "metadata": {}, + "outputs": [], + "source": [ + "for (const item of toBeModified) {\n", + " const cardId = item.id;\n", + " const url = `${aemOrigin}/adobe/sites/cf/fragments/${cardId}`;\n", + "\n", + " console.log(`Getting card ${cardId}`);\n", + "\n", + " // Get the card\n", + " const response = await fetch(url, {\n", + " headers: {\n", + " \"x-api-key\": \"mas-studio\",\n", + " \"Authorization\": `Bearer ${token}`,\n", + " pragma: 'no-cache',\n", + " 'cache-control': 'no-cache',\n", + " 'x-aem-affinity-type': 'api',\n", + " }\n", + " });\n", + "\n", + " if (!response.ok) {\n", + " logError(response);\n", + " }\n", + "\n", + " const data = await response.json();\n", + " const etag = response.headers.get('etag');\n", + "\n", + " // Update the description\n", + "\n", + " // Get the current value\n", + " const descField = data.fields.filter(f => f.name === 'description')[0];\n", + " const descHtml = descField.values[0];\n", + "\n", + " console.log(`Before:\\n ${descHtml}`);\n", + "\n", + " // Prepare the update\n", + " const doc = new DOMParser().parseFromString(descHtml, 'text/html');\n", + " const links = doc.querySelectorAll('a');\n", + " for (const link of links) {\n", + " if (link.getAttribute('class')?.includes('secondary-link')) {\n", + " link.setAttribute('class', 'primary-link');\n", + " }\n", + " }\n", + " descField.values[0] = doc.body.innerHTML;\n", + " console.log(`Ready to update:\\n ${descField.values[0]}`);\n", + "\n", + " // Update the card\n", + " const responsePut = await fetch(url, {\n", + " method: 'PUT',\n", + " headers: {\n", + " 'Content-Type': 'application/json',\n", + " \"x-api-key\": \"mas-studio\",\n", + " \"Authorization\": `Bearer ${token}`,\n", + " pragma: 'no-cache',\n", + " 'cache-control': 'no-cache',\n", + " 'x-aem-affinity-type': 'api',\n", + " 'If-Match': etag,\n", + " },\n", + " body: JSON.stringify({\n", + " title: data.title,\n", + " name: data.name,\n", + " modelId: data.modelId,\n", + " parentPath: data.parentPath,\n", + " description: data.description,\n", + " fields: data.fields,\n", + " }),\n", + " });\n", + "\n", + " if (!responsePut.ok) {\n", + " logError(responsePut);\n", + " }\n", + "\n", + " const dataPut = await responsePut.json();\n", + " console.log(`After:\\n ${dataPut.fields.filter(f => f.name === 'description')[0].values[0]}`);\n", + "}" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Deno", + "language": "typescript", + "name": "deno" + }, + "language_info": { + "codemirror_mode": "typescript", + "file_extension": ".ts", + "mimetype": "text/x.typescript", + "name": "typescript", + "nbconvert_exporter": "script", + "pygments_lexer": "typescript", + "version": "5.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scripts/notebooks/reloadEnv.js b/scripts/notebooks/reloadEnv.js new file mode 100644 index 000000000..bc3b837e5 --- /dev/null +++ b/scripts/notebooks/reloadEnv.js @@ -0,0 +1,27 @@ +// Force reload environment variables by clearing existing ones first +import { load } from 'jsr:@std/dotenv'; + +export async function reloadEnv() { + const envPath = '../../.env'; + try { + // Read the .env file content directly + const envContent = await Deno.readTextFile(envPath); + const envLines = envContent.split('\n').filter((line) => line.trim() && !line.startsWith('#')); + + // Clear existing environment variables that are defined in .env + for (const line of envLines) { + const [key] = line.split('='); + if (key) { + Deno.env.delete(key.trim()); + } + } + + // Now load fresh from .env file + await load({ envPath, export: true, override: true }); + console.log('Environment variables reloaded successfully'); + } catch (error) { + console.error('Error reloading environment variables:', error); + // Fallback to regular load + await load({ envPath, export: true, override: true }); + } +}