diff --git a/README.md b/README.md index fa7e06ced..64dc41bdb 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,19 @@ Check for readme.md files and install dependencies for folders This is the migration V2's node server. +### Installation + +1. Navigate to the project directory: + + ```sh + cd api + ``` + +2. Install the dependencies: + ```sh + npm install + ``` + ### Environment Variables The following environment variables are used in this project: @@ -26,14 +39,18 @@ The following environment variables are used in this project: - `APP_TOKEN_KEY`: The token key for the application. Default is `MIGRATION_V2`. - `PORT`: The port number on which the application runs. Default is `5001`. -Make sure to set these variables in a `.env` file at the root of your project. +Make sure to set these variables in a `.env` file at the root of your api project. 1. To run the development server, create a `./development.env` file and add environment variables as per `./example.env` 2. To run the production server, create a `./production.env` file and add environment variables as per `./example.env` ### To start the server -Run `npm run dev` +Run : + +```sh +`npm run dev` +``` ## Migration UI @@ -105,7 +122,7 @@ The migration-v2 upload-api project is designed to facilitate the migration of d Navigate to the project directory: ``` -cd migration-v2/upload-api +cd upload-api ``` Install dependencies: @@ -121,7 +138,7 @@ The following environment variables are used in this project: - `PORT`: The port number on which the application runs. Default is `4002`. - `NODE_BACKEND_API`: The backend API endpoint. Default is `http://localhost:5001`. -Make sure to set these variables in a `.env` file at the root of your project. +Make sure to set these variables in a `.env` file at the root of your upload-api project. ### Configuration @@ -155,6 +172,14 @@ The following configuration is used in this project: - `npm run postinstall`: Installs dependencies for the api, ui, and upload-api directories. - `npm test`: Displays an error message indicating that no tests are specified. +### Usage + +Start the development server: + +```sh +npm start +``` + ## Repository - Type: git @@ -202,4 +227,4 @@ The author information is not specified. ## Contact -For further assistance, please contact the project maintainer through the issues page on GitHub. +For further assistance, please contact the project maintainer through the issues page on GitHub. \ No newline at end of file diff --git a/api/src/services/wordpress.service.ts b/api/src/services/wordpress.service.ts index 6388081fb..ac2a19837 100644 --- a/api/src/services/wordpress.service.ts +++ b/api/src/services/wordpress.service.ts @@ -12,7 +12,7 @@ import { getLogMessage } from "../utils/index.js"; import { v4 as uuidv4 } from "uuid"; import { orgService } from "./org.service.js"; import * as cheerio from 'cheerio'; -import { setupWordPressBlocks, stripHtmlTags } from "../utils/wordpressParseUtil.js"; +import { hasMeaningfulHtmlContent, setupWordPressBlocks, stripHtmlTags } from "../utils/wordpressParseUtil.js"; import { getMimeTypeFromExtension } from "../utils/mimeTypes.js"; import { MEDIA_BLOCK_NAMES, WORDPRESS_MISSSING_BLOCKS } from "../constants/index.js"; @@ -171,12 +171,157 @@ function unwrapSingleChildGroup(block: any): any { return current; } +/** core/cover puts the image in attrs — not in innerBlocks; schema maps it to a file field otherCmsField "media". */ +function attachCoverBackgroundMediaToChildren( + coverBlock: any, + modularChild: any, + fields: any[], + assetData: any, + out: Record, +): void { + const url = String(coverBlock?.attrs?.url ?? '').trim(); + if (coverBlock?.blockName !== 'core/cover' || !url || !modularChild) return; + + const prefix = + modularChild?.contentstackFieldUid || getLastUid(modularChild.contentstackUid); + const mediaField = fields?.find( + (f: any) => + f?.contentstackFieldType === 'file' && + `${f?.contentstackFieldUid || ''}`?.startsWith(`${prefix}.`) && + ((f?.otherCmsField || '')?.toLowerCase() === 'media' || + (f?.otherCmsType || '')?.toLowerCase() === 'media'), + ); + if (!mediaField) return; + + const key = getLastUid(mediaField?.contentstackFieldUid); + if (out[key] != null && out[key] !== '') return; + + const asset = formatChildByType( + { + blockName: 'core/image', + attrs: { ...(coverBlock?.attrs || {}), url, src: url }, + innerHTML: coverBlock?.innerHTML, + innerBlocks: [], + }, + mediaField, + assetData, + ); + if (asset != null && asset !== '') out[key] = asset; +} + +function firstImgSrcFromInnerHtml(innerHtml?: string): string { + if (!innerHtml || typeof innerHtml !== 'string') return ''; + try { + const $ = cheerio.load(innerHtml); + return String($('img')?.first()?.attr('src') || '')?.trim(); + } catch { + return ''; + } +} + +/** core/media-text keeps mediaId / mediaType in attrs and image URL in innerHTML (not innerBlocks). */ +function attachMediaTextFieldsToChildren( + mediaTextBlock: any, + modularChild: any, + fields: any[], + assetData: any, + out: Record, +): void { + if (mediaTextBlock?.blockName !== 'core/media-text' || !modularChild) return; + + const prefix = + modularChild?.contentstackFieldUid || getLastUid(modularChild?.contentstackUid); + const attrs = mediaTextBlock?.attrs || {}; + + const mediaField = fields?.find( + (f: any) => + f?.contentstackFieldType === 'file' && + `${f?.contentstackFieldUid || ''}`.startsWith(`${prefix}.`) && + ((f?.otherCmsField || '').toLowerCase() === 'media' || + (f?.otherCmsType || '').toLowerCase() === 'media'), + ); + + const rawId = attrs?.mediaId ?? attrs?.media_id; + const idNum = Number(rawId); + const hasPositiveMediaId = + rawId != null && rawId !== '' && !Number.isNaN(idNum) && idNum > 0; + + const urlFromAttrs = String(attrs?.url ?? attrs?.src ?? '')?.trim(); + const urlFromMarkup = firstImgSrcFromInnerHtml(mediaTextBlock?.innerHTML); + const resolvedUrl = urlFromAttrs || urlFromMarkup; + + if (mediaField) { + const key = getLastUid(mediaField?.contentstackFieldUid); + const slotFree = out[key] == null || out[key] === ''; + const shouldAttach = slotFree && (hasPositiveMediaId || Boolean(resolvedUrl)); + if (shouldAttach) { + const asset = formatChildByType( + { + blockName: 'core/image', + attrs: { + ...attrs, + id: hasPositiveMediaId ? idNum : attrs?.id, + url: resolvedUrl || urlFromAttrs, + src: resolvedUrl || urlFromAttrs, + }, + innerHTML: mediaTextBlock?.innerHTML, + innerBlocks: [], + }, + mediaField, + assetData, + ); + if (asset != null && asset !== '') out[key] = asset; + } + } + + const mtRaw = attrs?.mediaType; + const mt = typeof mtRaw === 'string' ? mtRaw.trim() : mtRaw != null ? String(mtRaw).trim() : ''; + if (!mt) return; + + const mediatypeField = fields?.find( + (f: any) => + (f?.contentstackFieldType === 'single_line_text' || + f?.contentstackFieldType === 'text') && + `${f?.contentstackFieldUid || ''}`.startsWith(`${prefix}.`) && + (f?.otherCmsField || '')?.toLowerCase() === 'mediatype', + ); + if (!mediatypeField) return; + + const mtk = getLastUid(mediatypeField?.contentstackFieldUid); + if (out[mtk] != null && out[mtk] !== '') return; + + const textValue = formatChildByType( + { blockName: 'core/paragraph', attrs: {}, innerHTML: `

