From 2298f49a645103e9a4535fc5509d8be59dbc00e5 Mon Sep 17 00:00:00 2001 From: shobhit upadhyay Date: Thu, 13 Nov 2025 18:02:36 +0530 Subject: [PATCH 1/3] Enhance AEM service with improved error handling, added carousel component scanning, and updated field processing logic. Introduced UID sanitization and refined schema building for carousel items. Updated package dependencies for better type support. Bug FIx - CMG-742, CMG-744, CMG-745, CMG-746, CMG-755 --- api/src/services/aem.service.ts | 638 +++++++++++++----- upload-api/migration-aem/helper/index.ts | 84 ++- .../components/CarouselComponent.ts | 307 +++++++-- .../contentType/components/TeaserComponent.ts | 205 ++++-- .../components/TextBannerComponent.ts | 40 +- .../fields/contentstackFields/index.ts | 8 +- .../migration-aem/libs/contentType/index.ts | 73 +- 7 files changed, 1033 insertions(+), 322 deletions(-) diff --git a/api/src/services/aem.service.ts b/api/src/services/aem.service.ts index 8631dd52e..455f75151 100644 --- a/api/src/services/aem.service.ts +++ b/api/src/services/aem.service.ts @@ -1,3 +1,6 @@ +/* eslint-disable no-inner-declarations */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-unsafe-optional-chaining */ import fs from 'fs'; import path from 'path'; import read from 'fs-readdir-recursive'; @@ -39,7 +42,6 @@ interface FieldMapping { interface Project { master_locale?: object; locales?: object; - // add other properties as needed } interface ContentType { @@ -89,44 +91,35 @@ interface AssetJSON { async function isAssetJsonCreated(assetJsonPath: string): Promise { try { await fs.promises.access(assetJsonPath, fs.constants.F_OK); - return true; // File exists + return true; } catch { - return false; // File does not exist + return false; } } -// Helper function to sanitize data and remove unwanted HTML tags and other chars function stripHtmlTags(html: string): string { if (!html || typeof html !== 'string') return ''; - // Use JSDOM to parse and extract text content const dom = new JSDOM(html); const text = dom.window.document.body.textContent || ''; - // Clean up extra whitespace and newlines return text.trim().replace(/\s+/g, ' '); } -// Helper Function to extract value from items object based on the fieldName function getFieldValue(items: any, fieldName: string): any { if (!items || !fieldName) return undefined; - // Try exact match first if (items[fieldName] !== undefined) { return items[fieldName]; } - // Try camelCase conversion (snake_case → camelCase) - // Handle both single letter and multiple letter segments const camelCaseFieldName = fieldName?.replace(/_([a-z]+)/gi, (_, letters) => { - // Capitalize first letter, keep rest as-is for acronyms return letters?.charAt(0)?.toUpperCase() + letters?.slice(1); }); if (items[camelCaseFieldName] !== undefined) { return items[camelCaseFieldName]; } - // Try all uppercase version for acronyms const acronymVersion = fieldName?.replace(/_([a-z]+)/gi, (_, letters) => { return letters?.toUpperCase(); }); @@ -134,7 +127,6 @@ function getFieldValue(items: any, fieldName: string): any { return items[acronymVersion]; } - // Try case-insensitive match as last resort const itemKeys = Object.keys(items); const matchedKey = itemKeys?.find(key => key.toLowerCase() === fieldName?.toLowerCase()); if (matchedKey && items[matchedKey] !== undefined) { @@ -144,7 +136,6 @@ function getFieldValue(items: any, fieldName: string): any { return undefined; } - function getActualFieldUid(uid: string, fieldUid: string): string { if (RESERVED_FIELD_MAPPINGS[uid]) { return RESERVED_FIELD_MAPPINGS[uid]; @@ -155,12 +146,6 @@ function getActualFieldUid(uid: string, fieldUid: string): string { return uid; } -/** - * Finds and returns the asset object from assetJsonData where assetPath matches the given string. - * @param assetJsonData - The asset JSON data object. - * @param assetPathToFind - The asset path string to match. - * @returns The matching AssetJSON object, or undefined if not found. - */ function findAssetByPath( assetJsonData: Record, assetPathToFind: string @@ -219,11 +204,11 @@ function slugify(text: unknown): string { if (typeof text !== 'string') return ''; return text .toLowerCase() - .replace(/\|/g, '') // Remove pipe characters - .replace(/[^\w\s-]/g, '') // Remove non-word, non-space, non-hyphen chars - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/-+/g, '-') // Replace multiple hyphens with one - .replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens + .replace(/\|/g, '') + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-+|-+$/g, ''); } function addEntryToEntriesData( @@ -241,11 +226,6 @@ function addEntryToEntriesData( entriesData[contentTypeUid][mappedLocale].push(entryObj); } -/** - * Extracts the current locale from the given parseData object. - * @param parseData The parsed data object from the JSON file. - * @returns The locale string if found, otherwise undefined. - */ function getCurrentLocale(parseData: any): string | undefined { if (parseData.language) { return parseData.language; @@ -260,7 +240,6 @@ function getLocaleFromMapper(mapper: Record, locale: string): st return Object.keys(mapper).find(key => mapper[key] === locale); } - const deepFlattenObject = (obj: any, prefix = '', res: any = {}) => { if (Array.isArray(obj) || (obj && typeof obj === 'object')) { const entries = Array.isArray(obj) ? obj.map((v, i) => [i, v]) @@ -285,13 +264,9 @@ export function isImageType(path: string): boolean { export function isExperienceFragment(data: any) { if (data?.templateType && data[':type']) { - // Check templateType starts with 'xf-' const hasXfTemplate = data?.templateType?.startsWith('xf-'); - - // Check :type contains 'components/xfpage' const hasXfComponent = data[':type']?.includes('components/xfpage'); - // Return analysis return { isXF: hasXfTemplate || hasXfComponent, confidence: (hasXfTemplate && hasXfComponent) ? 'high' @@ -305,32 +280,17 @@ export function isExperienceFragment(data: any) { return null; } - -/** - * Ensures the directory exists at the given path. - * If it does not exist, creates it recursively. - * @param assetsSave - The relative path to the assets directory. - */ async function ensureAssetsDirectory(assetsSave: string): Promise { const fullPath = path.join(process.cwd(), assetsSave); try { await fs.promises.access(fullPath); - // Directory exists console.info(`Directory exists: ${fullPath}`); } catch (err) { - // Directory does not exist, create it await fs.promises.mkdir(fullPath, { recursive: true }); console.info(`Directory created: ${fullPath}`); } } - -/** - * Fetch files from a directory based on a query. - * @param dirPath - The directory to search in. - * @param query - The query string (filename, extension, or regex). - * @returns Array of matching file paths. - */ export async function fetchFilesByQuery( dirPath: string, query: string | RegExp @@ -361,7 +321,11 @@ function addUidToEntryMapping( entryMapping[contentType.contentstackUid].push(uid); } +function uidCorrector(str: string): string { + return str?.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase(); +} +declare const contentstackComponents: Record | undefined; const createAssets = async ({ destinationStackId, @@ -375,12 +339,11 @@ const createAssets = async ({ await ensureAssetsDirectory(assetsSave); const assetsDir = path.resolve(packagePath); - const allAssetJSON: Record = {}; // UID-based index.json - const pathToUidMap: Record = {}; // Path-to-UID mapping + const allAssetJSON: Record = {}; + const pathToUidMap: Record = {}; const seenFilenames = new Map(); const pathToFilenameMap = new Map(); - // Discover assets and deduplicate by filename for await (const fileName of read(assetsDir)) { const filePath = path.join(assetsDir, fileName); if (filePath?.startsWith?.(damPath)) { @@ -405,10 +368,8 @@ const createAssets = async ({ const parseData = JSON.parse(contentAst); const filename = parseData?.asset?.name; - // Store mapping from this AEM path to filename pathToFilenameMap.set(value, filename); - // Only create asset ONCE per unique filename if (!seenFilenames?.has(filename)) { const uid = uuidv4?.()?.replace?.(/-/g, ''); const blobPath = firstJson?.replace?.('.metadata.json', ''); @@ -418,8 +379,6 @@ const createAssets = async ({ metadata: parseData, blobPath }); - } else { - console.info(`Reusing asset: ${filename} → ${seenFilenames?.get(filename)?.uid}`); } } } @@ -432,13 +391,12 @@ const createAssets = async ({ } } - // Create physical asset files (one per unique filename) for (const [filename, assetInfo] of seenFilenames?.entries()) { const { uid, metadata, blobPath } = assetInfo; - const nameWithoutExt = typeof filename === 'string' - ? filename.split('.').slice(0, -1).join('.') + const nameWithoutExt = typeof filename === 'string' + ? filename.split('.').slice(0, -1).join('.') : filename; - + try { const assets = fs.readFileSync(path.join(blobPath)); fs.mkdirSync(path.join(assetsSave, 'files', uid), { @@ -448,14 +406,14 @@ const createAssets = async ({ path.join(process.cwd(), assetsSave, 'files', uid, filename), assets ); - + const message = getLogMessage( srcFunc, `Asset "${filename}" has been successfully transformed.`, {} ); await customLogger(projectId, destinationStackId, 'info', message); - + } catch (err) { console.error(`Failed to create asset: ${filename}`, err); const message = getLogMessage( @@ -467,30 +425,26 @@ const createAssets = async ({ await customLogger(projectId, destinationStackId, 'error', message); } } - - // Track first path for each asset and build mappings + const assetFirstPath = new Map(); - - // Build path-to-UID mapping (ALL paths map to the SAME deduplicated UID) + for (const [aemPath, filename] of pathToFilenameMap.entries()) { const assetInfo = seenFilenames?.get(filename); if (assetInfo) { pathToUidMap[aemPath] = assetInfo.uid; - - // Track first path for index.json + if (!assetFirstPath.has(assetInfo.uid)) { assetFirstPath.set(assetInfo.uid, aemPath); } } } - - // Create UID-based index.json + for (const [filename, assetInfo] of seenFilenames?.entries()) { const { uid, metadata } = assetInfo; - const nameWithoutExt = typeof filename === 'string' - ? filename?.split('.').slice(0, -1).join('.') + const nameWithoutExt = typeof filename === 'string' + ? filename?.split('.').slice(0, -1).join('.') : filename; - + allAssetJSON[uid] = { urlPath: `/assets/${uid}`, uid: uid, @@ -509,20 +463,17 @@ const createAssets = async ({ }; } - // Write files const fileMeta = { '1': ASSETS_SCHEMA_FILE }; await fs.promises.writeFile( path.join(process.cwd(), assetsSave, ASSETS_FILE_NAME), JSON.stringify(fileMeta) ); - - // index.json - UID-based + await fs.promises.writeFile( path.join(process.cwd(), assetsSave, ASSETS_SCHEMA_FILE), JSON.stringify(allAssetJSON, null, 2) ); - - // path-mapping.json - For entry transformation + await fs.promises.writeFile( path.join(process.cwd(), assetsSave, 'path-mapping.json'), JSON.stringify(pathToUidMap, null, 2) @@ -530,16 +481,16 @@ const createAssets = async ({ }; function processFieldsRecursive( - fields: any[], - items: any, - title: string, + fields: any[], + items: any, + title: string, pathToUidMap: Record, assetDetailsMap: Record ) { if (!fields) return; const obj: any = {}; const data: any = []; - + for (const field of fields) { switch (field?.contentstackFieldType) { case 'modular_blocks': { @@ -552,58 +503,304 @@ function processFieldsRecursive( } break; } - + case 'modular_blocks_child': { - for (const [, value] of Object.entries(items)) { - const objData: any = {}; - const typeValue = (value as { [key: string]: string })[':type']; + const list: any[] = (() => { + if (Array.isArray(items)) return items; + const order = Array.isArray(items?.[':itemsOrder']) ? items[':itemsOrder'] : null; + const map = items?.[':items'] || items; + if (order && map) return order.map((k: string) => map?.[k]).filter(Boolean); + return Object.values(map || {}); + })(); + + const uid = getLastKey(field?.contentstackFieldUid); + const blockTypeUid = field?.uid; + + for (const value of list) { + if (!value || typeof value !== 'object') continue; + const typeValue = (value as any)[':type'] || ''; const getTypeComp = getLastKey(typeValue, '/'); - const uid = getLastKey(field?.contentstackFieldUid); - if (getTypeComp === field?.uid) { - const compValue = processFieldsRecursive(field.schema, value, title, pathToUidMap, assetDetailsMap); - if (Object?.keys?.(compValue)?.length) { - objData[uid] = compValue; - data?.push(objData); - } + if (getTypeComp !== blockTypeUid) continue; + + const compValue = processFieldsRecursive(field.schema, value, title, pathToUidMap, assetDetailsMap); + if (compValue && Object.keys(compValue).length) { + const objData: any = {}; + objData[uid] = compValue; + data.push(objData); } } break; } - + case 'group': { - const groupData: unknown[] = []; - const groupValue = items?.[field?.uid]?.items ?? items?.[field?.uid]; const uid = getLastKey(field?.contentstackFieldUid); - if (Array.isArray(groupValue)) { - for (const element of groupValue) { + + const isMultiple = + (field?.multiple === true) || + (field?.advanced && field.advanced.multiple === true) || + (field?.maxInstance && field.maxInstance > 1); + + const isCarouselItems = + uid === 'items' && + items?.[':items'] && + items?.[':itemsOrder']; + + const carouselPreferredTypeHint = + (items && (items['activeItem'] || items['active_item'])) || + (items && items[':type'] && String(items[':type']).split('/').pop()) || + null; + + let groupValue; + if (isCarouselItems) { + groupValue = items; + } else { + groupValue = items?.[field?.uid]?.items ?? items?.[field?.uid]; + } + + if (isMultiple) { + const groupData: any[] = []; + + if (isCarouselItems) { + const order = items[':itemsOrder'] as string[]; + const map = items[':items'] as Record; + if (!Array.isArray(order) || !map) { + obj[uid] = groupData; + break; + } + + const childSchemasByUid = new Map(); if (Array.isArray(field?.schema)) { - const value = processFieldsRecursive(field.schema, element, title, pathToUidMap, assetDetailsMap); - groupData?.push(value); + for (const child of field.schema) { + const childUid = getLastKey(child?.contentstackFieldUid) || child?.uid; + if (childUid) childSchemasByUid.set(childUid, child); + if (child?.title) childSchemasByUid.set(uidCorrector(child.title), child); + } } + + const globalCanonical = (typeof contentstackComponents !== 'undefined' && contentstackComponents) ? contentstackComponents : null; + const canonicalTeaserFromGlobal = globalCanonical && (globalCanonical['teaser'] || globalCanonical['Teaser'] || globalCanonical[uidCorrector('teaser')]) || null; + + function buildMinimalSchemaFromSlide(slide: any = {}, hintSchema: any = null) { + const fields: any[] = []; + + if (hintSchema && typeof hintSchema === 'object') { + const candidateKeys = Object.keys(hintSchema).filter(k => !k.startsWith(':') && k !== 'id' && k !== 'dataLayer').slice(0, 12); + for (const k of candidateKeys) { + fields.push({ uid: k, contentstackFieldUid: k, contentstackFieldType: 'single_line_text' }); + } + if (fields.length) return fields; + } + + if (slide?.title || slide?.titleType) fields.push({ uid: 'title', contentstackFieldUid: 'title', contentstackFieldType: 'single_line_text' }); + if (slide?.description) fields.push({ uid: 'description', contentstackFieldUid: 'description', contentstackFieldType: 'single_line_text' }); + if (slide?.imagePath || slide?.src || slide?.image) { + fields.push({ uid: 'src', contentstackFieldUid: 'src', contentstackFieldType: 'file' }); + fields.push({ uid: 'alt', contentstackFieldUid: 'alt', contentstackFieldType: 'single_line_text' }); + } + if (slide?.linkURL || slide?.actions) fields.push({ uid: 'linkURL', contentstackFieldUid: 'linkURL', contentstackFieldType: 'single_line_text' }); + + if (fields.length === 0) fields.push({ uid: 'content', contentstackFieldUid: 'content', contentstackFieldType: 'single_line_text' }); + + return fields; + } + + function findSchemaCandidate(candidateKey: string | null) { + if (!candidateKey) return null; + const cand = String(candidateKey); + const variants = [ + cand, + cand.toLowerCase(), + uidCorrector(cand), + cand.replace(/[^a-zA-Z0-9]/g, ''), + cand.split('_')[0], + cand.split('-')[0] + ]; + for (const v of variants) { + if (childSchemasByUid.has(v)) return childSchemasByUid.get(v); + } + return null; + } + + const preferredHint = carouselPreferredTypeHint ? String(carouselPreferredTypeHint).split('/').pop() : null; + + if (!childSchemasByUid.has('teaser') && canonicalTeaserFromGlobal) { + childSchemasByUid.set('teaser', canonicalTeaserFromGlobal); + } + + for (const key of order) { + const slide = map?.[key]; + if (!slide || typeof slide !== 'object') { + continue; + } + + const explicitType = slide[':type'] || slide[':Type'] || null; + let typeTail = explicitType ? String(explicitType).split('/').pop() : ''; + + if (!typeTail) { + const fromKeyMatch = String(key).match(/^[a-zA-Z]+/); + if (fromKeyMatch) typeTail = fromKeyMatch[0]; + } + if (!typeTail && preferredHint) typeTail = preferredHint; + typeTail = String(typeTail || '').toLowerCase(); + + let targetSchema = findSchemaCandidate(typeTail); + + if (!targetSchema) { + if (typeTail.includes('image')) targetSchema = findSchemaCandidate('image'); + else if (typeTail.includes('teaser')) targetSchema = findSchemaCandidate('teaser'); + else if (typeTail.includes('button')) targetSchema = findSchemaCandidate('button'); + else if (typeTail.includes('textbanner')) targetSchema = findSchemaCandidate('textBanner'); + else if (typeTail.includes('title')) targetSchema = findSchemaCandidate('title'); + else if (typeTail.includes('text')) targetSchema = findSchemaCandidate('text'); + else if (typeTail.includes('search')) targetSchema = findSchemaCandidate('search'); + else if (typeTail.includes('separator')) targetSchema = findSchemaCandidate('separator'); + else if (typeTail.includes('spacer')) targetSchema = findSchemaCandidate('spacer'); + } + + if (!targetSchema && preferredHint) { + targetSchema = findSchemaCandidate(preferredHint) || findSchemaCandidate(uidCorrector(String(preferredHint))); + } + + if (!targetSchema && canonicalTeaserFromGlobal) { + targetSchema = canonicalTeaserFromGlobal; + } + + if (!targetSchema && childSchemasByUid.size > 0) { + targetSchema = Array.from(childSchemasByUid.values())[0]; + } + + if (!targetSchema) { + const generatedSchema = buildMinimalSchemaFromSlide(slide); + const processed = processFieldsRecursive(generatedSchema, slide, title, pathToUidMap, assetDetailsMap); + if (processed && Object.keys(processed).length) { + groupData.push(processed); + } + continue; + } + + let schemaArray = null; + if (Array.isArray(targetSchema)) schemaArray = targetSchema; + else if (Array.isArray(targetSchema?.schema)) schemaArray = targetSchema.schema; + else if (Array.isArray(targetSchema?.fields)) schemaArray = targetSchema.fields; + else if (Array.isArray(targetSchema?.schemaFields)) schemaArray = targetSchema.schemaFields; + else if (Array.isArray(targetSchema?.content)) schemaArray = targetSchema.content; + else if (Array.isArray(targetSchema?.schemaDefinition)) schemaArray = targetSchema.schemaDefinition; + + if (!schemaArray || schemaArray.length === 0) { + const generatedSchema = buildMinimalSchemaFromSlide(slide, targetSchema); + const processed = processFieldsRecursive(generatedSchema, slide, title, pathToUidMap, assetDetailsMap); + if (processed && Object.keys(processed).length) { + groupData.push(processed); + } + continue; + } + + try { + const processed = processFieldsRecursive(schemaArray, slide, title, pathToUidMap, assetDetailsMap); + + if (processed && Object.keys(processed).length) { + groupData.push(processed); + } else { + const generatedSchema2 = buildMinimalSchemaFromSlide(slide, targetSchema); + const processed2 = processFieldsRecursive(generatedSchema2, slide, title, pathToUidMap, assetDetailsMap); + if (processed2 && Object.keys(processed2).length) { + groupData.push(processed2); + } else { + if (canonicalTeaserFromGlobal) { + const fallbackSchemaArray = canonicalTeaserFromGlobal?.schema || canonicalTeaserFromGlobal?.fields || canonicalTeaserFromGlobal; + if (Array.isArray(fallbackSchemaArray) && fallbackSchemaArray.length) { + const processed3 = processFieldsRecursive(fallbackSchemaArray, slide, title, pathToUidMap, assetDetailsMap); + if (processed3 && Object.keys(processed3).length) { + groupData.push(processed3); + } + } + } + } + } + } catch (err) { + console.error(`Error processing slide ${key}:`, err); + } + } + + obj[uid] = groupData; + break; } - obj[uid] = groupData; - } else { + + if (Array.isArray(groupValue)) { + if (Array.isArray(field?.schema)) { + for (const element of groupValue) { + groupData.push( + processFieldsRecursive(field.schema, element, title, pathToUidMap, assetDetailsMap) + ); + } + } + obj[uid] = groupData; + break; + } + + const order2 = Array.isArray(items?.[':itemsOrder']) ? items[':itemsOrder'] : null; + const map2 = items?.[':items'] || items; + if (order2 && map2) { + const baseUid = field?.uid; + const keysForThisGroup = order2.filter( + (k) => k === baseUid || new RegExp(`^${baseUid}_`).test(k) + ); + if (Array.isArray(field?.schema) && keysForThisGroup.length > 0) { + for (const k of keysForThisGroup) { + const el = map2[k]; + if (!el || typeof el !== 'object') continue; + const processed = processFieldsRecursive( + field.schema, + el, + title, + pathToUidMap, + assetDetailsMap + ); + if (processed && Object.keys(processed).length) groupData.push(processed); + } + } + obj[uid] = groupData; + break; + } + if (Array.isArray(field?.schema)) { const value = processFieldsRecursive(field.schema, groupValue, title, pathToUidMap, assetDetailsMap); - obj[uid] = value; + obj[uid] = Array.isArray(value) ? value : (value ? [value] : []); + } + } else { + if (Array.isArray(groupValue)) { + const groupData: unknown[] = []; + if (Array.isArray(field?.schema)) { + for (const element of groupValue) { + groupData.push( + processFieldsRecursive(field.schema, element, title, pathToUidMap, assetDetailsMap) + ); + } + } + obj[uid] = groupData; + } else { + if (Array.isArray(field?.schema)) { + const value = processFieldsRecursive(field.schema, groupValue, title, pathToUidMap, assetDetailsMap); + obj[uid] = value; + } } } break; - } + } case 'boolean': { - const aemFieldName = field?.otherCmsField - ? getLastKey(field.otherCmsField, ' > ') + const aemFieldName = field?.otherCmsField + ? getLastKey(field.otherCmsField, ' > ') : getLastKey(field?.uid); const uid = getLastKey(field?.contentstackFieldUid); - const value = getFieldValue(items, aemFieldName); - + const value = getFieldValue(items, aemFieldName); + if (typeof value === 'boolean') { obj[uid] = value; - } + } else if (typeof value === 'object' && value !== null && value?.[':type']?.includes('separator')) { obj[uid] = true; - } + } else if (typeof value === 'string') { const lowerValue = value?.toLowerCase()?.trim(); if (lowerValue === 'true' || lowerValue === 'yes' || lowerValue === '1') { @@ -622,30 +819,30 @@ function processFieldsRecursive( } break; } - + case 'single_line_text': { const aemFieldName = field?.otherCmsField ? getLastKey(field?.otherCmsField, ' > ') : getLastKey(field?.uid); - let value = getFieldValue(items, aemFieldName); + let value = getFieldValue(items, aemFieldName); const uid = getLastKey(field?.contentstackFieldUid); - + const actualUid = getActualFieldUid(uid, field?.uid); if (value && typeof value === 'string' && /<[^>]+>/.test(value)) { value = stripHtmlTags(value); } - + obj[actualUid] = value !== null && value !== undefined ? String(value) : ""; break; } case 'multi_line_text': { const aemFieldName = field?.otherCmsField ? getLastKey(field.otherCmsField, ' > ') : getLastKey(field?.uid); - let value = getFieldValue(items, aemFieldName); + let value = getFieldValue(items, aemFieldName); const uid = getLastKey(field?.contentstackFieldUid); - + if (value && typeof value === 'string' && /<[^>]+>/.test(value)) { value = stripHtmlTags(value); } - + obj[uid] = value !== null && value !== undefined ? String(value) : ""; break; } @@ -654,16 +851,16 @@ function processFieldsRecursive( const uid = getLastKey(field?.contentstackFieldUid); obj[uid] = title ?? ''; break; - } + } case 'url': { const uid = getLastKey(field?.contentstackFieldUid); obj[uid] = `/${slugify(title)}`; break; - } + } case 'reference': { const fieldKey = getLastKey(field?.contentstackFieldUid); const refCtUid = field?.referenceTo?.[0] || field?.uid; - const references = []; + const references = []; for (const [key, val] of Object.entries(items) as [string, Record][]) { if (!val?.configured || (val[':type'] as string) === 'nt:folder') { continue; @@ -688,9 +885,9 @@ function processFieldsRecursive( } case 'number': { const aemFieldName = field?.otherCmsField ? getLastKey(field.otherCmsField, ' > ') : getLastKey(field?.uid); - const value = getFieldValue(items, aemFieldName); + const value = getFieldValue(items, aemFieldName); const uid = getLastKey(field?.contentstackFieldUid); - + if (value !== null && value !== undefined && value !== '') { const numValue = typeof value === 'number' ? value : Number(value); if (!isNaN(numValue)) { @@ -705,88 +902,173 @@ function processFieldsRecursive( } case 'json': { const aemFieldName = field?.otherCmsField ? getLastKey(field.otherCmsField, ' > ') : field?.uid; - const value = getFieldValue(items, aemFieldName); + const value = getFieldValue(items, aemFieldName); const uid = getLastKey(field?.contentstackFieldUid); - + const actualUid = getActualFieldUid(uid, field?.uid); + let htmlContent = ''; - + if (typeof value === 'string') { htmlContent = value; } else if (value && typeof value === 'object') { htmlContent = value.text || value.content || ''; } - + const jsonData = attachJsonRte({ content: htmlContent }); - obj[uid] = jsonData; + obj[actualUid] = jsonData; break; } - case 'html': { + case 'html': { const aemFieldName = field?.otherCmsField ? getLastKey(field?.otherCmsField, ' > ') : field?.uid; - const value = getFieldValue(items, aemFieldName); + const value = getFieldValue(items, aemFieldName); const uid = getLastKey(field?.contentstackFieldUid); - + const actualUid = getActualFieldUid(uid, field?.uid); let htmlContent = ''; - + if (typeof value === 'string') { htmlContent = value; } else if (value && typeof value === 'object') { htmlContent = value?.text || value?.content || ''; } - obj[uid] = htmlContent; + obj[actualUid] = htmlContent; break; } case 'link': { - const value = { - title: getFieldValue(items, 'title') ?? '', - href: getFieldValue(items, 'url') ?? '' - }; const uid = getLastKey(field?.contentstackFieldUid); + + const aemFieldName = field?.otherCmsField + ? getLastKey(field.otherCmsField, ' > ') + : 'link'; + + let linkUrl = getFieldValue(items, aemFieldName); + + if (!linkUrl) { + const urlCandidates = ['link', 'url', 'href', 'linkURL']; + for (const candidate of urlCandidates) { + const val = getFieldValue(items, candidate); + if (val) { + linkUrl = val; + break; + } + } + } + + let linkTitle = getFieldValue(items, 'title'); + + if (!linkTitle) { + const titleCandidates = ['text', 'label', 'linkText']; + for (const candidate of titleCandidates) { + const val = getFieldValue(items, candidate); + if (val) { + linkTitle = val; + break; + } + } + } + + const value = { + title: linkTitle ?? '', + href: linkUrl ?? '' + }; + obj[uid] = value; break; - } + } case 'file': { const uid = getLastKey(field?.contentstackFieldUid); - const aemFieldName = field?.otherCmsField ? getLastKey(field.otherCmsField, ' > ') : 'src'; - const imageSrc = getFieldValue(items, aemFieldName) || getFieldValue(items, 'src'); - - if (!imageSrc || !Object?.keys(pathToUidMap)?.length) { - obj[uid] = null; - break; + + const candidateKeys = [ + field?.otherCmsField ? getLastKey(field.otherCmsField, ' > ') : 'src', + 'src', + 'imagePath', + 'fileReference', + 'file', + 'path', + 'href', + 'url' + ]; + + let val: any = undefined; + for (const key of candidateKeys) { + const v = getFieldValue(items, key); + if (v !== undefined && v !== null && v !== '') { val = v; break; } } - const assetUid = pathToUidMap[imageSrc]; - - if (assetUid) { - const assetDetails = assetDetailsMap?.[assetUid]; - - if (assetDetails) { - obj[uid] = { - uid: assetDetails?.uid, - filename: assetDetails?.filename, - content_type: assetDetails?.content_type, - file_size: assetDetails?.file_size, - title: assetDetails?.title, - url: assetDetails?.url, - tags: assetDetails?.tags || [], - publish_details: assetDetails?.publish_details || [], - parent_uid: assetDetails?.parent_uid || null, - is_dir: false, - ACL: assetDetails?.ACL || [] - }; - } else { - obj[uid] = { - uid: assetUid - }; + + const toArray = (v: any) => Array.isArray(v) ? v : (v != null ? [v] : []); + const rawSrcs = toArray(val).map(v => { + if (v && typeof v === 'object') { + return ( + v.src || + v.imagePath || + v.fileReference || + v.path || + v.url || + v.href || + null + ); } - } else { + return v; + }).filter(Boolean); + + if (!rawSrcs.length || !Object?.keys(pathToUidMap)?.length) { obj[uid] = null; + break; } + + const normalize = (p: string) => (p || '') + .trim() + .replace(/\?.*$/, '') + .replace(/^https?:\/\/[^/]+/, '') + .replace(/\/jcr:content.*$/, '') + .replace(/\/_jcr_content.*$/, '') + .replace(/\.transform\/.*$/, ''); + + const mapOne = (s: string) => { + const n = normalize(s); + const candidates = [s, n, decodeURI(n), encodeURI(n)]; + + let assetUid: string | undefined; + for (const c of candidates) { + if (pathToUidMap?.[c]) { + assetUid = pathToUidMap[c]; + break; + } + } + + if (!assetUid) { + console.warn('[ASSET MISS file]', { raw: s, normalized: n }); + return null; + } + + const det = assetDetailsMap?.[assetUid]; + + return det ? { + uid: det.uid, + filename: det.filename, + content_type: det.content_type, + file_size: det.file_size, + title: det.title, + url: det.url, + tags: det.tags || [], + publish_details: det.publish_details || [], + parent_uid: det.parent_uid || null, + is_dir: false, + ACL: det.ACL || [] + } : { uid: assetUid }; + }; + + const mapped = rawSrcs.map(mapOne).filter(Boolean); + const isMultiple = Boolean(field?.multiple || field?.maxInstance > 1 || field?.advanced?.multiple); + + obj[uid] = isMultiple ? mapped : (mapped[0] || null); break; - } + } + case 'app': { break; - } + } default: { - console.info("🚀 ~ processFieldsRecursive ~ childItems:", field?.uid, field?.contentstackFieldType); + console.info("Unhandled field type:", field?.uid, field?.contentstackFieldType); break; } } @@ -795,9 +1077,9 @@ function processFieldsRecursive( } const containerCreator = ( - fieldMapping: any, - items: any, - title: string, + fieldMapping: any, + items: any, + title: string, pathToUidMap: Record, assetDetailsMap: Record ) => { @@ -822,11 +1104,10 @@ const createEntry = async ({ const assetsSave = path.join(baseDir, ASSETS_DIR_NAME); const pathMappingFile = path.join(assetsSave, 'path-mapping.json'); const assetIndexFile = path.join(assetsSave, ASSETS_SCHEMA_FILE); - + let pathToUidMap: Record = {}; let assetDetailsMap: Record = {}; - // Load path-to-UID mapping try { const mappingData = await fs.promises.readFile(pathMappingFile, 'utf-8'); pathToUidMap = JSON.parse(mappingData); @@ -834,7 +1115,6 @@ const createEntry = async ({ console.warn('path-mapping.json not found, assets will not be attached'); } - // Load full asset details from index.json try { const assetIndexData = await fs.promises.readFile(assetIndexFile, 'utf-8'); assetDetailsMap = JSON.parse(assetIndexData); @@ -848,7 +1128,6 @@ const createEntry = async ({ const allLocales: object = { ...project?.master_locale, ...project?.locales }; const entryMapping: Record = {}; - // FIRST PASS: Process all entries and build mappings for await (const fileName of read(entriesDir)) { const filePath = path.join(entriesDir, fileName); if (filePath?.startsWith?.(damPath)) { @@ -924,7 +1203,6 @@ const createEntry = async ({ } } - const createLocale = async ( req: Request, destinationStackId: string, @@ -999,7 +1277,6 @@ const createLocale = async ( } }; - const createVersionFile = async (destinationStackId: string) => { const baseDir = path.join(baseDirName, destinationStackId); fs.writeFile( @@ -1016,7 +1293,6 @@ const createVersionFile = async (destinationStackId: string) => { ); }; - export const aemService = { createAssets, createEntry, diff --git a/upload-api/migration-aem/helper/index.ts b/upload-api/migration-aem/helper/index.ts index 011402e4f..20f739b52 100644 --- a/upload-api/migration-aem/helper/index.ts +++ b/upload-api/migration-aem/helper/index.ts @@ -1,5 +1,6 @@ import fs from 'fs'; import path from 'path'; +import read from 'fs-readdir-recursive'; import { IReadFiles } from "./types/index.interface"; import { MergeStrategy } from "./types/index.interface"; import { v4 as uuidv4 } from "uuid"; @@ -241,13 +242,10 @@ export function findComponentByType(contentstackComponents: any, type: string, e export function countComponentTypes(component: any, result: Record = {}) { if (!component || typeof component !== "object") return result; - // Check for ':type' at current level - const typeField = component[":type"]?.value; - if (typeField) { - result[typeField] = (result[typeField] || 0) + 1; - } + const t = component[":type"]; + const typeField = typeof t === "string" ? t : t?.value; + if (typeField) result[typeField] = (result[typeField] || 0) + 1; - // Recursively check nested properties for (const key in component) { if (component[key] && typeof component[key] === "object") { countComponentTypes(component[key], result); @@ -333,4 +331,78 @@ export function ensureField( if (!found) { mainSchema.unshift(fieldConfig); } +} + +/** + * Helper function that recursively searches for carousel components and extracts item types + */ +function findCarouselsRecursive(obj: any, itemTypes: Set): void { + if (!obj || typeof obj !== 'object') return; + + const typeValue = obj[':type'] || obj['type']; + const isCarousel = typeof typeValue === 'string' && typeValue.includes('carousel'); + + if (isCarousel && obj[':items']) { + // Get the items object + const items = obj[':items']; + + if (items && typeof items === 'object') { + // Iterate through each item in the carousel + for (const [key, value] of Object.entries(items)) { + if (value && typeof value === 'object') { + const itemType = (value as any)[':type']; + + if (itemType && typeof itemType === 'string') { + // Extract component name from path + const componentName = itemType.split('/').pop(); + if (componentName) { + itemTypes.add(componentName); + } + } + } + } + } + } + + // Recursively search nested objects + if (Array.isArray(obj)) { + obj.forEach(item => findCarouselsRecursive(item, itemTypes)); + } else { + Object.values(obj).forEach(value => findCarouselsRecursive(value, itemTypes)); + } +} + +/** + * Scans all JSON files to find all unique component types used in carousel items + * @param packagePath - Path to the AEM package + * @returns Set of all component types found in any carousel + */ +export async function scanAllCarouselItemTypes(packagePath: string): Promise> { + const carouselItemTypes = new Set(); + const filesDir = path.resolve(packagePath); + + try { + const allFiles = read(filesDir); + const jsonFiles = allFiles.filter(f => f.endsWith('.json')); + + for (const fileName of jsonFiles) { + const filePath = path.join(filesDir, fileName); + + try { + const content = await fs.promises.readFile(filePath, 'utf-8'); + const data = JSON.parse(content); + + // Recursively search for carousel components + findCarouselsRecursive(data, carouselItemTypes); + + } catch (err) { + // Skip invalid JSON files + continue; + } + } + return carouselItemTypes; + } catch (err) { + console.error('❌ Error scanning carousel items:', err); + return carouselItemTypes; + } } \ No newline at end of file diff --git a/upload-api/migration-aem/libs/contentType/components/CarouselComponent.ts b/upload-api/migration-aem/libs/contentType/components/CarouselComponent.ts index ee9212c0d..bcadbd238 100644 --- a/upload-api/migration-aem/libs/contentType/components/CarouselComponent.ts +++ b/upload-api/migration-aem/libs/contentType/components/CarouselComponent.ts @@ -1,4 +1,4 @@ -import { countComponentTypes, findFirstComponentByType, uidCorrector } from "../../../helper"; +import { countComponentTypes, findFirstComponentByType, uidCorrector, scanAllCarouselItemTypes } from "../../../helper"; import { ContentstackComponent } from "../fields"; import { BooleanField, GroupField, TextField } from "../fields/contentstackFields"; import { ButtonComponent, TeaserComponent, ImageComponent, TextBannerComponent, TextComponent, TitleComponent, SearchComponent, SpacerComponent, SeparatorComponent } from "./index"; @@ -12,7 +12,38 @@ const carouselExclude = [ ':itemsOrder' ]; +let globalCarouselItemTypes: Set | null = null; + +const globalComponentSchemas: Map = new Map(); + export class CarouselComponent extends ContentstackComponent { + + /** + * Initialize carousel item types by scanning all files + * Call this BEFORE processing any components + */ + static async initializeCarouselItemTypes(packagePath: string): Promise { + if (!globalCarouselItemTypes) { + globalCarouselItemTypes = await scanAllCarouselItemTypes(packagePath); + } + } + + /** + * Store a component schema for later reuse + */ + static storeComponentSchema(componentType: string, schema: any): void { + if (!globalComponentSchemas.has(componentType)) { + globalComponentSchemas.set(componentType, schema); + } + } + + /** + * Get a stored component schema + */ + static getStoredComponentSchema(componentType: string): any | null { + return globalComponentSchemas.get(componentType) || null; + } + static isCarousel(component: any): boolean { const properties = component?.convertedSchema?.properties; if (properties && typeof properties === 'object') { @@ -48,81 +79,222 @@ export class CarouselComponent extends ContentstackComponent { defaultValue: "" }).toContentstack(), object: (fieldKey: string, schemaProp: SchemaProperty): any => { - const normalizedUid = uidCorrector(fieldKey) + const normalizedUid = uidCorrector(fieldKey); const countObject = countComponentTypes(schemaProp.properties); const schema: any[] = []; - for (const [type, count] of Object.entries(countObject as Record)) { - const data = findFirstComponentByType(schemaProp?.properties, type); - if (!data) continue; // skip if not found + // Check if this is the carousel items object + const isCarouselItems = normalizedUid === 'items' || fieldKey === 'items' || fieldKey === ':items'; - // Correct way to build the component object - const component = { convertedSchema: { type: 'object', properties: data } }; - const isMultiple = count > 1; - - const componentActions: [ - any, string, (comp: any, isMultiple?: boolean) => void - ][] = [ - [ImageComponent, "isImage", (comp, isMultiple) => { - const imageData = ImageComponent.mapImageToContentstack(comp, "image"); - if (imageData && isMultiple) imageData.advanced.multiple = true; - if (imageData) schema.push(imageData); - }], - [TeaserComponent, "isTeaser", (comp, isMultiple) => { - const teaserData = TeaserComponent.mapTeaserToContentstack(comp, "teaser"); - if (teaserData && isMultiple) teaserData.advanced.multiple = true; - if (teaserData) schema.push(teaserData); - }], - [ButtonComponent, "isButton", (comp, isMultiple) => { - const buttonData = ButtonComponent.mapButtonToContentstack(comp, "button"); - if (buttonData && isMultiple) buttonData.advanced.multiple = true; - if (buttonData) schema.push(buttonData); - }], - [TextBannerComponent, "isTextBanner", (comp, isMultiple) => { - const textBannerData = TextBannerComponent.mapTextBannerToContentstack(comp, "textBanner"); - if (textBannerData && isMultiple) textBannerData.advanced.multiple = true; - if (textBannerData) schema.push(textBannerData); - }], - [TextComponent, "isText", (comp, isMultiple) => { - const textData = TextComponent.mapTextToContentstack(comp); - if (textData && isMultiple) textData.multiple = true; - if (textData) schema.push(textData); - }], - [TitleComponent, "isTitle", (comp, isMultiple) => { - const titleData = TitleComponent.mapTitleToContentstack(comp, "title"); - if (titleData && isMultiple) titleData.multiple = true; - if (titleData) schema.push(titleData); - }], - [SearchComponent, "isSearch", (comp, isMultiple) => { - const searchData = SearchComponent.mapSearchToContentstack(comp, "search"); - if (searchData && isMultiple) searchData.multiple = true; - if (searchData) schema.push(searchData); - }], - [SpacerComponent, "isSpacer", (comp, isMultiple) => { - const spacerData = SpacerComponent.mapSpacerToContentstack(comp, "spacer"); - if (spacerData && isMultiple) spacerData.multiple = true; - if (spacerData) schema.push(spacerData); - }], - [SeparatorComponent, "isSeparator", (comp, isMultiple) => { - const separatorData = SeparatorComponent.mapSeparatorToContentstack(comp, "separator"); - if (separatorData && isMultiple) separatorData.multiple = true; - if (separatorData) schema.push(separatorData); - }], - ]; - - componentActions.forEach(([Comp, method, action]) => { - if (Comp?.[method]?.(component)) { - action(component, isMultiple); + if (isCarouselItems) { + const typesToProcess = globalCarouselItemTypes && globalCarouselItemTypes.size > 0 + ? globalCarouselItemTypes + : new Set(Object.keys(countObject).map(t => t.split('/').pop()).filter(Boolean)); + + // Process each component type + for (const componentType of typesToProcess) { + const fullType = `baem/components/${componentType}`; + const currentData = findFirstComponentByType(schemaProp?.properties, fullType); + const currentComponent = currentData ? { convertedSchema: { type: 'object', properties: currentData } } : null; + + // Map each component type + if (componentType === 'teaser' || componentType === 'heroTeaser' || componentType === 'overlayBoxTeaser') { + + let teaserData = null; + if (currentComponent && TeaserComponent.isTeaser(currentComponent)) { + teaserData = TeaserComponent.mapTeaserToContentstack(currentComponent, "teaser"); + if (teaserData) { + //store for reuse + CarouselComponent.storeComponentSchema('teaser', teaserData); + } + } else { + // Try to get from stored schemas + teaserData = CarouselComponent.getStoredComponentSchema('teaser'); + if (teaserData) { + console.log('Reusing previously stored teaser schema'); + } else { + console.warn('No teaser schema available (not in current file and not stored yet)'); + } + } + + if (teaserData) { + const clonedTeaserData = JSON.parse(JSON.stringify(teaserData)); + clonedTeaserData.advanced = { + ...clonedTeaserData.advanced, + multiple: true, + mandatory: false + }; + schema.push(clonedTeaserData); + } + } else if (componentType === 'image') { + + let imageData = null; + if (currentComponent && ImageComponent.isImage(currentComponent)) { + imageData = ImageComponent.mapImageToContentstack(currentComponent, "image"); + if (imageData) { + // Store for future reuse + CarouselComponent.storeComponentSchema('image', imageData); + } + } else { + // Try to get from stored schemas + imageData = CarouselComponent.getStoredComponentSchema('image'); + if (imageData) { + console.log('Reusing previously stored image schema'); + } + } + + if (imageData) { + const clonedImageData = JSON.parse(JSON.stringify(imageData)); + clonedImageData.advanced = { + ...clonedImageData.advanced, + multiple: true, + mandatory: false + }; + schema.push(clonedImageData); + } + } else if (componentType === 'button') { + let buttonData = null; + + if (currentComponent && ButtonComponent.isButton(currentComponent)) { + buttonData = ButtonComponent.mapButtonToContentstack(currentComponent, "button"); + if (buttonData) { + CarouselComponent.storeComponentSchema('button', buttonData); + } + } else { + buttonData = CarouselComponent.getStoredComponentSchema('button'); + } + + if (buttonData) { + const clonedData = JSON.parse(JSON.stringify(buttonData)); + clonedData.advanced = { + ...clonedData.advanced, + multiple: true, + mandatory: false + }; + schema.push(clonedData); + } + } else if (componentType === 'textbanner' || componentType === 'textBanner') { + let textBannerData = null; + + if (currentComponent && TextBannerComponent.isTextBanner(currentComponent)) { + textBannerData = TextBannerComponent.mapTextBannerToContentstack(currentComponent, "textBanner"); + if (textBannerData) { + CarouselComponent.storeComponentSchema('textbanner', textBannerData); + } + } else { + textBannerData = CarouselComponent.getStoredComponentSchema('textbanner'); + } + + if (textBannerData) { + const clonedData = JSON.parse(JSON.stringify(textBannerData)); + clonedData.advanced = { + ...clonedData.advanced, + multiple: true, + mandatory: false + }; + schema.push(clonedData); + } + } else if (componentType === 'text') { + let textData = null; + + if (currentComponent && TextComponent.isText(currentComponent)) { + textData = TextComponent.mapTextToContentstack(currentComponent); + if (textData) { + CarouselComponent.storeComponentSchema('text', textData); + } + } else { + textData = CarouselComponent.getStoredComponentSchema('text'); + } + + if (textData) { + const clonedData = JSON.parse(JSON.stringify(textData)); + clonedData.multiple = true; + schema.push(clonedData); + } + } else if (componentType === 'title') { + let titleData = null; + + if (currentComponent && TitleComponent.isTitle(currentComponent)) { + titleData = TitleComponent.mapTitleToContentstack(currentComponent, "title"); + if (titleData) { + CarouselComponent.storeComponentSchema('title', titleData); + } + } else { + titleData = CarouselComponent.getStoredComponentSchema('title'); + } + + if (titleData) { + const clonedData = JSON.parse(JSON.stringify(titleData)); + clonedData.multiple = true; + schema.push(clonedData); + } + } else if (componentType === 'search') { + let searchData = null; + + if (currentComponent && SearchComponent.isSearch(currentComponent)) { + searchData = SearchComponent.mapSearchToContentstack(currentComponent, "search"); + if (searchData) { + CarouselComponent.storeComponentSchema('search', searchData); + } + } else { + searchData = CarouselComponent.getStoredComponentSchema('search'); + } + + if (searchData) { + const clonedData = JSON.parse(JSON.stringify(searchData)); + clonedData.multiple = true; + schema.push(clonedData); + } + } else if (componentType === 'spacer') { + let spacerData = null; + + if (currentComponent && SpacerComponent.isSpacer(currentComponent)) { + spacerData = SpacerComponent.mapSpacerToContentstack(currentComponent, "spacer"); + if (spacerData) { + CarouselComponent.storeComponentSchema('spacer', spacerData); + } + } else { + spacerData = CarouselComponent.getStoredComponentSchema('spacer'); + } + + if (spacerData) { + const clonedData = JSON.parse(JSON.stringify(spacerData)); + clonedData.multiple = true; + schema.push(clonedData); + } + } else if (componentType === 'separator') { + let separatorData = null; + + if (currentComponent && SeparatorComponent.isSeparator(currentComponent)) { + separatorData = SeparatorComponent.mapSeparatorToContentstack(currentComponent, "separator"); + if (separatorData) { + CarouselComponent.storeComponentSchema('separator', separatorData); + } + } else { + separatorData = CarouselComponent.getStoredComponentSchema('separator'); + } + + if (separatorData) { + const clonedData = JSON.parse(JSON.stringify(separatorData)); + clonedData.multiple = true; + schema.push(clonedData); + } + } else { + console.warn(`Unknown carousel item type: ${componentType}`); } - }); + } } + if (schema.length === 0) { + console.warn('Carousel items schema is empty! No components were mapped.'); + } + return new GroupField({ uid: normalizedUid, displayName: normalizedUid, fields: schema, required: false, multiple: false - }).toContentstack() + }).toContentstack(); }, array: () => null, }; @@ -135,10 +307,13 @@ export class CarouselComponent extends ContentstackComponent { if (!carouselExclude.includes(key)) { const schemaProp = value as SchemaProperty; if (schemaProp?.type && CarouselComponent.fieldTypeMap[schemaProp.type]) { - fields.push(CarouselComponent.fieldTypeMap[schemaProp.type](key, schemaProp)); + const mappedField = CarouselComponent.fieldTypeMap[schemaProp.type](key, schemaProp); + if (mappedField) { + fields.push(mappedField); + } } } - } + } return { ...new GroupField({ uid: parentKey, diff --git a/upload-api/migration-aem/libs/contentType/components/TeaserComponent.ts b/upload-api/migration-aem/libs/contentType/components/TeaserComponent.ts index 757b5684d..effefe873 100644 --- a/upload-api/migration-aem/libs/contentType/components/TeaserComponent.ts +++ b/upload-api/migration-aem/libs/contentType/components/TeaserComponent.ts @@ -1,6 +1,7 @@ + import { isImageType } from "../../../helper"; import { ContentstackComponent } from "../fields"; -import { BooleanField, GroupField, ImageField, LinkField, TextField } from "../fields/contentstackFields"; +import { BooleanField, Field, GroupField, ImageField, LinkField, TextField } from "../fields/contentstackFields"; import { SchemaProperty } from "./index.interface"; @@ -22,26 +23,21 @@ function uidContainsNumber(uid: string): boolean { export class TeaserComponent extends ContentstackComponent { static isTeaser(component: any): boolean { - const properties = component?.convertedSchema?.properties; + const properties = component?.convertedSchema?.properties; if (properties && typeof properties === 'object') { const typeField = properties[":type"]; - if ( - (typeof typeField === "string" && - (/\/components\/(teaser|heroTeaser|overlayBoxTeaser)/.test(typeField) || - typeField.includes("/productCategoryTeaserList")) - ) || - (typeof typeField === "object" && - (/\/components\/(teaser|heroTeaser|overlayBoxTeaser)/.test(typeField.value ?? "") || - (typeField.value ?? "").includes("/productCategoryTeaserList")) - ) - ) { - return true; - } + const typeValue = typeof typeField === "string" ? typeField : typeField?.value; + + const isTeaser = + /\/components\/(teaser|heroTeaser|overlayBoxTeaser)/.test(typeValue ?? "") || + (typeValue ?? "").includes("/productCategoryTeaserList"); + + return isTeaser; } + return false; } - static fieldTypeMap: Record any> = { string: (key, schemaProp, isImg) => isImg ? @@ -69,14 +65,21 @@ export class TeaserComponent extends ContentstackComponent { defaultValue: "" }).toContentstack(), object: (key, schemaProp) => { - const data = { convertedSchema: schemaProp } - const objectData = this.mapTeaserToContentstack(data, key); - if (objectData?.uid && (uidContainsNumber(objectData?.uid) === false) && objectData?.schema?.length) { + const data = { convertedSchema: schemaProp }; + const objectData = TeaserComponent.mapTeaserToContentstack(data, key); + // Accept either `schema` or `fields` depending on what toContentstack returns + const hasFieldsArray = + !!objectData && ( + (Array.isArray((objectData as any).schema) && (objectData as any).schema.length > 0) || + (Array.isArray((objectData as any).fields) && (objectData as any).fields.length > 0) + ); + + if (objectData?.uid && !uidContainsNumber(objectData?.uid) && hasFieldsArray) { return objectData; } const urlValue = schemaProp?.properties?.url?.value; if (urlValue !== undefined) { - return new LinkField({ + return new TextField({ uid: key, displayName: key, description: "", @@ -84,58 +87,145 @@ export class TeaserComponent extends ContentstackComponent { }).toContentstack(); } return null; - }, + }, array: (key, schemaProp) => { - if ( - schemaProp?.type === 'array' && - schemaProp?.items?.properties && - Object.keys(schemaProp?.items?.properties)?.length - ) { - const componentsData: any[] = []; - for (const [key, value] of Object.entries(schemaProp.items.properties)) { - const schemaProp = value as SchemaProperty; - if ( - !teaserExclude.includes(key) && - schemaProp?.type && - TeaserComponent.fieldTypeMap[schemaProp.type] - ) { - const isImg = isImageType(schemaProp?.value) - componentsData.push( - TeaserComponent.fieldTypeMap[schemaProp.type](key, schemaProp, isImg) - ); - } - } - return componentsData?.length ? new GroupField({ - uid: key, + // Special-case for actions array + if (key === 'actions') { + + const actionFields = [ + new TextField({ + uid: 'title', + displayName: 'title', + description: '', + defaultValue: '', + }), + new TextField({ + uid: 'url', + displayName: 'url', + description: '', + defaultValue: '' + }) + ]; + + // Return GroupField instance + return new GroupField({ + uid: key, displayName: key, - fields: componentsData, + fields: actionFields, required: false, multiple: true - }).toContentstack() : null; + }).toContentstack(); + } + const inferItemSample = (): any | null => { + if (schemaProp?.items?.properties && Object.keys(schemaProp.items.properties).length) { + return { from: 'items.properties', properties: schemaProp.items.properties }; + } + if (schemaProp?.items?.value && Array.isArray(schemaProp.items.value) && schemaProp.items.value[0] && typeof schemaProp.items.value[0] === 'object') { + return { from: 'items.value', sample: schemaProp.items.value[0] }; + } + if (Array.isArray(schemaProp?.value) && schemaProp.value[0] && typeof schemaProp.value[0] === 'object') { + return { from: 'value', sample: schemaProp.value[0] }; + } + return null; + }; + + const inferred = inferItemSample(); + if (!inferred) { + console.warn(`Array field "${key}" had no schema or sample items to infer from`); + return null; + } + + let itemProperties: Record | null = null; + + if (inferred.from === 'items.properties') { + itemProperties = inferred.properties; + } else if (inferred.from === 'items.value' || inferred.from === 'value') { + const sample = inferred.sample; + itemProperties = {}; + for (const k of Object.keys(sample)) { + itemProperties[k] = { type: typeof sample[k] === 'number' ? 'integer' : 'string', value: sample[k] }; + } + } + + if (!itemProperties || !Object.keys(itemProperties).length) { + console.warn(`After inference, no item properties found for "${key}"`); + return null; } + + const componentsData: Field[] = []; // Array of Field instances + + for (const [itemKey, itemProp] of Object.entries(itemProperties)) { + const ik = String(itemKey); + const inferredType = (itemProp?.type ?? 'string').toLowerCase(); + + if (inferredType === 'string' || inferredType === 'integer') { + // Create Field instance + componentsData.push(new TextField({ + uid: ik, + displayName: ik, + description: "", + defaultValue: "", + isNumber: inferredType === 'integer' + })); + continue; + } + + if (inferredType === 'object' && itemProp?.properties) { + const nested = TeaserComponent.fieldTypeMap.object(ik, itemProp as SchemaProperty, false); + if (nested) { + componentsData.push(nested); + } + continue; + } + } + if (!componentsData.length) { + console.warn(`No components generated for array field "${key}" after inference`); + return null; + } + + // Return GroupField instance + return new GroupField({ + uid: key, + displayName: key, + fields: componentsData, + required: false, + multiple: true + }).toContentstack(); }, }; - - static mapTeaserToContentstack(component: any, parentKey: any) { const componentSchema = component?.convertedSchema; if (componentSchema?.type === 'object' && componentSchema?.properties) { - const componentsData: any[] = []; + const componentsData: any[] = []; for (const [key, value] of Object.entries(componentSchema.properties)) { const schemaProp = value as SchemaProperty; - if ( - !teaserExclude.includes(key) && - schemaProp?.type && - TeaserComponent.fieldTypeMap[schemaProp.type] - ) { - const isImg = isImageType(schemaProp?.value) - componentsData.push( - TeaserComponent.fieldTypeMap[schemaProp.type](key, schemaProp, isImg) - ); + if (teaserExclude.includes(key)) { + continue; + } + + if (schemaProp?.type && TeaserComponent.fieldTypeMap[schemaProp.type]) { + const isImg = isImageType(schemaProp?.value); + const fieldData = TeaserComponent.fieldTypeMap[schemaProp.type](key, schemaProp, isImg); + + if (fieldData) { + componentsData.push(fieldData); + } else { + console.warn(`Field mapping returned null for: ${key} (type: ${schemaProp.type})`); + } + } else { + console.warn(`No field type mapper for: ${key}`, { + type: schemaProp?.type, + availableMappers: Object.keys(TeaserComponent.fieldTypeMap) + }); } + } + if (componentsData.length === 0) { + console.warn('No fields were generated for teaser component!'); + return null; } - return componentsData?.length ? { + + return { ...new GroupField({ uid: parentKey, displayName: parentKey, @@ -144,7 +234,8 @@ export class TeaserComponent extends ContentstackComponent { multiple: true }).toContentstack(), type: component?.convertedSchema?.properties?.[":type"]?.value - } : null; + }; } + return null; } } \ No newline at end of file diff --git a/upload-api/migration-aem/libs/contentType/components/TextBannerComponent.ts b/upload-api/migration-aem/libs/contentType/components/TextBannerComponent.ts index 04fd54c99..4273cf709 100644 --- a/upload-api/migration-aem/libs/contentType/components/TextBannerComponent.ts +++ b/upload-api/migration-aem/libs/contentType/components/TextBannerComponent.ts @@ -14,12 +14,11 @@ export class TextBannerComponent extends ContentstackComponent { const properties = component?.convertedSchema?.properties; if (properties && typeof properties === 'object') { const typeField = properties[":type"]; - if ( + const isTextBanner = ( (typeof typeField === "string" && typeField.includes("/components/textbanner")) || (typeof typeField === "object" && typeField.value?.includes("/components/textbanner")) - ) { - return true; - } + ); + return isTextBanner; } return false; } @@ -85,20 +84,41 @@ export class TextBannerComponent extends ContentstackComponent { static mapTextBannerToContentstack(component: any, parentKey: any) { const componentSchema = component?.convertedSchema; if (componentSchema?.type === 'object' && componentSchema?.properties) { - const fields: any[] = []; + const fields: any[] = []; for (const [key, value] of Object.entries(componentSchema.properties)) { - if (!textBannerExclude.includes(key)) { - const schemaProp = value as SchemaProperty; - if (schemaProp?.type && TextBannerComponent.fieldTypeMap[schemaProp.type]) { - fields.push(TextBannerComponent.fieldTypeMap[schemaProp.type](key, schemaProp)); + const schemaProp = value as SchemaProperty; + + if (textBannerExclude.includes(key)) { + console.log(`⏭️ Skipping excluded key: ${key}`); + continue; + } + + if (schemaProp?.type && TextBannerComponent.fieldTypeMap[schemaProp.type]) { + const field = TextBannerComponent.fieldTypeMap[schemaProp.type](key, schemaProp); + + if (field) { + fields.push(field); + } else { + console.warn(`Field mapping returned null for: ${key} (type: ${schemaProp.type})`); } + } else { + console.warn(`No field type mapper for: ${key}`, { + type: schemaProp?.type, + availableMappers: Object.keys(TextBannerComponent.fieldTypeMap) + }); } } + + if (fields.length === 0) { + console.warn('No fields were generated for textbanner component!'); + return null; + } + return { ...new GroupField({ uid: parentKey, displayName: parentKey, - fields, + fields: fields, required: false, multiple: false }).toContentstack(), diff --git a/upload-api/migration-aem/libs/contentType/fields/contentstackFields/index.ts b/upload-api/migration-aem/libs/contentType/fields/contentstackFields/index.ts index a6d00ed88..034f024e6 100644 --- a/upload-api/migration-aem/libs/contentType/fields/contentstackFields/index.ts +++ b/upload-api/migration-aem/libs/contentType/fields/contentstackFields/index.ts @@ -184,6 +184,12 @@ export class GroupField extends Field { } toContentstack() { + const processedSchema = this.fields.filter(Boolean).map(field => { + if (field && typeof field.toContentstack === 'function') { + return field.toContentstack(); + } + return field; + }); return { id: this.generateId(), uid: this.uid, @@ -194,7 +200,7 @@ export class GroupField extends Field { contentstackFieldType: 'group', backupFieldType: 'group', backupFieldUid: this.uid, - schema: this.fields.filter(Boolean), + schema: processedSchema, advanced: { mandatory: !!this.required, multiple: this.multiple, diff --git a/upload-api/migration-aem/libs/contentType/index.ts b/upload-api/migration-aem/libs/contentType/index.ts index 2f3defee9..6e7533a8d 100644 --- a/upload-api/migration-aem/libs/contentType/index.ts +++ b/upload-api/migration-aem/libs/contentType/index.ts @@ -42,6 +42,7 @@ function processComponents(components: Record | Record const mappingRules = [ () => BreadcrumbComponent.isBreadcrumb(component) && BreadcrumbComponent.mapBreadcrumbToContentstack(component, key), () => TitleComponent.isTitle(component) && TitleComponent.mapTitleToContentstack(component, key), + () => TextBannerComponent.isTextBanner(component) && TextBannerComponent.mapTextBannerToContentstack(component, key), () => TextComponent.isText(component) && TextComponent.mapTextToContentstack(component), () => NavigationComponent.isNavigation(component) && NavigationComponent.mapNavigationTOContentstack(component, key), () => NavigationComponent.isLanguageNavigation(component) && NavigationComponent.mapNavigationTOContentstack(component, key), @@ -53,7 +54,6 @@ function processComponents(components: Record | Record () => CustomEmbedComponent.isCustomEmbed(component) && CustomEmbedComponent.mapCustomEmbedToContentstack(component, key), () => ProductListingComponent.isProductListing(component) && ProductListingComponent.mapProductListingToContentstack(component, key), () => ButtonComponent.isButton(component) && ButtonComponent.mapButtonToContentstack(component, key), - () => TextBannerComponent.isTextBanner(component) && TextBannerComponent.mapTextBannerToContentstack(component, key), () => ImageComponent.isImage(component) && ImageComponent.mapImageToContentstack(component, key), () => CarouselComponent.isCarousel(component) && CarouselComponent.mapCarouselToContentstack(component, key), @@ -84,9 +84,78 @@ const mergeChildComponent = (contentstackComponents: any) => { return contentstackComponents; } +/** + * Pre-process components to build schema cache for carousel items + * This ensures all standalone components are mapped before carousels + */ +async function preBuildComponentSchemas(mergedComponents: Record) { + let schemasBuilt = 0; + + for (const [key, component] of Object.entries(mergedComponents)) { + // Process standalone components and store their schemas + if (TeaserComponent.isTeaser(component)) { + const schema = TeaserComponent.mapTeaserToContentstack(component, key); + if (schema) { + CarouselComponent.storeComponentSchema('teaser', schema); + schemasBuilt++; + } + } else if (ImageComponent.isImage(component)) { + const schema = ImageComponent.mapImageToContentstack(component, key); + if (schema) { + CarouselComponent.storeComponentSchema('image', schema); + schemasBuilt++; + } + } else if (ButtonComponent.isButton(component)) { + const schema = ButtonComponent.mapButtonToContentstack(component, key); + if (schema) { + CarouselComponent.storeComponentSchema('button', schema); + schemasBuilt++; + } + } else if (TextBannerComponent.isTextBanner(component)) { + const schema = TextBannerComponent.mapTextBannerToContentstack(component, key); + if (schema) { + CarouselComponent.storeComponentSchema('textbanner', schema); + schemasBuilt++; + } + } else if (TextComponent.isText(component)) { + const schema = TextComponent.mapTextToContentstack(component); + if (schema) { + CarouselComponent.storeComponentSchema('text', schema); + schemasBuilt++; + } + } else if (TitleComponent.isTitle(component)) { + const schema = TitleComponent.mapTitleToContentstack(component, key); + if (schema) { + CarouselComponent.storeComponentSchema('title', schema); + schemasBuilt++; + } + } else if (SearchComponent.isSearch(component)) { + const schema = SearchComponent.mapSearchToContentstack(component, key); + if (schema) { + CarouselComponent.storeComponentSchema('search', schema); + schemasBuilt++; + } + } else if (SpacerComponent.isSpacer(component)) { + const schema = SpacerComponent.mapSpacerToContentstack(component, key); + if (schema) { + CarouselComponent.storeComponentSchema('spacer', schema); + schemasBuilt++; + } + } else if (SeparatorComponent.isSeparator(component)) { + const schema = SeparatorComponent.mapSeparatorToContentstack(component, key); + if (schema) { + CarouselComponent.storeComponentSchema('separator', schema); + schemasBuilt++; + } + } + } + +} const convertContentType: IConvertContentType = async (dirPath) => { const templatesDir = path.resolve(dirPath); + await CarouselComponent.initializeCarouselItemTypes(templatesDir); + const templateFiles = read(templatesDir); const damPath = path?.resolve?.(path?.join?.(templatesDir, CONSTANTS.AEM_DAM_DIR)); const allComponentData: Record[] = []; @@ -100,7 +169,9 @@ const convertContentType: IConvertContentType = async (dirPath) => { const trackerData = tracker.getAllComponents(); allComponentData.push(trackerData); } + const mergedComponents = mergeComponentObjects(allComponentData); + await preBuildComponentSchemas(mergedComponents); const contentstackComponents: any = processComponents(mergedComponents); const mergeChildData = mergeChildComponent(contentstackComponents); await writeJsonFile(mergeChildData, CONSTANTS.TMP_FILE); From 718bbb7e34ba78cf7998d61768c7781024a7fee1 Mon Sep 17 00:00:00 2001 From: shobhit upadhyay Date: Fri, 14 Nov 2025 10:21:51 +0530 Subject: [PATCH 2/3] Enhance AEM service with additional helper functions for data sanitization, field value extraction, and asset management. Improved error logging and added detailed comments for better code clarity. Updated asset directory handling and refined file fetching logic. --- api/src/services/aem.service.ts | 75 ++++++++++++++++++++---- upload-api/migration-aem/helper/index.ts | 2 + 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/api/src/services/aem.service.ts b/api/src/services/aem.service.ts index 455f75151..b999135a7 100644 --- a/api/src/services/aem.service.ts +++ b/api/src/services/aem.service.ts @@ -42,6 +42,7 @@ interface FieldMapping { interface Project { master_locale?: object; locales?: object; + // add other properties as needed } interface ContentType { @@ -91,35 +92,44 @@ interface AssetJSON { async function isAssetJsonCreated(assetJsonPath: string): Promise { try { await fs.promises.access(assetJsonPath, fs.constants.F_OK); - return true; + return true; // File exists } catch { - return false; + return false; // File does not exist } } +// Helper function to sanitize data and remove unwanted HTML tags and other chars function stripHtmlTags(html: string): string { if (!html || typeof html !== 'string') return ''; + // Use JSDOM to parse and extract text content const dom = new JSDOM(html); const text = dom.window.document.body.textContent || ''; + // Clean up extra whitespace and newlines return text.trim().replace(/\s+/g, ' '); } +// Helper Function to extract value from items object based on the fieldName function getFieldValue(items: any, fieldName: string): any { if (!items || !fieldName) return undefined; + // Try exact match first if (items[fieldName] !== undefined) { return items[fieldName]; } + // Try camelCase conversion (snake_case → camelCase) + // Handle both single letter and multiple letter segments const camelCaseFieldName = fieldName?.replace(/_([a-z]+)/gi, (_, letters) => { + // Capitalize first letter, keep rest as-is for acronyms return letters?.charAt(0)?.toUpperCase() + letters?.slice(1); }); if (items[camelCaseFieldName] !== undefined) { return items[camelCaseFieldName]; } + // Try all uppercase version for acronyms const acronymVersion = fieldName?.replace(/_([a-z]+)/gi, (_, letters) => { return letters?.toUpperCase(); }); @@ -127,6 +137,7 @@ function getFieldValue(items: any, fieldName: string): any { return items[acronymVersion]; } + // Try case-insensitive match as last resort const itemKeys = Object.keys(items); const matchedKey = itemKeys?.find(key => key.toLowerCase() === fieldName?.toLowerCase()); if (matchedKey && items[matchedKey] !== undefined) { @@ -146,6 +157,12 @@ function getActualFieldUid(uid: string, fieldUid: string): string { return uid; } +/** + * Finds and returns the asset object from assetJsonData where assetPath matches the given string. + * @param assetJsonData - The asset JSON data object. + * @param assetPathToFind - The asset path string to match. + * @returns The matching AssetJSON object, or undefined if not found. + */ function findAssetByPath( assetJsonData: Record, assetPathToFind: string @@ -204,11 +221,11 @@ function slugify(text: unknown): string { if (typeof text !== 'string') return ''; return text .toLowerCase() - .replace(/\|/g, '') - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .replace(/^-+|-+$/g, ''); + .replace(/\|/g, '') // Remove pipe characters + .replace(/[^\w\s-]/g, '') // Remove special characters + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/-+/g, '-') // Replace multiple hyphens with a single hyphen + .replace(/^-+|-+$/g, ''); // Remove leading and trailing hyphens } function addEntryToEntriesData( @@ -226,6 +243,11 @@ function addEntryToEntriesData( entriesData[contentTypeUid][mappedLocale].push(entryObj); } +/** + * Extracts the current locale from the given parseData object. + * @param parseData The parsed data object from the JSON file. + * @returns The locale string if found, otherwise undefined. + */ function getCurrentLocale(parseData: any): string | undefined { if (parseData.language) { return parseData.language; @@ -264,9 +286,12 @@ export function isImageType(path: string): boolean { export function isExperienceFragment(data: any) { if (data?.templateType && data[':type']) { + // Check templateType starts with 'xf-' const hasXfTemplate = data?.templateType?.startsWith('xf-'); + // Check :type contains 'components/xfpage' const hasXfComponent = data[':type']?.includes('components/xfpage'); + // Return analysis return { isXF: hasXfTemplate || hasXfComponent, confidence: (hasXfTemplate && hasXfComponent) ? 'high' @@ -280,17 +305,31 @@ export function isExperienceFragment(data: any) { return null; } +/** + * Ensures the assets directory exists. + * If it does not exist, creates it recursively. + * @param assetsSave The path to the assets directory. + */ async function ensureAssetsDirectory(assetsSave: string): Promise { const fullPath = path.join(process.cwd(), assetsSave); try { await fs.promises.access(fullPath); + // Directory exists, log it console.info(`Directory exists: ${fullPath}`); } catch (err) { await fs.promises.mkdir(fullPath, { recursive: true }); + // Directory created, log it console.info(`Directory created: ${fullPath}`); } } + +/** + * Fetch files from a directory based on a query. + * @param dirPath - The directory to search in. + * @param query - The query string (filename, extension, or regex). + * @returns Array of matching file paths. + */ export async function fetchFilesByQuery( dirPath: string, query: string | RegExp @@ -339,11 +378,12 @@ const createAssets = async ({ await ensureAssetsDirectory(assetsSave); const assetsDir = path.resolve(packagePath); - const allAssetJSON: Record = {}; - const pathToUidMap: Record = {}; + const allAssetJSON: Record = {}; // UID-based index.json + const pathToUidMap: Record = {}; // Path to UID mapping const seenFilenames = new Map(); const pathToFilenameMap = new Map(); + // Discover assets and deduplicate by filename for await (const fileName of read(assetsDir)) { const filePath = path.join(assetsDir, fileName); if (filePath?.startsWith?.(damPath)) { @@ -367,9 +407,9 @@ const createAssets = async ({ if (typeof contentAst === 'string') { const parseData = JSON.parse(contentAst); const filename = parseData?.asset?.name; - + // Store mapping from this AEM path to filename pathToFilenameMap.set(value, filename); - + // Only create asset ONCE per unique filename if (!seenFilenames?.has(filename)) { const uid = uuidv4?.()?.replace?.(/-/g, ''); const blobPath = firstJson?.replace?.('.metadata.json', ''); @@ -391,6 +431,7 @@ const createAssets = async ({ } } + // Create physical asset files (one per unique filename) for (const [filename, assetInfo] of seenFilenames?.entries()) { const { uid, metadata, blobPath } = assetInfo; const nameWithoutExt = typeof filename === 'string' @@ -426,19 +467,23 @@ const createAssets = async ({ } } + // Track first path for each asset and build mappings const assetFirstPath = new Map(); + // Build path-to-UID mapping (ALL paths map to the SAME deduplicated UID) for (const [aemPath, filename] of pathToFilenameMap.entries()) { const assetInfo = seenFilenames?.get(filename); if (assetInfo) { pathToUidMap[aemPath] = assetInfo.uid; + // Track first path for index.json if (!assetFirstPath.has(assetInfo.uid)) { assetFirstPath.set(assetInfo.uid, aemPath); } } } + // Create UID-based index.json for (const [filename, assetInfo] of seenFilenames?.entries()) { const { uid, metadata } = assetInfo; const nameWithoutExt = typeof filename === 'string' @@ -463,17 +508,20 @@ const createAssets = async ({ }; } + // Write files const fileMeta = { '1': ASSETS_SCHEMA_FILE }; await fs.promises.writeFile( path.join(process.cwd(), assetsSave, ASSETS_FILE_NAME), JSON.stringify(fileMeta) ); + // index.json - UID-based await fs.promises.writeFile( path.join(process.cwd(), assetsSave, ASSETS_SCHEMA_FILE), JSON.stringify(allAssetJSON, null, 2) ); + // path-mapping.json - For entry transformation await fs.promises.writeFile( path.join(process.cwd(), assetsSave, 'path-mapping.json'), JSON.stringify(pathToUidMap, null, 2) @@ -1068,7 +1116,7 @@ function processFieldsRecursive( break; } default: { - console.info("Unhandled field type:", field?.uid, field?.contentstackFieldType); + console.info("🚀 ~ processFieldsRecursive ~ childItems", field?.uid, field?.contentstackFieldType); break; } } @@ -1108,6 +1156,7 @@ const createEntry = async ({ let pathToUidMap: Record = {}; let assetDetailsMap: Record = {}; + // Load path-to-UID mapping try { const mappingData = await fs.promises.readFile(pathMappingFile, 'utf-8'); pathToUidMap = JSON.parse(mappingData); @@ -1115,6 +1164,7 @@ const createEntry = async ({ console.warn('path-mapping.json not found, assets will not be attached'); } + // Load full asset details from index.json try { const assetIndexData = await fs.promises.readFile(assetIndexFile, 'utf-8'); assetDetailsMap = JSON.parse(assetIndexData); @@ -1128,6 +1178,7 @@ const createEntry = async ({ const allLocales: object = { ...project?.master_locale, ...project?.locales }; const entryMapping: Record = {}; + // Process each entry file for await (const fileName of read(entriesDir)) { const filePath = path.join(entriesDir, fileName); if (filePath?.startsWith?.(damPath)) { diff --git a/upload-api/migration-aem/helper/index.ts b/upload-api/migration-aem/helper/index.ts index 20f739b52..ba7a9cbeb 100644 --- a/upload-api/migration-aem/helper/index.ts +++ b/upload-api/migration-aem/helper/index.ts @@ -242,10 +242,12 @@ export function findComponentByType(contentstackComponents: any, type: string, e export function countComponentTypes(component: any, result: Record = {}) { if (!component || typeof component !== "object") return result; + // Check for ':type' at current level const t = component[":type"]; const typeField = typeof t === "string" ? t : t?.value; if (typeField) result[typeField] = (result[typeField] || 0) + 1; + // Recursively check nested properties for (const key in component) { if (component[key] && typeof component[key] === "object") { countComponentTypes(component[key], result); From 038a1af10ed3db4118234dbc4abc9668679ad487 Mon Sep 17 00:00:00 2001 From: shobhit upadhyay Date: Mon, 17 Nov 2025 11:48:27 +0530 Subject: [PATCH 3/3] Add detailed JSDoc comments to AEM service functions for better documentation and clarity. Updated CarouselComponent to use optional chaining for safer property access, enhancing code robustness. --- api/src/services/aem.service.ts | 83 ++++++++++++++++++ .../components/CarouselComponent.ts | 86 +++++++++---------- 2 files changed, 126 insertions(+), 43 deletions(-) diff --git a/api/src/services/aem.service.ts b/api/src/services/aem.service.ts index b999135a7..f3776074f 100644 --- a/api/src/services/aem.service.ts +++ b/api/src/services/aem.service.ts @@ -89,6 +89,12 @@ interface AssetJSON { _version?: number; } + +/** + * + * @param assetJsonPath - The path to the asset JSON file. + * @returns True if the asset JSON file exists, false otherwise. + */ async function isAssetJsonCreated(assetJsonPath: string): Promise { try { await fs.promises.access(assetJsonPath, fs.constants.F_OK); @@ -366,6 +372,18 @@ function uidCorrector(str: string): string { declare const contentstackComponents: Record | undefined; +/** + * Function to create assets for the given destination stack. + * @param destinationStackId - The ID of the destination stack. + * @param projectId - The ID of the project. + * @param packagePath - The path to the package. + * @returns void - The function does not return a value. + * @description - This function creates the assets for the given destination stack. + * The assets are created in the following files: + * - index.json - The asset index. + * - path-mapping.json - The path to UID mapping. + * @throws Will log an error if the assets are not created. + */ const createAssets = async ({ destinationStackId, projectId, @@ -528,6 +546,33 @@ const createAssets = async ({ ); }; +/** + * + * @param fields - The fields object. + * @param items - The items object. + * @param title - The title of the entry. + * @param pathToUidMap - The path to UID map. + * @param assetDetailsMap - The asset details map. + * @returns The processed fields object. + * @description - This function processes the fields recursively. + * The fields are processed in the following order: + * - Modular blocks + * - Group + * - Container + * - Single line text + * - Multiple line text + * - Rich text + * - Date + * - Number + * - Boolean + * - Image + * - Video + * - Audio + * - Link + * - Embed + * - Custom + * @throws Will log an error if the fields are not processed. + */ function processFieldsRecursive( fields: any[], items: any, @@ -1139,6 +1184,20 @@ const getTitle = (parseData: any) => { return parseData?.title ?? parseData?.templateType; } + +/** + * + * @param packagePath - The path to the package. + * @param contentTypes - The content types. + * @param destinationStackId - The ID of the destination stack. + * @param projectId - The ID of the project. + * @param project - The project object. + * @returns void - The function does not return a value. + * @description - This function creates the entry for the given destination stack. + * The entry is created in the following files: + * - index.json - The entry index. + * @throws Will log an error if the entry is not created. + */ const createEntry = async ({ packagePath, contentTypes, @@ -1254,6 +1313,20 @@ const createEntry = async ({ } } +/** + * + * @param req - The request object. + * @param destinationStackId - The ID of the destination stack. + * @param projectId - The ID of the project. + * @param project - The project object. + * @returns void - The function does not return a value. + * @description - This function creates the locales for the given destination stack. + * The locales are created in the following files: + * - locale.json - The master locale. + * - allLocales.json - All the locales. + * @throws Will log an error if the file writing operation fails. + * @throws Will log an error if the locales are not created. + */ const createLocale = async ( req: Request, destinationStackId: string, @@ -1328,6 +1401,16 @@ const createLocale = async ( } }; +/** + * + * @param destinationStackId - The ID of the destination stack. + * @returns void - The function does not return a value. + * @description - This function creates a version file for the given destination stack. + * The version file contains the following information: + * - contentVersion: The version of the content schema (set to `2`). + * - logsPath: An empty string reserved for future log path information. + * @throws Will log an error if the file writing operation fails. + */ const createVersionFile = async (destinationStackId: string) => { const baseDir = path.join(baseDirName, destinationStackId); fs.writeFile( diff --git a/upload-api/migration-aem/libs/contentType/components/CarouselComponent.ts b/upload-api/migration-aem/libs/contentType/components/CarouselComponent.ts index bcadbd238..a0885bc7c 100644 --- a/upload-api/migration-aem/libs/contentType/components/CarouselComponent.ts +++ b/upload-api/migration-aem/libs/contentType/components/CarouselComponent.ts @@ -32,7 +32,7 @@ export class CarouselComponent extends ContentstackComponent { * Store a component schema for later reuse */ static storeComponentSchema(componentType: string, schema: any): void { - if (!globalComponentSchemas.has(componentType)) { + if (!globalComponentSchemas?.has(componentType)) { globalComponentSchemas.set(componentType, schema); } } @@ -41,7 +41,7 @@ export class CarouselComponent extends ContentstackComponent { * Get a stored component schema */ static getStoredComponentSchema(componentType: string): any | null { - return globalComponentSchemas.get(componentType) || null; + return globalComponentSchemas?.get(componentType) || null; } static isCarousel(component: any): boolean { @@ -49,8 +49,8 @@ export class CarouselComponent extends ContentstackComponent { if (properties && typeof properties === 'object') { const typeField = properties[":type"]; if ( - (typeof typeField === "string" && typeField.includes("/components/carousel")) || - (typeof typeField === "object" && typeField.value?.includes("/components/carousel")) + (typeof typeField === "string" && typeField?.includes("/components/carousel")) || + (typeof typeField === "object" && typeField?.value?.includes("/components/carousel")) ) { return true; } @@ -87,7 +87,7 @@ export class CarouselComponent extends ContentstackComponent { const isCarouselItems = normalizedUid === 'items' || fieldKey === 'items' || fieldKey === ':items'; if (isCarouselItems) { - const typesToProcess = globalCarouselItemTypes && globalCarouselItemTypes.size > 0 + const typesToProcess = globalCarouselItemTypes && globalCarouselItemTypes?.size > 0 ? globalCarouselItemTypes : new Set(Object.keys(countObject).map(t => t.split('/').pop()).filter(Boolean)); @@ -101,15 +101,15 @@ export class CarouselComponent extends ContentstackComponent { if (componentType === 'teaser' || componentType === 'heroTeaser' || componentType === 'overlayBoxTeaser') { let teaserData = null; - if (currentComponent && TeaserComponent.isTeaser(currentComponent)) { - teaserData = TeaserComponent.mapTeaserToContentstack(currentComponent, "teaser"); + if (currentComponent && TeaserComponent?.isTeaser(currentComponent)) { + teaserData = TeaserComponent?.mapTeaserToContentstack(currentComponent, "teaser"); if (teaserData) { //store for reuse - CarouselComponent.storeComponentSchema('teaser', teaserData); + CarouselComponent?.storeComponentSchema('teaser', teaserData); } } else { // Try to get from stored schemas - teaserData = CarouselComponent.getStoredComponentSchema('teaser'); + teaserData = CarouselComponent?.getStoredComponentSchema('teaser'); if (teaserData) { console.log('Reusing previously stored teaser schema'); } else { @@ -129,15 +129,15 @@ export class CarouselComponent extends ContentstackComponent { } else if (componentType === 'image') { let imageData = null; - if (currentComponent && ImageComponent.isImage(currentComponent)) { + if (currentComponent && ImageComponent?.isImage(currentComponent)) { imageData = ImageComponent.mapImageToContentstack(currentComponent, "image"); if (imageData) { // Store for future reuse - CarouselComponent.storeComponentSchema('image', imageData); + CarouselComponent?.storeComponentSchema('image', imageData); } } else { // Try to get from stored schemas - imageData = CarouselComponent.getStoredComponentSchema('image'); + imageData = CarouselComponent?.getStoredComponentSchema('image'); if (imageData) { console.log('Reusing previously stored image schema'); } @@ -155,13 +155,13 @@ export class CarouselComponent extends ContentstackComponent { } else if (componentType === 'button') { let buttonData = null; - if (currentComponent && ButtonComponent.isButton(currentComponent)) { + if (currentComponent && ButtonComponent?.isButton(currentComponent)) { buttonData = ButtonComponent.mapButtonToContentstack(currentComponent, "button"); if (buttonData) { - CarouselComponent.storeComponentSchema('button', buttonData); + CarouselComponent?.storeComponentSchema('button', buttonData); } } else { - buttonData = CarouselComponent.getStoredComponentSchema('button'); + buttonData = CarouselComponent?.getStoredComponentSchema('button'); } if (buttonData) { @@ -176,13 +176,13 @@ export class CarouselComponent extends ContentstackComponent { } else if (componentType === 'textbanner' || componentType === 'textBanner') { let textBannerData = null; - if (currentComponent && TextBannerComponent.isTextBanner(currentComponent)) { - textBannerData = TextBannerComponent.mapTextBannerToContentstack(currentComponent, "textBanner"); + if (currentComponent && TextBannerComponent?.isTextBanner(currentComponent)) { + textBannerData = TextBannerComponent?.mapTextBannerToContentstack(currentComponent, "textBanner"); if (textBannerData) { - CarouselComponent.storeComponentSchema('textbanner', textBannerData); + CarouselComponent?.storeComponentSchema('textbanner', textBannerData); } } else { - textBannerData = CarouselComponent.getStoredComponentSchema('textbanner'); + textBannerData = CarouselComponent?.getStoredComponentSchema('textbanner'); } if (textBannerData) { @@ -197,13 +197,13 @@ export class CarouselComponent extends ContentstackComponent { } else if (componentType === 'text') { let textData = null; - if (currentComponent && TextComponent.isText(currentComponent)) { - textData = TextComponent.mapTextToContentstack(currentComponent); + if (currentComponent && TextComponent?.isText(currentComponent)) { + textData = TextComponent?.mapTextToContentstack(currentComponent); if (textData) { - CarouselComponent.storeComponentSchema('text', textData); + CarouselComponent?.storeComponentSchema('text', textData); } } else { - textData = CarouselComponent.getStoredComponentSchema('text'); + textData = CarouselComponent?.getStoredComponentSchema('text'); } if (textData) { @@ -214,13 +214,13 @@ export class CarouselComponent extends ContentstackComponent { } else if (componentType === 'title') { let titleData = null; - if (currentComponent && TitleComponent.isTitle(currentComponent)) { - titleData = TitleComponent.mapTitleToContentstack(currentComponent, "title"); + if (currentComponent && TitleComponent?.isTitle(currentComponent)) { + titleData = TitleComponent?.mapTitleToContentstack(currentComponent, "title"); if (titleData) { - CarouselComponent.storeComponentSchema('title', titleData); + CarouselComponent?.storeComponentSchema('title', titleData); } } else { - titleData = CarouselComponent.getStoredComponentSchema('title'); + titleData = CarouselComponent?.getStoredComponentSchema('title'); } if (titleData) { @@ -231,13 +231,13 @@ export class CarouselComponent extends ContentstackComponent { } else if (componentType === 'search') { let searchData = null; - if (currentComponent && SearchComponent.isSearch(currentComponent)) { - searchData = SearchComponent.mapSearchToContentstack(currentComponent, "search"); + if (currentComponent && SearchComponent?.isSearch(currentComponent)) { + searchData = SearchComponent?.mapSearchToContentstack(currentComponent, "search"); if (searchData) { - CarouselComponent.storeComponentSchema('search', searchData); + CarouselComponent?.storeComponentSchema('search', searchData); } } else { - searchData = CarouselComponent.getStoredComponentSchema('search'); + searchData = CarouselComponent?.getStoredComponentSchema('search'); } if (searchData) { @@ -248,13 +248,13 @@ export class CarouselComponent extends ContentstackComponent { } else if (componentType === 'spacer') { let spacerData = null; - if (currentComponent && SpacerComponent.isSpacer(currentComponent)) { - spacerData = SpacerComponent.mapSpacerToContentstack(currentComponent, "spacer"); + if (currentComponent && SpacerComponent?.isSpacer(currentComponent)) { + spacerData = SpacerComponent?.mapSpacerToContentstack(currentComponent, "spacer"); if (spacerData) { - CarouselComponent.storeComponentSchema('spacer', spacerData); + CarouselComponent?.storeComponentSchema('spacer', spacerData); } } else { - spacerData = CarouselComponent.getStoredComponentSchema('spacer'); + spacerData = CarouselComponent?.getStoredComponentSchema('spacer'); } if (spacerData) { @@ -265,13 +265,13 @@ export class CarouselComponent extends ContentstackComponent { } else if (componentType === 'separator') { let separatorData = null; - if (currentComponent && SeparatorComponent.isSeparator(currentComponent)) { - separatorData = SeparatorComponent.mapSeparatorToContentstack(currentComponent, "separator"); + if (currentComponent && SeparatorComponent?.isSeparator(currentComponent)) { + separatorData = SeparatorComponent?.mapSeparatorToContentstack(currentComponent, "separator"); if (separatorData) { - CarouselComponent.storeComponentSchema('separator', separatorData); + CarouselComponent?.storeComponentSchema?.('separator', separatorData); } } else { - separatorData = CarouselComponent.getStoredComponentSchema('separator'); + separatorData = CarouselComponent?.getStoredComponentSchema?.('separator'); } if (separatorData) { @@ -284,7 +284,7 @@ export class CarouselComponent extends ContentstackComponent { } } } - if (schema.length === 0) { + if (schema?.length === 0) { console.warn('Carousel items schema is empty! No components were mapped.'); } @@ -303,11 +303,11 @@ export class CarouselComponent extends ContentstackComponent { const componentSchema = component?.convertedSchema; if (componentSchema?.type === 'object' && componentSchema?.properties) { const fields: any[] = []; - for (const [key, value] of Object.entries(componentSchema.properties)) { + for (const [key, value] of Object.entries(componentSchema?.properties)) { if (!carouselExclude.includes(key)) { const schemaProp = value as SchemaProperty; - if (schemaProp?.type && CarouselComponent.fieldTypeMap[schemaProp.type]) { - const mappedField = CarouselComponent.fieldTypeMap[schemaProp.type](key, schemaProp); + if (schemaProp?.type && CarouselComponent?.fieldTypeMap?.[schemaProp?.type]) { + const mappedField = CarouselComponent?.fieldTypeMap?.[schemaProp?.type](key, schemaProp); if (mappedField) { fields.push(mappedField); }