${mt}

`, innerBlocks: [] }, + mediatypeField, + assetData, + ); + if (textValue != null && textValue !== '') out[mtk] = textValue; +} + async function createSchema(fields: any, blockJson : any, title: string, uid: string, assetData: any, duplicateBlockMappings?: Record) { const schema : any = { title: title, uid: uid, //fields: fields?.fields, }; + + const cmsFieldMatchesWpBlockName = ( + otherCmsType: string | undefined, + otherCmsField: string | undefined, + wpRawName: string | undefined, + ): boolean => { + const primary = (wpRawName ?? "").toLowerCase(); + if (!primary) return false; + const mapped = + duplicateBlockMappings && typeof duplicateBlockMappings[primary] === "string" + ? duplicateBlockMappings[primary]?.toLowerCase() + : ""; + const candidates = + mapped && mapped !== primary ? [primary, mapped] : [primary]; + const t = otherCmsType?.toLowerCase() ?? ""; + const f = otherCmsField?.toLowerCase() ?? ""; + return candidates?.some((n) => n === t || n === f); + }; try { // Ensure blockJson is an array and fields is defined @@ -221,102 +366,172 @@ async function createSchema(fields: any, blockJson : any, title: string, uid: st return blockName === fieldName }); + let modularMatchFromDuplicateMap = false; + // Fallback: if no direct match, check duplicate block mappings if (!matchingModularBlockChild && duplicateBlockMappings) { - const mappedName = duplicateBlockMappings[blockName]; - + const blockKeyLc = blockName?.toLowerCase?.() ?? ""; + const mappedName = + duplicateBlockMappings[blockKeyLc] ?? duplicateBlockMappings[blockName]; + if (mappedName) { - matchingModularBlockChild = modularBlockChildren.find((childField: any) => { const fieldName = childField?.otherCmsField?.toLowerCase(); return mappedName === fieldName; }); - - //if (!matchingChildField) { - matchingChildField = fields.find((childField: any) => { - const fieldName = childField?.otherCmsField?.toLowerCase(); - const fieldType = childField?.otherCmsType?.toLowerCase(); - return (childField?.contentstackFieldType !== 'modular_blocks_child') && (mappedName === fieldName || mappedName === fieldType); - }); - - // } + + matchingChildField = fields.find((childField: any) => { + const fieldName = childField?.otherCmsField?.toLowerCase(); + const fieldType = childField?.otherCmsType?.toLowerCase(); + return ( + childField?.contentstackFieldType !== "modular_blocks_child" && + (mappedName === fieldName || mappedName === fieldType) + ); + }); + modularMatchFromDuplicateMap = !!( + matchingModularBlockChild && matchingChildField + ); } } - + + // Duplicate-map + single inner: new modular row; list item lives in mapped field only (does not merge into prior heading). + if ( + modularMatchFromDuplicateMap && + blockForProcessing?.innerBlocks?.length === 1 + ) { + const piece = formatChildByType( + unwrapSingleChildGroup(blockForProcessing.innerBlocks[0]), + matchingChildField, + assetData, + ); + if (piece != null && piece !== "") { + const mk = getLastUid(matchingModularBlockChild!.contentstackFieldUid); + const fk = getLastUid(matchingChildField!.contentstackFieldUid); + modularBlocksArray.push({ [mk]: { [fk]: piece } }); + continue; + } + } + //if (matchingChildField) { - // Process innerBlocks (children) if they exist - if (blockForProcessing?.innerBlocks?.length > 0 && Array.isArray(blockForProcessing?.innerBlocks) && matchingModularBlockChild?.uid) { + if (matchingModularBlockChild?.uid) { const childrenObject: Record = {}; - - blockForProcessing.innerBlocks.forEach((child: any, childIndex: number) => { - try { - const effectiveChild = unwrapSingleChildGroup(child); - const childBlockName = - getFieldName(resolvedBlockName(effectiveChild))?.toLowerCase() || - getFieldName(resolvedBlockName(effectiveChild)?.toLowerCase()); - // Find the field that matches this inner block - // Look for fields that belong to this modular_blocks_child - const childFieldUid = matchingModularBlockChild?.contentstackFieldUid || getLastUid(matchingModularBlockChild?.contentstackUid); - const childField = fields.find((f: any) => { - const fUid = f?.contentstackFieldUid || ''; - const fOtherCmsType = f?.otherCmsType?.toLowerCase(); - const fOtherCmsField = f?.otherCmsField?.toLowerCase(); - const childKey = getLastUid(f?.contentstackFieldUid); - const alreadyPopulated = childrenObject[childKey] !== undefined && childrenObject[childKey] !== null; - return fUid.startsWith(childFieldUid + '.') && - (fOtherCmsType === childBlockName || fOtherCmsField === childBlockName) && (!alreadyPopulated || f?.advanced?.multiple === true); - }); - - if (childField) { - const childKey = getLastUid(childField?.contentstackFieldUid); - - if (childField?.contentstackFieldType === 'group') { - - // Process group recursively - handles nested structures - const processedGroup = processNestedGroup(effectiveChild, childField, fields); - if (childField?.advanced?.multiple === true && processedGroup) { - if (Array.isArray(childrenObject[childKey])) { - childrenObject[childKey].push(processedGroup); + attachCoverBackgroundMediaToChildren( + blockForProcessing, + matchingModularBlockChild, + fields, + assetData, + childrenObject, + ); + attachMediaTextFieldsToChildren( + blockForProcessing, + matchingModularBlockChild, + fields, + assetData, + childrenObject, + ); + + const inners = blockForProcessing?.innerBlocks; + if (Array.isArray(inners) && inners?.length > 0) { + inners.forEach((child: any, childIndex: number) => { + try { + const effectiveChild = unwrapSingleChildGroup(child); + + const childBlockName = + getFieldName(resolvedBlockName(effectiveChild))?.toLowerCase() || + getFieldName(resolvedBlockName(effectiveChild)?.toLowerCase()); + const childFieldUid = + matchingModularBlockChild?.contentstackFieldUid || + getLastUid(matchingModularBlockChild?.contentstackUid); + const childField = fields.find((f: any) => { + const fUid = f?.contentstackFieldUid || ''; + const fOtherCmsType = f?.otherCmsType?.toLowerCase(); + const fOtherCmsField = f?.otherCmsField?.toLowerCase(); + const ck = getLastUid(f?.contentstackFieldUid); + const taken = childrenObject[ck] !== undefined && childrenObject[ck] !== null; + return ( + fUid.startsWith(childFieldUid + '.') && + cmsFieldMatchesWpBlockName( + fOtherCmsType, + fOtherCmsField, + childBlockName, + ) && + (!taken || f?.advanced?.multiple === true) + ); + }); + + if (childField) { + const childKey = getLastUid(childField?.contentstackFieldUid); + + if (childField?.contentstackFieldType === 'group') { + const processedGroup = processNestedGroup( + effectiveChild, + childField, + fields, + ); + if (childField?.advanced?.multiple === true && processedGroup) { + if (Array.isArray(childrenObject[childKey])) { + childrenObject[childKey].push(processedGroup); + } else { + childrenObject[childKey] = [processedGroup]; + } } else { - childrenObject[childKey] = [processedGroup]; + processedGroup && (childrenObject[childKey] = processedGroup); } - } else { - processedGroup && (childrenObject[childKey] = processedGroup); - } - - const formattedChild = formatChildByType(effectiveChild, childField, assetData); - - if (childField?.advanced?.multiple === true && formattedChild) { - if (Array.isArray(childrenObject[childKey])) { - childrenObject[childKey].push(formattedChild); + + const formattedChild = formatChildByType( + effectiveChild, + childField, + assetData, + ); + + if (childField?.advanced?.multiple === true && formattedChild) { + if (Array.isArray(childrenObject[childKey])) { + childrenObject[childKey]?.push(formattedChild); + } else { + childrenObject[childKey] = [formattedChild]; + } } else { - childrenObject[childKey] = [formattedChild]; + formattedChild && (childrenObject[childKey] = formattedChild); } } else { - formattedChild && (childrenObject[childKey] = formattedChild); + const formattedChild = formatChildByType( + effectiveChild, + childField, + assetData, + ); + if (childField?.advanced?.multiple === true && formattedChild) { + if (Array.isArray(childrenObject[childKey])) { + childrenObject[childKey]?.push(formattedChild); + } else { + childrenObject[childKey] = [formattedChild]; + } + } else { + formattedChild && (childrenObject[childKey] = formattedChild); + } } } + } catch (childError) { + console.warn(`Error processing child block at index ${childIndex}:`, childError); } - } catch (childError) { - console.warn(`Error processing child block at index ${childIndex}:`, childError); - } - }); - - // Add the block to the modular blocks array with the child field's UID as the key - if (Object?.keys(childrenObject)?.length > 0) { - modularBlocksArray.push({[getLastUid(matchingModularBlockChild?.contentstackFieldUid)] : childrenObject }); - } else if (getLastUid(matchingModularBlockChild?.contentstackFieldUid) && matchingChildField) { - // Fallback: inner blocks didn't match child fields (e.g., duplicate-mapped block with different inner block types) - - const formattedBlock = formatChildByType(blockForProcessing, matchingChildField, assetData); - formattedBlock && modularBlocksArray.push({[getLastUid(matchingModularBlockChild?.contentstackFieldUid)] : { [getLastUid(matchingChildField?.contentstackFieldUid)]: formattedBlock }}); + }); + } + + const modularKey = getLastUid(matchingModularBlockChild?.contentstackFieldUid); + if (Object.keys(childrenObject).length > 0) { + modularBlocksArray.push({ [modularKey]: childrenObject }); + } else if (modularKey && matchingChildField) { + const formattedBlock = formatChildByType( + blockForProcessing, + matchingChildField, + assetData, + ); + formattedBlock && + modularBlocksArray?.push({ + [modularKey]: { + [getLastUid(matchingChildField?.contentstackFieldUid)]: formattedBlock, + }, + }); } - } else if(getLastUid(matchingModularBlockChild?.contentstackFieldUid) && matchingChildField){ - // Handle blocks with no inner blocks - format the block itself - - const formattedBlock = formatChildByType(blockForProcessing, matchingChildField, assetData); - - formattedBlock && modularBlocksArray.push({[getLastUid(matchingModularBlockChild?.contentstackFieldUid)] : { [getLastUid(matchingChildField?.contentstackFieldUid)]: formattedBlock }}); } //} } catch (blockError) { @@ -528,8 +743,9 @@ function formatChildByType(child: any, field: any, assetData: any) { ? child?.innerHTML : child; } - const hasMeaningfulHtml = stripHtmlTags(htmlContent)?.trim()?.length > 0; - // Only set when there is text; do not assign `undefined` (avoids false from `a && fn()` in multi-RTE). + const hasMeaningfulHtml = hasMeaningfulHtmlContent(htmlContent); + + // Only set when there is visible text or media/embeds; do not assign `undefined` (avoids false from `a && fn()` in multi-RTE). if (hasMeaningfulHtml) { formatted = RteJsonConverter(htmlContent); } @@ -537,7 +753,13 @@ function formatChildByType(child: any, field: any, assetData: any) { } case 'html': - formatted = child?.blockName ? formatted ?? child?.innerHTML : `

${child}

`; + const htmlContent = child?.blockName ? (formatted ?? child?.innerHTML) : `

${child?.innerHTML}

`; + const hasMeaningfulHtml = hasMeaningfulHtmlContent(htmlContent); + + if(hasMeaningfulHtml){ + formatted = htmlContent + } + break; case 'link': { @@ -580,7 +802,8 @@ function formatChildByType(child: any, field: any, assetData: any) { case 'file': { // Extract media URL from innerHTML: img (core/image) or audio/source (core/audio) let fileName = ''; - let imgUrl = child?.attrs?.src; + let imgUrl = child?.attrs?.src ?? child?.attrs?.url; + let id = child?.attrs?.id; const innerHtml = child?.innerHTML; if (innerHtml && typeof innerHtml === 'string') { @@ -632,7 +855,8 @@ function formatChildByType(child: any, field: any, assetData: any) { const urlParts = imgUrl.split('/'); fileName = urlParts[urlParts.length - 1].split('?')[0]; } - const asset = assetData[fileName?.replace(/-/g, '_')?.toLowerCase()]; + + const asset = assetData[`assets_${id}`] || assetData[fileName?.replace(/-/g, '_')?.toLowerCase()]; formatted = asset; break; } @@ -782,24 +1006,6 @@ async function saveEntry(fields: any, entry: any, file_path: string, assetData const contentEncoded = $(xmlItem)?.find("content\\:encoded")?.text() || ''; const blocksJson = await setupWordPressBlocks(contentEncoded); - try { - const blocksDir = path.join( - MIGRATION_DATA_CONFIG.DATA, - destinationStackId, - MIGRATION_DATA_CONFIG.WORDPRESS_BLOCKS_DIR_NAME - ); - await fs.promises.mkdir(blocksDir, { recursive: true }); - await writeFileAsync(path.join(blocksDir, `${uid}.json`), blocksJson, 4); - } catch (writeErr) { - customLogger( - project?.id, - destinationStackId, - 'warn', - `Failed to write wordpress blocks JSON for ${uid}: ${ - writeErr instanceof Error ? writeErr.message : String(writeErr) - }` - ); - } customLogger(project?.id, destinationStackId,'info', `Processed blocks for entry ${uid}`); @@ -925,8 +1131,11 @@ async function createEntry(file_path: string, packagePath: string, destinationSt await fs.promises.mkdir(postFolderPath, { recursive: true }); } const contentTypeUid = contentType?.contentstackTitle?.toLowerCase(); - const entry = entries?.filter((entry: any) => { - return entry?.['wp:post_type']?.toLowerCase() === contentTypeUid; + const statusArray = ["publish", "inherit"]; + const entry = entries?.filter((data: any) => { + const matchesType = data?.["wp:post_type"]?.toLowerCase() === contentTypeUid; + const matchesStatus = statusArray.includes(data?.["wp:status"]); + return matchesType && matchesStatus; }); const content = await saveEntry(contentType?.fieldMapping, entry,file_path, assetData, allCategories, master_locale, destinationStackId, project, allTerms, contentType?.duplicateBlockMappings) || {}; @@ -1615,7 +1824,7 @@ async function saveAssetFromUrl( // Use customId as filename to ensure uniqueness, preserve extension const filename = `${customId}${fileExtension}`; - const assetPath = path.resolve(assetsSave, "files",customId, filename); + const assetPath = path.resolve(assetsSave, "files", customId); // Check if asset already exists if (fs.existsSync(assetPath)) { @@ -1632,14 +1841,14 @@ async function saveAssetFromUrl( }); // Ensure files directory exists - fs.mkdirSync( + await fs.promises.mkdir( path.resolve(assetsSave, "files", customId), { recursive: true } ); - fs.writeFileSync(assetPath, response.data); + await fs.promises.writeFile(path.resolve(assetsSave, "files", customId, filename), response?.data); - const stats = fs.lstatSync(assetPath); + const stats = fs.lstatSync(path.resolve(assetsSave, "files", customId, filename)); const acc: any = {}; const key = customId; diff --git a/api/src/utils/mimeTypes.ts b/api/src/utils/mimeTypes.ts index 92dadcbac..6c8ef9e89 100644 --- a/api/src/utils/mimeTypes.ts +++ b/api/src/utils/mimeTypes.ts @@ -37,7 +37,7 @@ export const EXT_TO_MIME_MAP: Record = { * Return the MIME type for a given file extension, or `undefined` if unknown. */ export function getMimeTypeFromExtension(ext: string): string | undefined { - return EXT_TO_MIME_MAP[ext.toLowerCase()]; + return EXT_TO_MIME_MAP[ext?.toLowerCase()]; } export default EXT_TO_MIME_MAP; diff --git a/api/src/utils/wordpressParseUtil.ts b/api/src/utils/wordpressParseUtil.ts index 11fee638b..4573abed8 100644 --- a/api/src/utils/wordpressParseUtil.ts +++ b/api/src/utils/wordpressParseUtil.ts @@ -126,3 +126,44 @@ export const stripHtmlTags = (htmlString: string | null | undefined): string => return htmlString.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); } }; + +const MEDIA_AND_EMBED_SELECTOR = [ + 'img[src]', + 'img[data-src]', + 'img[srcset]', + 'picture', + 'video', + 'audio', + 'iframe[src]', + 'iframe[srcdoc]', + 'embed[src]', + 'object[data]', + 'canvas', + 'svg', +].join(', '); + +/** + * True when HTML has visible text, or substantive non-text markup (media, embeds, etc.). + * Pure whitespace / empty paragraphs / br-only crumbs are treated as empty. + */ +export const hasMeaningfulHtmlContent = ( + htmlString: string | null | undefined, +): boolean => { + if (!htmlString || typeof htmlString !== 'string' || !htmlString.trim()) { + return false; + } + if (stripHtmlTags(htmlString)?.length > 0) { + return true; + } + try { + ensureDomGlobals(); + const doc = (global as any).document; + const tempDiv = doc.createElement('div'); + tempDiv.innerHTML = htmlString; + return tempDiv.querySelector(MEDIA_AND_EMBED_SELECTOR) !== null; + } catch { + return /<(img|picture|video|audio|iframe|embed|object|canvas|svg)\b/i.test( + htmlString, + ); + } +}; diff --git a/upload-api/migration-wordpress/libs/extractItems.ts b/upload-api/migration-wordpress/libs/extractItems.ts index b00df3401..594fe6179 100644 --- a/upload-api/migration-wordpress/libs/extractItems.ts +++ b/upload-api/migration-wordpress/libs/extractItems.ts @@ -44,7 +44,9 @@ function shouldSkipCrossBlockDedupe( const b = (newOtherCmsField || '').toLowerCase(); return ( (a === 'cover' && b === 'paragraph') || - (a === 'paragraph' && b === 'cover') + (a === 'paragraph' && b === 'cover') || + (a === 'media-text' && b === 'paragraph') || + (a === 'paragraph' && b === 'media-text') ); } @@ -71,8 +73,8 @@ function isSameStructure(obj1: any, obj2: any): boolean { const n1 = obj1?.name; const n2 = obj2?.name; if ( - (n1 === 'core/cover' && n2 === 'core/paragraph') || - (n1 === 'core/paragraph' && n2 === 'core/cover') + ((n1 === 'core/cover' || n1 === 'core/media-text') && n2 === 'core/paragraph') || + (n1 === 'core/paragraph' && (n2 === 'core/cover' || n2 === 'core/media-text')) ) { return false; } @@ -237,10 +239,10 @@ function getLastUid(uid : string) { return uid?.split?.('.')?.[uid?.split?.('.')?.length - 1]; } -/** Passed to schemaMapper — must be the block itself when switch cases use processInnerBlocks(inner block). If we only pass innerBlocks, parent cases (e.g. core/cover) never run. */ +/** Passed to schemaMapper — must be the block itself when switch cases use processInnerBlocks(inner block). If we only pass innerBlocks, parent cases (e.g. core/cover, core/media-text) never run. */ function rootBlockForSchemaMapper(field: any) { if (!field?.innerBlocks?.length) return field; - if (field?.name === 'core/cover') return field; + if (field?.name === 'core/cover' || field?.name === 'core/media-text') return field; return field.innerBlocks; } diff --git a/upload-api/migration-wordpress/libs/schemaMapper.ts b/upload-api/migration-wordpress/libs/schemaMapper.ts index 7a50bb008..d0aea2fea 100644 --- a/upload-api/migration-wordpress/libs/schemaMapper.ts +++ b/upload-api/migration-wordpress/libs/schemaMapper.ts @@ -593,7 +593,59 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: } } - + + case 'core/media-text': { + const mediaTextSchema: Field[] = []; + const mediaTextUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}` : getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix); + const innerBlocks = + key?.innerBlocks && key?.innerBlocks?.length > 0 + ? await processInnerBlocks(key, parentUid, parentFieldName, affix) + : []; + const mediaId = key?.attributes?.mediaId; + const hasMediaAttr = + mediaId != null && mediaId !== '' && Number(mediaId) > 0; + + if (!hasMediaAttr && innerBlocks?.length === 0) { + return []; + } + + if (hasMediaAttr) { + mediaTextSchema.push( + { + uid: mediaTextUid, + otherCmsField: 'media', + otherCmsType: getFieldName(key?.attributes?.metadata?.name ?? key?.name), + contentstackField: `${parentFieldName} > media`, + contentstackFieldUid: mediaTextUid, + contentstackFieldType: 'file', + backupFieldType: 'file', + backupFieldUid: mediaTextUid, + advanced: {}, + }, + { + uid: mediaTextUid, + otherCmsField: 'mediatype', + otherCmsType: getFieldName(key?.attributes?.metadata?.name ?? key?.name), + contentstackField: `${parentFieldName} > mediatype`, + contentstackFieldUid: mediaTextUid, + contentstackFieldType: 'single_line_text', + backupFieldType: 'single_line_text', + backupFieldUid: mediaTextUid, + advanced: {}, + } + ); + } + innerBlocks?.forEach((schemaObj) => { + if (schemaObj) { + if (Array.isArray(schemaObj)) { + mediaTextSchema.push(...schemaObj); + } else { + mediaTextSchema.push(schemaObj); + } + } + }); + return mediaTextSchema; + } } return []; diff --git a/upload-api/src/controllers/wordpress/index.ts b/upload-api/src/controllers/wordpress/index.ts index df35f3f68..a8ee43bca 100644 --- a/upload-api/src/controllers/wordpress/index.ts +++ b/upload-api/src/controllers/wordpress/index.ts @@ -12,9 +12,30 @@ const createWordpressMapper = async (filePath: string = "", projectId: string | try { const localeData = await extractLocale(filePath); + const mapperConfig = { + method: 'post', + maxBodyLength: Infinity, + url: `${process.env.NODE_BACKEND_API}/v2/migration/localeMapper/${projectId}`, + headers: { + app_token, + 'Content-Type': 'application/json' + }, + data: { + locale:Array.from(localeData) + }, + }; + + const mapRes = await axios.request(mapperConfig) + if(mapRes?.status==200){ + logger.info('Legacy CMS', { + status: HTTP_CODES?.OK, + message: HTTP_TEXTS?.LOCALE_SAVED, + }); + } + const contentTypeData : any = await extractContentTypes(affix as string, filePath, config); //const contentTypeData = await contentTypeMaker(affix, filePath) - + if(contentTypeData){ const fieldMapping: any = { contentTypes: [], extractPath: filePath }; contentTypeData.forEach((contentType: any) => { @@ -22,7 +43,7 @@ const createWordpressMapper = async (filePath: string = "", projectId: string | jsonfileContent.type = "content_type"; fieldMapping?.contentTypes?.push(jsonfileContent); }) - + const config = { method: 'post', maxBodyLength: Infinity, @@ -40,29 +61,7 @@ const createWordpressMapper = async (filePath: string = "", projectId: string | status: HTTP_CODES?.OK, message: HTTP_TEXTS?.MAPPER_SAVED }); - - } - - - const mapperConfig = { - method: 'post', - maxBodyLength: Infinity, - url: `${process.env.NODE_BACKEND_API}/v2/migration/localeMapper/${projectId}`, - headers: { - app_token, - 'Content-Type': 'application/json' - }, - data: { - locale:Array.from(localeData) - }, - }; - const mapRes = await axios.request(mapperConfig) - if(mapRes?.status==200){ - logger.info('Legacy CMS', { - status: HTTP_CODES?.OK, - message: HTTP_TEXTS?.LOCALE_SAVED, - }); } } } catch (err: any) { diff --git a/upload-api/tests/unit/controllers/wordpress.controller.test.ts b/upload-api/tests/unit/controllers/wordpress.controller.test.ts index 86abbc044..b973de937 100644 --- a/upload-api/tests/unit/controllers/wordpress.controller.test.ts +++ b/upload-api/tests/unit/controllers/wordpress.controller.test.ts @@ -26,9 +26,10 @@ describe('createWordpressMapper', () => { it('should extract content types and send mapper to API', async () => { mockExtractLocale.mockResolvedValue(['en']); mockExtractContentTypes.mockResolvedValue([{ uid: 'post', title: 'Post' }]); + // localeMapper runs first; createDummyData second—order must match controller mockAxiosRequest - .mockResolvedValueOnce({ data: { data: { content_mapper: [1] } } }) - .mockResolvedValueOnce({ status: 200 }); + .mockResolvedValueOnce({ status: 200, data: {} }) + .mockResolvedValueOnce({ data: { data: { content_mapper: [1] } } }); await createWordpressMapper('/path', 'proj-1', 'token', 'csm', {}); @@ -37,12 +38,14 @@ describe('createWordpressMapper', () => { expect(mockDeleteFolderSync).toHaveBeenCalled(); }); - it('should not send mapper when contentTypeData is falsy', async () => { + it('should only send localeMapper when contentTypeData is falsy', async () => { mockExtractLocale.mockResolvedValue([]); mockExtractContentTypes.mockResolvedValue(null); await createWordpressMapper('/path', 'proj-1', 'token', 'csm', {}); - expect(mockAxiosRequest).not.toHaveBeenCalled(); + // Locale mapper is always POSTed; content-type mapper is skipped when extract returns falsy + expect(mockAxiosRequest).toHaveBeenCalledTimes(1); + expect(mockAxiosRequest.mock.calls[0][0].url).toContain('localeMapper'); }); it('should handle error gracefully', async () => { @@ -61,12 +64,14 @@ describe('createWordpressMapper', () => { mockExtractLocale.mockResolvedValue(['en']); mockExtractContentTypes.mockResolvedValue([{ uid: 'post' }, { uid: 'page' }]); mockAxiosRequest - .mockResolvedValueOnce({ data: { data: { content_mapper: [1] } } }) - .mockResolvedValueOnce({ status: 200 }); + .mockResolvedValueOnce({ status: 200, data: {} }) + .mockResolvedValueOnce({ data: { data: { content_mapper: [1] } } }); await createWordpressMapper('/path', 'proj-1', 'token', 'csm', {}); - const payload = JSON.parse(mockAxiosRequest.mock.calls[0][0].data); + const contentTypeRequest = mockAxiosRequest.mock.calls[1][0]; + expect(contentTypeRequest.url).toContain('createDummyData'); + const payload = JSON.parse(contentTypeRequest.data as string); expect(payload.contentTypes[0].type).toBe('content_type'); expect(payload.contentTypes[1].type).toBe('content_type'); });