From be3e4dca2736d6b64883bb8122439defad2d919a Mon Sep 17 00:00:00 2001 From: shobhit upadhyay Date: Wed, 15 Oct 2025 17:03:21 +0530 Subject: [PATCH 1/5] refactor: enhance JSON handling in processFieldsRecursive function of aem.service.ts to support string and object types for content --- api/src/services/aem.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/api/src/services/aem.service.ts b/api/src/services/aem.service.ts index 5e29f46dc..44d164c7d 100644 --- a/api/src/services/aem.service.ts +++ b/api/src/services/aem.service.ts @@ -513,7 +513,16 @@ function processFieldsRecursive(fields: any[], items: any, title: string, assetJ case 'json': { const value = items?.[field?.uid]; const uid = getLastKey(field?.contentstackFieldUid); - const jsonData = attachJsonRte({ content: value }) + + 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; break; } From 1f0d2cbb9cb4c4eb9f66cfee2ca1c41de6b270c2 Mon Sep 17 00:00:00 2001 From: shobhit upadhyay Date: Tue, 28 Oct 2025 14:36:26 +0530 Subject: [PATCH 2/5] refactor: enhance asset processing in aem.service.ts by adding new fields to AssetJSON interface, implementing helper functions for HTML sanitization and field value extraction, and improving asset deduplication logic Resolved Bugs - CMG-743 CMG-747 CMG-748 CMG-758 --- api/src/services/aem.service.ts | 420 ++++++++++++++---- .../helper/fieldMappings.merge.ts | 256 ++++++++--- 2 files changed, 520 insertions(+), 156 deletions(-) diff --git a/api/src/services/aem.service.ts b/api/src/services/aem.service.ts index 44d164c7d..b163a3dc3 100644 --- a/api/src/services/aem.service.ts +++ b/api/src/services/aem.service.ts @@ -69,7 +69,7 @@ interface AssetJSON { urlPath: string; uid: string; content_type?: string; - file_size?: number; + file_size?: number | string; tags: string[]; filename?: string; is_dir: boolean; @@ -77,6 +77,13 @@ interface AssetJSON { title?: string; publish_details: unknown[]; assetPath: string; + url?: string; + ACL?: unknown[]; + created_at?: string; + updated_at?: string; + created_by?: string; + updated_by?: string; + _version?: number; } async function isAssetJsonCreated(assetJsonPath: string): Promise { @@ -88,6 +95,54 @@ async function isAssetJsonCreated(assetJsonPath: string): Promise { } } +// Helper function to remove 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 (show_oos → showOOS) + const acronymVersion = fieldName.replace(/_([a-z]+)/gi, (_, letters) => { + return letters.toUpperCase(); + }); + if (items[acronymVersion] !== undefined) { + 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) { + return items[matchedKey]; + } + + return undefined; +} /** * Finds and returns the asset object from assetJsonData where assetPath matches the given string. @@ -308,10 +363,15 @@ const createAssets = async ({ const assetsSave = path.join(baseDir, ASSETS_DIR_NAME); await ensureAssetsDirectory(assetsSave); const assetsDir = path.resolve(packagePath); - const allAssetJSON: 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); - // Exclude files from dam-downloads directory if (filePath?.startsWith?.(damPath)) { continue; } @@ -320,105 +380,168 @@ const createAssets = async ({ try { const parseData = JSON.parse(content); const flatData = deepFlattenObject(parseData); + for await (const [, value] of Object.entries(flatData)) { if (typeof value === 'string' && isImageType?.(value)) { const lastSegment = value?.split?.('/')?.pop?.(); if (typeof lastSegment === 'string') { const assetsQueryPath = await fetchFilesByQuery(damPath, lastSegment); - const firstJson = assetsQueryPath.find((filePath: string) => filePath?.endsWith?.('.json')) ?? null; + const firstJson = assetsQueryPath.find((fp: string) => fp?.endsWith?.('.json')) ?? null; + if (typeof firstJson === 'string' && firstJson?.endsWith('.json')) { const contentAst = await fs.promises.readFile(firstJson, 'utf-8'); if (typeof contentAst === 'string') { - const uid = uuidv4?.()?.replace?.(/-/g, ''); const parseData = JSON.parse(contentAst); const filename = parseData?.asset?.name; - const nameWithoutExt = typeof filename === 'string' ? filename.split('.').slice(0, -1).join('.') : filename; - allAssetJSON[uid] = { - urlPath: `/assets/${uid}`, - uid: uid, - content_type: parseData?.asset?.mimeType, - file_size: parseData?.download?.downloadedSize, - tags: [], - filename, - is_dir: false, - parent_uid: null, - title: nameWithoutExt, - publish_details: [], - assetPath: value, - }; - try { + + // 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', ''); - const assets = fs.readFileSync(path.join(blobPath)); - fs.mkdirSync(path.join(assetsSave, 'files', uid), { - recursive: true, + + seenFilenames.set(filename, { + uid, + metadata: parseData, + blobPath }); - fs.writeFileSync( - path.join( - process.cwd(), - assetsSave, - 'files', - uid, - filename - ), - assets - ); - } catch (err) { - console.error( - 'šŸš€ ~ file: assets.js:52 ~ xml_folder?.forEach ~ err:', - err - ); - const message = getLogMessage( - srcFunc, - `Not able to read the asset"${nameWithoutExt}(${uid})".`, - {}, - err - ); - await customLogger(projectId, destinationStackId, 'error', message); + } else { + console.info(`Reusing asset: ${filename} → ${seenFilenames.get(filename)!.uid}`); } - const message = getLogMessage( - srcFunc, - `Asset "${parseData?.asset?.name}" has been successfully transformed.`, - {} - ); - await customLogger(projectId, destinationStackId, 'info', message); } } } } } } catch (err) { - console.error(`āŒ Failed to parse JSON in ${fileName}:`, err); + console.error(`Failed to parse JSON in ${fileName}:`, err); } } } + + // 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('.') + : filename; + + try { + const assets = fs.readFileSync(path.join(blobPath)); + fs.mkdirSync(path.join(assetsSave, 'files', uid), { + recursive: true, + }); + fs.writeFileSync( + 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( + srcFunc, + `Not able to read the asset "${nameWithoutExt}(${uid})".`, + {}, + err + ); + 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('.') + : filename; + + allAssetJSON[uid] = { + urlPath: `/assets/${uid}`, + uid: uid, + content_type: metadata?.asset?.mimeType, + file_size: metadata?.download?.downloadedSize, + tags: [], + filename, + is_dir: false, + parent_uid: null, + title: nameWithoutExt, + publish_details: [], + assetPath: assetFirstPath.get(uid) || '', + url: `https://images.contentstack.io/v3/assets/${destinationStackId}/${uid}/${filename}`, + ACL: [], + _version: 1 + }; + } + // 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) + 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) ); }; -function processFieldsRecursive(fields: any[], items: any, title: string, assetJsonData: Record) { +function processFieldsRecursive( + 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': { const modularData = items?.[field?.uid] ? items?.[field?.uid] : items?.[':items']; if (Array.isArray(field?.schema)) { const itemsData = modularData?.[':items'] ?? modularData; - const value = processFieldsRecursive(field.schema, itemsData, title, assetJsonData) + const value = processFieldsRecursive(field.schema, itemsData, title, pathToUidMap, assetDetailsMap); const uid = getLastKey(field?.contentstackFieldUid); obj[uid] = value; } break; } + case 'modular_blocks_child': { for (const [, value] of Object.entries(items)) { const objData: any = {}; @@ -426,7 +549,7 @@ function processFieldsRecursive(fields: any[], items: any, title: string, assetJ const getTypeComp = getLastKey(typeValue, '/'); const uid = getLastKey(field?.contentstackFieldUid); if (getTypeComp === field?.uid) { - const compValue = processFieldsRecursive(field.schema, value, title, assetJsonData); + const compValue = processFieldsRecursive(field.schema, value, title, pathToUidMap, assetDetailsMap); if (Object?.keys?.(compValue)?.length) { objData[uid] = compValue; data?.push(objData); @@ -435,6 +558,7 @@ function processFieldsRecursive(fields: any[], items: any, title: string, assetJ } break; } + case 'group': { const groupData: unknown[] = []; const groupValue = items?.[field?.uid]?.items ?? items?.[field?.uid]; @@ -442,47 +566,92 @@ function processFieldsRecursive(fields: any[], items: any, title: string, assetJ if (Array.isArray(groupValue)) { for (const element of groupValue) { if (Array.isArray(field?.schema)) { - const value = processFieldsRecursive(field.schema, element, title, assetJsonData); + const value = processFieldsRecursive(field.schema, element, title, pathToUidMap, assetDetailsMap); groupData?.push(value); } } obj[uid] = groupData; } else { if (Array.isArray(field?.schema)) { - const value = processFieldsRecursive(field.schema, groupValue, title, assetJsonData); + const value = processFieldsRecursive(field.schema, groupValue, title, pathToUidMap, assetDetailsMap); obj[uid] = value; } } break; - } + } + case 'boolean': { - const aemFieldName = field?.otherCmsField ? getLastKey(field.otherCmsField, ' > ') : getLastKey(field?.uid); - const value = items?.[aemFieldName]; - const uid = getLastKey(field?.contentstackFieldUid); - if (typeof value === 'boolean' || (typeof value === 'object' && value?.[':type']?.includes('separator'))) { - obj[uid] = typeof value === 'boolean' ? value : true; + const aemFieldName = field?.otherCmsField + ? getLastKey(field.otherCmsField, ' > ') + : getLastKey(field?.uid); + const uid = getLastKey(field?.contentstackFieldUid); + 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') { + obj[uid] = true; + } else if (lowerValue === 'false' || lowerValue === 'no' || lowerValue === '0' || lowerValue === '') { + obj[uid] = false; + } else { + obj[uid] = true; + } + } + else if (typeof value === 'number') { + obj[uid] = value !== 0; + } + else { + obj[uid] = false; } break; } + case 'single_line_text': { - const value = items?.[field?.uid]; + const aemFieldName = field?.otherCmsField ? getLastKey(field.otherCmsField, ' > ') : getLastKey(field?.uid); + 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; + } + + case 'multi_line_text': { + const aemFieldName = field?.otherCmsField ? getLastKey(field.otherCmsField, ' > ') : getLastKey(field?.uid); + let value = getFieldValue(items, aemFieldName); const uid = getLastKey(field?.contentstackFieldUid); - obj[uid] = value ?? ''; + + if (value && typeof value === 'string' && /<[^>]+>/.test(value)) { + value = stripHtmlTags(value); + } + + obj[uid] = value !== null && value !== undefined ? String(value) : ""; break; } + case 'text': { 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 = []; for (const [key, val] of Object.entries(items) as [string, Record][]) { if (!val?.configured || (val[':type'] as string) === 'nt:folder') { continue; @@ -494,24 +663,37 @@ function processFieldsRecursive(fields: any[], items: any, title: string, assetJ const pathMatchesField = val.localizedFragmentVariationPath.includes(`/${field?.uid}`); const pathMatchesRefType = val.localizedFragmentVariationPath.includes(`/${refCtUid}`); if (pathMatchesField || pathMatchesRefType) { - obj[fieldKey] = [{ + references.push({ "uid": val?.id, "_content_type_uid": refCtUid - }]; + }); break; } } } + obj[fieldKey] = references; break; } case 'number': { - const value = items?.[field?.uid]; + const aemFieldName = field?.otherCmsField ? getLastKey(field.otherCmsField, ' > ') : getLastKey(field?.uid); + const value = getFieldValue(items, aemFieldName); const uid = getLastKey(field?.contentstackFieldUid); - obj[uid] = value ?? ''; + + if (value !== null && value !== undefined && value !== '') { + const numValue = typeof value === 'number' ? value : Number(value); + if (!isNaN(numValue)) { + obj[uid] = numValue; + } else { + obj[uid] = null; + } + } else { + obj[uid] = null; + } break; } case 'json': { - const value = items?.[field?.uid]; + const aemFieldName = field?.otherCmsField ? getLastKey(field.otherCmsField, ' > ') : field?.uid; + const value = getFieldValue(items, aemFieldName); const uid = getLastKey(field?.contentstackFieldUid); let htmlContent = ''; @@ -527,28 +709,57 @@ function processFieldsRecursive(fields: any[], items: any, title: string, assetJ break; } case 'link': { - const value = { title: items?.['title'] ?? '', href: items?.['url'] ?? '' }; + const value = { + title: getFieldValue(items, 'title') ?? '', + href: getFieldValue(items, 'url') ?? '' + }; const uid = getLastKey(field?.contentstackFieldUid); obj[uid] = value; break; - } + } case 'file': { const uid = getLastKey(field?.contentstackFieldUid); - obj[uid] = null - if (Object.keys(assetJsonData)?.length) { - const match = findAssetByPath(assetJsonData, items?.src); - if (match) { - obj[uid] = match?.uid + 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 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 + }; } + } else { + obj[uid] = null; } break; - } + } case 'app': { - // console.info(items) break; - } + } default: { - console.info("šŸš€ ~ processFieldsRecursive ~ childItems:", field?.uid, field?.contentstackFieldType) + console.info("šŸš€ ~ processFieldsRecursive ~ childItems:", field?.uid, field?.contentstackFieldType); break; } } @@ -556,9 +767,15 @@ function processFieldsRecursive(fields: any[], items: any, title: string, assetJ return data?.length ? data : obj; } -const containerCreator = (fieldMapping: any, items: any, title: string, assetJsonData: Record) => { +const containerCreator = ( + fieldMapping: any, + items: any, + title: string, + pathToUidMap: Record, + assetDetailsMap: Record +) => { const fields = buildSchemaTree(fieldMapping); - return processFieldsRecursive(fields, items, title, assetJsonData); + return processFieldsRecursive(fields, items, title, pathToUidMap, assetDetailsMap); } const getTitle = (parseData: any) => { @@ -576,15 +793,26 @@ const createEntry = async ({ const baseDir = path.join(baseDirName, destinationStackId); const entrySave = path.join(baseDir, ENTRIES_DIR_NAME); const assetsSave = path.join(baseDir, ASSETS_DIR_NAME); - const assetJson = path.join(assetsSave, ASSETS_SCHEMA_FILE); - const exists = await isAssetJsonCreated(assetJson); - let assetJsonData: Record = {}; - - if (exists) { - const assetData = await fs.promises.readFile(assetJson, 'utf-8'); - if (typeof assetData === 'string') { - assetJsonData = JSON.parse(assetData); - } + 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); + } catch (err) { + 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); + } catch (err) { + console.warn('index.json not found in assets, will use minimal asset structure'); } const entriesDir = path.resolve(packagePath ?? ''); @@ -610,7 +838,7 @@ const createEntry = async ({ const locale = getCurrentLocale(parseData); const mappedLocale = locale ? getLocaleFromMapper(allLocales as Record, locale) : Object?.keys?.(project?.master_locale ?? {})?.[0]; const items = parseData?.[':items']?.root?.[':items']; - const data = containerCreator(contentType?.fieldMapping, items, title, assetJsonData); + const data = containerCreator(contentType?.fieldMapping, items, title, pathToUidMap, assetDetailsMap); data.uid = uid; data.publish_details = []; diff --git a/upload-api/migration-aem/helper/fieldMappings.merge.ts b/upload-api/migration-aem/helper/fieldMappings.merge.ts index 7c93b8c6c..2ba11efb6 100644 --- a/upload-api/migration-aem/helper/fieldMappings.merge.ts +++ b/upload-api/migration-aem/helper/fieldMappings.merge.ts @@ -1,3 +1,5 @@ +import { uidCorrector } from "."; + interface FieldMapping { id?: string; uid?: string; @@ -53,6 +55,126 @@ function trackComponentType(type: string): void { } } +/** + * Apply uidCorrector to all contentstackFieldUid values in the field mapping + */ +function normalizeFieldUids(field: FieldMapping | undefined): FieldMapping | undefined { + if (!field) return undefined; + + const normalized = { ...field }; + if (normalized.contentstackFieldUid) { + normalized.contentstackFieldUid = uidCorrector(normalized.contentstackFieldUid); + } + + // Also fix backupFieldUid if it exists + if (normalized.backupFieldUid) { + normalized.backupFieldUid = uidCorrector(normalized.backupFieldUid); + } + + // Fix UIDs in blocks + if (normalized.blocks && Array.isArray(normalized.blocks)) { + normalized.blocks = normalized.blocks + .map(block => normalizeBlockUids(block)) + .filter(block => block !== undefined) as BlockItem[]; + } + + // Fix UIDs in schema + if (normalized.schema) { + if (Array.isArray(normalized.schema)) { + normalized.schema = normalized.schema + .map((schemaItem: any) => normalizeSchemaItemUids(schemaItem)) + .filter((item: any) => item !== undefined); + } else if (typeof normalized.schema === 'object') { + normalized.schema = normalizeSchemaItemUids(normalized.schema); + } + } + + return normalized; +} + +/** + * Normalize UIDs in a block item + */ +function normalizeBlockUids(block: BlockItem | undefined): BlockItem | undefined { + if (!block) return undefined; + + const normalized = { ...block }; + + // Fix the block's UID + if (normalized.contentstackFieldUid) { + normalized.contentstackFieldUid = uidCorrector(normalized.contentstackFieldUid); + } + + // Fix backup UID + if (normalized.backupFieldUid) { + normalized.backupFieldUid = uidCorrector(normalized.backupFieldUid); + } + + // Fix uid field (different from contentstackFieldUid) + if (normalized.uid) { + normalized.uid = uidCorrector(normalized.uid); + } + + // Keep contentstackField unchanged + + if (normalized.blocks && Array.isArray(normalized.blocks)) { + normalized.blocks = normalized.blocks + .map(b => normalizeBlockUids(b)) + .filter(b => b !== undefined) as BlockItem[]; + } + + if (normalized.schema) { + if (Array.isArray(normalized.schema)) { + normalized.schema = normalized.schema + .map((item: any) => normalizeSchemaItemUids(item)) + .filter((item: any) => item !== undefined); + } else if (typeof normalized.schema === 'object') { + normalized.schema = normalizeSchemaItemUids(normalized.schema); + } + } + + return normalized; +} + +/** + * Normalize UIDs in a schema item + */ +function normalizeSchemaItemUids(schemaItem: any): any { + if (!schemaItem || typeof schemaItem !== 'object') { + return schemaItem; + } + + const normalized = { ...schemaItem }; + if (normalized.contentstackFieldUid) { + normalized.contentstackFieldUid = uidCorrector(normalized.contentstackFieldUid); + } + + if (normalized.backupFieldUid) { + normalized.backupFieldUid = uidCorrector(normalized.backupFieldUid); + } + + if (normalized.uid) { + normalized.uid = uidCorrector(normalized.uid); + } + + if (normalized.blocks && Array.isArray(normalized.blocks)) { + normalized.blocks = normalized.blocks + .map((b: any) => normalizeBlockUids(b)) + .filter((b: any) => b !== undefined); + } + + if (normalized.schema) { + if (Array.isArray(normalized.schema)) { + normalized.schema = normalized.schema + .map((item: any) => normalizeSchemaItemUids(item)) + .filter((item: any) => item !== undefined); + } else if (typeof normalized.schema === 'object') { + normalized.schema = normalizeSchemaItemUids(normalized.schema); + } + } + + return normalized; +} /** * Main entry point for processing content models */ @@ -72,7 +194,12 @@ export function processContentModels( console.log('Data appears to be already processed. Applying deep deduplication and container merging.'); return (jsonData as MergedContentModel[]).map(model => ({ ...model, - fieldMapping: model.fieldMapping.map(field => processFieldDeep(field)) + fieldMapping: model.fieldMapping + .map(field => { + const processed = processFieldDeep(field); + return normalizeFieldUids(processed); + }) + .filter(field => field !== undefined) as FieldMapping[] })); } @@ -110,8 +237,12 @@ function mergeContentModels(models: ContentModel[]): MergedContentModel[] { // Single instance - just process blocks const singleModel = group[0]; const processedFieldMapping = singleModel.fieldMapping - .filter(f => f !== null) - .map(field => processFieldDeep(field as FieldMapping)); + .filter(f => f !== null && f !== undefined) + .map(field => { + const processed = processFieldDeep(field as FieldMapping); + return normalizeFieldUids(processed); + }) + .filter(field => field !== undefined) as FieldMapping[]; mergedModels.push({ ...singleModel, @@ -324,48 +455,49 @@ function mergeBlocksWithSameUid(blocks: any[]): any[] { } /** + * COMMENTED CODE - OLDER VERSION * Process a single block recursively */ -function processBlockRecursively(block: BlockItem): BlockItem { - const processedBlock = { ...block }; - - // Special handling for blocks with schema containing multiple containers at the same level - if (processedBlock.schema && Array.isArray(processedBlock.schema)) { - console.log(` šŸ” Processing schema array for ${processedBlock.uid || processedBlock.contentstackFieldUid}`); - - // Use mergeBlocksWithSameUid for schema arrays - processedBlock.schema = mergeBlocksWithSameUid(processedBlock.schema); - - // Then process each schema item recursively - processedBlock.schema = processedBlock.schema.map((schemaItem: any) => { - if (schemaItem.blocks && Array.isArray(schemaItem.blocks)) { - return { - ...schemaItem, - blocks: mergeBlocksWithSameUid(schemaItem.blocks) - }; - } - return schemaItem; - }); - } else if (processedBlock.schema && typeof processedBlock.schema === 'object' && processedBlock.schema.blocks) { - // Schema is an object with blocks - const nestedBlocks = processedBlock.schema.blocks; - if (Array.isArray(nestedBlocks)) { - console.log(` šŸ“‚ Processing nested blocks in schema object`); - processedBlock.schema = { - ...processedBlock.schema, - blocks: mergeBlocksWithSameUid(nestedBlocks) - }; - } - } - - // If the block itself has blocks (another pattern) - if (processedBlock.blocks && Array.isArray(processedBlock.blocks)) { - console.log(` šŸ“‚ Processing blocks array in ${processedBlock.uid}`); - processedBlock.blocks = mergeBlocksWithSameUid(processedBlock.blocks); - } - - return processedBlock; -} +// function processBlockRecursively(block: BlockItem): BlockItem { +// const processedBlock = { ...block }; + +// // Special handling for blocks with schema containing multiple containers at the same level +// if (processedBlock.schema && Array.isArray(processedBlock.schema)) { +// console.log(` šŸ” Processing schema array for ${processedBlock.uid || processedBlock.contentstackFieldUid}`); + +// // Use mergeBlocksWithSameUid for schema arrays +// processedBlock.schema = mergeBlocksWithSameUid(processedBlock.schema); + +// // Then process each schema item recursively +// processedBlock.schema = processedBlock.schema.map((schemaItem: any) => { +// if (schemaItem.blocks && Array.isArray(schemaItem.blocks)) { +// return { +// ...schemaItem, +// blocks: mergeBlocksWithSameUid(schemaItem.blocks) +// }; +// } +// return schemaItem; +// }); +// } else if (processedBlock.schema && typeof processedBlock.schema === 'object' && processedBlock.schema.blocks) { +// // Schema is an object with blocks +// const nestedBlocks = processedBlock.schema.blocks; +// if (Array.isArray(nestedBlocks)) { +// console.log(` šŸ“‚ Processing nested blocks in schema object`); +// processedBlock.schema = { +// ...processedBlock.schema, +// blocks: mergeBlocksWithSameUid(nestedBlocks) +// }; +// } +// } + +// // If the block itself has blocks (another pattern) +// if (processedBlock.blocks && Array.isArray(processedBlock.blocks)) { +// console.log(` šŸ“‚ Processing blocks array in ${processedBlock.uid}`); +// processedBlock.blocks = mergeBlocksWithSameUid(processedBlock.blocks); +// } + +// return processedBlock; +// } /** * Sort components dynamically based on their natural order in the data @@ -484,20 +616,21 @@ function createDetailedSchemaSignature(schemaItem: any): string { } /** + * COMMENTED CODE - OLDER VERSION * Deduplicate blocks array */ -function deduplicateBlocks(blocks: BlockItem[]): BlockItem[] { - const uniqueBlocks = new Map(); +// function deduplicateBlocks(blocks: BlockItem[]): BlockItem[] { +// const uniqueBlocks = new Map(); - blocks.forEach(block => { - const signature = createDetailedBlockSignature(block); - if (!uniqueBlocks.has(signature)) { - uniqueBlocks.set(signature, block); - } - }); +// blocks.forEach(block => { +// const signature = createDetailedBlockSignature(block); +// if (!uniqueBlocks.has(signature)) { +// uniqueBlocks.set(signature, block); +// } +// }); - return Array.from(uniqueBlocks.values()); -} +// return Array.from(uniqueBlocks.values()); +// } /** * Normalize object for signature creation @@ -553,7 +686,7 @@ function mergeMultipleInstances(instances: ContentModel[]): MergedContentModel { // Analyze all instances instances.forEach((instance) => { instance.fieldMapping.forEach((field, position) => { - if (field === null) { + if (field === null || field === undefined) { return; } @@ -580,12 +713,15 @@ function mergeMultipleInstances(instances: ContentModel[]): MergedContentModel { fieldPositionMap.forEach((data) => { // Process field with deep deduplication const processedField = processFieldDeep(data.field); - const optimalPosition = calculateOptimalPosition(data.positions); - - fieldsWithOptimalPosition.push({ - field: processedField, - position: optimalPosition - }); + const normalizedField = normalizeFieldUids(processedField); + + if (normalizedField) { + const optimalPosition = calculateOptimalPosition(data.positions); + fieldsWithOptimalPosition.push({ + field: normalizedField, + position: optimalPosition + }); + } }); // Sort by position From 351a68e6173be1844176f7632fc4ccc57aa010ab Mon Sep 17 00:00:00 2001 From: shobhit upadhyay Date: Tue, 28 Oct 2025 16:33:19 +0530 Subject: [PATCH 3/5] refactor: improve null safety in aem.service.ts and fieldMappings.merge.ts by adding optional chaining to prevent runtime errors --- api/src/services/aem.service.ts | 68 +++++------ .../helper/fieldMappings.merge.ts | 110 +++++++++--------- 2 files changed, 89 insertions(+), 89 deletions(-) diff --git a/api/src/services/aem.service.ts b/api/src/services/aem.service.ts index b163a3dc3..dab652aae 100644 --- a/api/src/services/aem.service.ts +++ b/api/src/services/aem.service.ts @@ -95,7 +95,7 @@ async function isAssetJsonCreated(assetJsonPath: string): Promise { } } -// Helper function to remove Sanitize data and remove unwanted HTML tags and other chars +// Helper function to sanitize data and remove unwanted HTML tags and other chars function stripHtmlTags(html: string): string { if (!html || typeof html !== 'string') return ''; @@ -118,17 +118,17 @@ function getFieldValue(items: any, fieldName: string): any { // Try camelCase conversion (snake_case → camelCase) // Handle both single letter and multiple letter segments - const camelCaseFieldName = fieldName.replace(/_([a-z]+)/gi, (_, letters) => { + 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); + return letters?.charAt(0)?.toUpperCase() + letters?.slice(1); }); if (items[camelCaseFieldName] !== undefined) { return items[camelCaseFieldName]; } - // Try all uppercase version for acronyms (show_oos → showOOS) - const acronymVersion = fieldName.replace(/_([a-z]+)/gi, (_, letters) => { - return letters.toUpperCase(); + // Try all uppercase version for acronyms + const acronymVersion = fieldName?.replace(/_([a-z]+)/gi, (_, letters) => { + return letters?.toUpperCase(); }); if (items[acronymVersion] !== undefined) { return items[acronymVersion]; @@ -136,7 +136,7 @@ function getFieldValue(items: any, fieldName: string): any { // Try case-insensitive match as last resort const itemKeys = Object.keys(items); - const matchedKey = itemKeys.find(key => key.toLowerCase() === fieldName.toLowerCase()); + const matchedKey = itemKeys?.find(key => key.toLowerCase() === fieldName?.toLowerCase()); if (matchedKey && items[matchedKey] !== undefined) { return items[matchedKey]; } @@ -386,7 +386,7 @@ const createAssets = async ({ const lastSegment = value?.split?.('/')?.pop?.(); if (typeof lastSegment === 'string') { const assetsQueryPath = await fetchFilesByQuery(damPath, lastSegment); - const firstJson = assetsQueryPath.find((fp: string) => fp?.endsWith?.('.json')) ?? null; + const firstJson = assetsQueryPath?.find((fp: string) => fp?.endsWith?.('.json')) ?? null; if (typeof firstJson === 'string' && firstJson?.endsWith('.json')) { const contentAst = await fs.promises.readFile(firstJson, 'utf-8'); @@ -394,21 +394,21 @@ const createAssets = async ({ const parseData = JSON.parse(contentAst); const filename = parseData?.asset?.name; - // Store mapping from this AEM path to filename + // Store mapping from this AEM path to filename pathToFilenameMap.set(value, filename); - // Only create asset ONCE per unique filename - if (!seenFilenames.has(filename)) { + // Only create asset ONCE per unique filename + if (!seenFilenames?.has(filename)) { const uid = uuidv4?.()?.replace?.(/-/g, ''); const blobPath = firstJson?.replace?.('.metadata.json', ''); - seenFilenames.set(filename, { + seenFilenames?.set(filename, { uid, metadata: parseData, blobPath }); } else { - console.info(`Reusing asset: ${filename} → ${seenFilenames.get(filename)!.uid}`); + console.info(`Reusing asset: ${filename} → ${seenFilenames?.get(filename)?.uid}`); } } } @@ -422,7 +422,7 @@ const createAssets = async ({ } // Create physical asset files (one per unique filename) - for (const [filename, assetInfo] of seenFilenames.entries()) { + for (const [filename, assetInfo] of seenFilenames?.entries()) { const { uid, metadata, blobPath } = assetInfo; const nameWithoutExt = typeof filename === 'string' ? filename.split('.').slice(0, -1).join('.') @@ -462,7 +462,7 @@ const createAssets = async ({ // 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); + const assetInfo = seenFilenames?.get(filename); if (assetInfo) { pathToUidMap[aemPath] = assetInfo.uid; @@ -474,10 +474,10 @@ const createAssets = async ({ } // Create UID-based index.json - for (const [filename, assetInfo] of seenFilenames.entries()) { + for (const [filename, assetInfo] of seenFilenames?.entries()) { const { uid, metadata } = assetInfo; const nameWithoutExt = typeof filename === 'string' - ? filename.split('.').slice(0, -1).join('.') + ? filename?.split('.').slice(0, -1).join('.') : filename; allAssetJSON[uid] = { @@ -491,7 +491,7 @@ const createAssets = async ({ parent_uid: null, title: nameWithoutExt, publish_details: [], - assetPath: assetFirstPath.get(uid) || '', + assetPath: assetFirstPath?.get(uid) ?? '', url: `https://images.contentstack.io/v3/assets/${destinationStackId}/${uid}/${filename}`, ACL: [], _version: 1 @@ -594,7 +594,7 @@ function processFieldsRecursive( obj[uid] = true; } else if (typeof value === 'string') { - const lowerValue = value.toLowerCase().trim(); + const lowerValue = value?.toLowerCase()?.trim(); if (lowerValue === 'true' || lowerValue === 'yes' || lowerValue === '1') { obj[uid] = true; } else if (lowerValue === 'false' || lowerValue === 'no' || lowerValue === '0' || lowerValue === '') { @@ -722,7 +722,7 @@ function processFieldsRecursive( const aemFieldName = field?.otherCmsField ? getLastKey(field.otherCmsField, ' > ') : 'src'; const imageSrc = getFieldValue(items, aemFieldName) || getFieldValue(items, 'src'); - if (!imageSrc || !Object.keys(pathToUidMap)?.length) { + if (!imageSrc || !Object?.keys(pathToUidMap)?.length) { obj[uid] = null; break; } @@ -733,17 +733,17 @@ function processFieldsRecursive( 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, + 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 || [] + ACL: assetDetails?.ACL || [] }; } else { obj[uid] = { @@ -868,20 +868,20 @@ const createEntry = async ({ const flatData = deepFlattenObject(entry); for (const [key, value] of Object.entries(flatData)) { if (key.endsWith('._content_type_uid') && typeof value === 'string') { - const uidField = key.replace('._content_type_uid', ''); + const uidField = key?.replace('._content_type_uid', ''); const refs: string[] = entryMapping?.[value]; if (refs?.length) { - _.set(entry, `${uidField}.uid`, refs[0]); + _.set(entry, `${uidField}.uid`, refs?.[0]); } else { - console.info(`āœ— No entry found for content type: ${value}`); + console.info(`No entry found for content type: ${value}`); } } } } const entriesObject: Record = {}; for (const entry of entries) { - entriesObject[entry.uid] = entry; + entriesObject[entry?.uid] = entry; } const fileMeta = { '1': `${locale}.json` }; const entryPath = path.join( diff --git a/upload-api/migration-aem/helper/fieldMappings.merge.ts b/upload-api/migration-aem/helper/fieldMappings.merge.ts index 2ba11efb6..b3f77516e 100644 --- a/upload-api/migration-aem/helper/fieldMappings.merge.ts +++ b/upload-api/migration-aem/helper/fieldMappings.merge.ts @@ -62,30 +62,30 @@ function normalizeFieldUids(field: FieldMapping | undefined): FieldMapping | und if (!field) return undefined; const normalized = { ...field }; - if (normalized.contentstackFieldUid) { - normalized.contentstackFieldUid = uidCorrector(normalized.contentstackFieldUid); + if (normalized?.contentstackFieldUid) { + normalized.contentstackFieldUid = uidCorrector(normalized?.contentstackFieldUid); } // Also fix backupFieldUid if it exists - if (normalized.backupFieldUid) { - normalized.backupFieldUid = uidCorrector(normalized.backupFieldUid); + if (normalized?.backupFieldUid) { + normalized.backupFieldUid = uidCorrector(normalized?.backupFieldUid); } // Fix UIDs in blocks - if (normalized.blocks && Array.isArray(normalized.blocks)) { - normalized.blocks = normalized.blocks + if (normalized?.blocks && Array.isArray(normalized?.blocks)) { + normalized.blocks = normalized?.blocks .map(block => normalizeBlockUids(block)) .filter(block => block !== undefined) as BlockItem[]; } // Fix UIDs in schema - if (normalized.schema) { - if (Array.isArray(normalized.schema)) { - normalized.schema = normalized.schema + if (normalized?.schema) { + if (Array.isArray(normalized?.schema)) { + normalized.schema = normalized?.schema .map((schemaItem: any) => normalizeSchemaItemUids(schemaItem)) .filter((item: any) => item !== undefined); - } else if (typeof normalized.schema === 'object') { - normalized.schema = normalizeSchemaItemUids(normalized.schema); + } else if (typeof normalized?.schema === 'object') { + normalized.schema = normalizeSchemaItemUids(normalized?.schema); } } @@ -101,29 +101,29 @@ function normalizeBlockUids(block: BlockItem | undefined): BlockItem | undefined const normalized = { ...block }; // Fix the block's UID - if (normalized.contentstackFieldUid) { + if (normalized?.contentstackFieldUid) { normalized.contentstackFieldUid = uidCorrector(normalized.contentstackFieldUid); } // Fix backup UID - if (normalized.backupFieldUid) { + if (normalized?.backupFieldUid) { normalized.backupFieldUid = uidCorrector(normalized.backupFieldUid); } // Fix uid field (different from contentstackFieldUid) - if (normalized.uid) { + if (normalized?.uid) { normalized.uid = uidCorrector(normalized.uid); } // Keep contentstackField unchanged - if (normalized.blocks && Array.isArray(normalized.blocks)) { + if (normalized?.blocks && Array.isArray(normalized?.blocks)) { normalized.blocks = normalized.blocks .map(b => normalizeBlockUids(b)) .filter(b => b !== undefined) as BlockItem[]; } - if (normalized.schema) { + if (normalized?.schema) { if (Array.isArray(normalized.schema)) { normalized.schema = normalized.schema .map((item: any) => normalizeSchemaItemUids(item)) @@ -145,31 +145,31 @@ function normalizeSchemaItemUids(schemaItem: any): any { } const normalized = { ...schemaItem }; - if (normalized.contentstackFieldUid) { - normalized.contentstackFieldUid = uidCorrector(normalized.contentstackFieldUid); + if (normalized?.contentstackFieldUid) { + normalized.contentstackFieldUid = uidCorrector(normalized?.contentstackFieldUid); } - if (normalized.backupFieldUid) { - normalized.backupFieldUid = uidCorrector(normalized.backupFieldUid); + if (normalized?.backupFieldUid) { + normalized.backupFieldUid = uidCorrector(normalized?.backupFieldUid); } - if (normalized.uid) { - normalized.uid = uidCorrector(normalized.uid); + if (normalized?.uid) { + normalized.uid = uidCorrector(normalized?.uid); } - if (normalized.blocks && Array.isArray(normalized.blocks)) { - normalized.blocks = normalized.blocks + if (normalized?.blocks && Array.isArray(normalized?.blocks)) { + normalized.blocks = normalized?.blocks .map((b: any) => normalizeBlockUids(b)) .filter((b: any) => b !== undefined); } - if (normalized.schema) { - if (Array.isArray(normalized.schema)) { - normalized.schema = normalized.schema + if (normalized?.schema) { + if (Array.isArray(normalized?.schema)) { + normalized.schema = normalized?.schema .map((item: any) => normalizeSchemaItemUids(item)) .filter((item: any) => item !== undefined); - } else if (typeof normalized.schema === 'object') { - normalized.schema = normalizeSchemaItemUids(normalized.schema); + } else if (typeof normalized?.schema === 'object') { + normalized.schema = normalizeSchemaItemUids(normalized?.schema); } } @@ -192,9 +192,9 @@ export function processContentModels( if (isAlreadyProcessed) { console.log('Data appears to be already processed. Applying deep deduplication and container merging.'); - return (jsonData as MergedContentModel[]).map(model => ({ + return (jsonData as MergedContentModel[])?.map(model => ({ ...model, - fieldMapping: model.fieldMapping + fieldMapping: model?.fieldMapping .map(field => { const processed = processFieldDeep(field); return normalizeFieldUids(processed); @@ -220,7 +220,7 @@ function mergeContentModels(models: ContentModel[]): MergedContentModel[] { // Group by contentstackUid models.forEach(model => { - const key = model.contentstackUid; + const key = model?.contentstackUid; if (!groupedModels.has(key)) { groupedModels.set(key, []); } @@ -231,12 +231,12 @@ function mergeContentModels(models: ContentModel[]): MergedContentModel[] { // Process each group groupedModels.forEach((group, contentstackUid) => { - console.log(`\nProcessing content type: ${contentstackUid} (${group.length} instances)`); + console.log(`\nProcessing content type: ${contentstackUid} (${group?.length} instances)`); if (group.length === 1) { // Single instance - just process blocks - const singleModel = group[0]; - const processedFieldMapping = singleModel.fieldMapping + const singleModel = group?.[0]; + const processedFieldMapping = singleModel?.fieldMapping .filter(f => f !== null && f !== undefined) .map(field => { const processed = processFieldDeep(field as FieldMapping); @@ -247,7 +247,7 @@ function mergeContentModels(models: ContentModel[]): MergedContentModel[] { mergedModels.push({ ...singleModel, fieldMapping: processedFieldMapping, - mergedFromIds: [singleModel.id] + mergedFromIds: [singleModel?.id] }); } else { // Multiple instances - merge them @@ -264,11 +264,11 @@ function mergeContentModels(models: ContentModel[]): MergedContentModel[] { */ function processFieldDeep(field: FieldMapping): FieldMapping { // Process blocks at the top level - if (field.blocks && Array.isArray(field.blocks)) { - console.log(`\nšŸ“¦ Processing field: ${field.uid || field.contentstackFieldUid}`); - console.log(` Initial blocks: ${field.blocks.length}`); + if (field?.blocks && Array.isArray(field?.blocks)) { + console.log(`\nšŸ“¦ Processing field: ${field?.uid || field?.contentstackFieldUid}`); + console.log(` Initial blocks: ${field?.blocks?.length}`); - const processedBlocks = mergeBlocksWithSameUid(field.blocks); + const processedBlocks = mergeBlocksWithSameUid(field?.blocks); console.log(` Final blocks: ${processedBlocks.length}`); @@ -286,13 +286,13 @@ function processFieldDeep(field: FieldMapping): FieldMapping { * This function handles all the merging logic for blocks with same UIDs */ function mergeBlocksWithSameUid(blocks: any[]): any[] { - if (!blocks || !Array.isArray(blocks) || blocks.length === 0) { + if (!blocks || !Array.isArray(blocks) || blocks?.length === 0) { return blocks; } // Track component types as they appear blocks.forEach(block => { - const type = block.uid || block.contentstackFieldUid || ''; + const type = block?.uid || block?.contentstackFieldUid || ''; if (type) { trackComponentType(type); } @@ -304,7 +304,7 @@ function mergeBlocksWithSameUid(blocks: any[]): any[] { blocks.forEach(block => { // Use contentstackFieldUid as the primary key for grouping, fallback to uid - const key = block.contentstackFieldUid || block.uid || 'unknown'; + const key = block?.contentstackFieldUid || block?.uid || 'unknown'; if (!blockGroups.has(key)) { blockGroups.set(key, []); @@ -326,30 +326,30 @@ function mergeBlocksWithSameUid(blocks: any[]): any[] { const block = deepClone(sameUidBlocks[0]); // Recursively process nested blocks if they exist - if (block.blocks && Array.isArray(block.blocks)) { - block.blocks = mergeBlocksWithSameUid(block.blocks); + if (block?.blocks && Array.isArray(block?.blocks)) { + block.blocks = mergeBlocksWithSameUid(block?.blocks); } // Also process schema if it contains blocks - if (block.schema) { - if (Array.isArray(block.schema)) { + if (block?.schema) { + if (Array.isArray(block?.schema)) { // Check if schema array has multiple items with same contentstackFieldUid - block.schema = mergeBlocksWithSameUid(block.schema); + block.schema = mergeBlocksWithSameUid(block?.schema); // Then process each schema item's blocks - block.schema = block.schema.map((schemaItem: any) => { + block.schema = block?.schema?.map((schemaItem: any) => { if (schemaItem.blocks && Array.isArray(schemaItem.blocks)) { return { ...schemaItem, - blocks: mergeBlocksWithSameUid(schemaItem.blocks) + blocks: mergeBlocksWithSameUid(schemaItem?.blocks) }; } return schemaItem; }); - } else if (typeof block.schema === 'object' && block.schema.blocks && Array.isArray(block.schema.blocks)) { + } else if (typeof block?.schema === 'object' && block?.schema?.blocks && Array.isArray(block?.schema?.blocks)) { block.schema = { ...block.schema, - blocks: mergeBlocksWithSameUid(block.schema.blocks) + blocks: mergeBlocksWithSameUid(block?.schema?.blocks) }; } } @@ -360,16 +360,16 @@ function mergeBlocksWithSameUid(blocks: any[]): any[] { console.log(` šŸ”€ Merging ${sameUidBlocks.length} blocks with contentstackFieldUid/uid: "${key}"`); // Use first block as base - const mergedBlock = deepClone(sameUidBlocks[0]); + const mergedBlock = deepClone(sameUidBlocks?.[0]); // Collect all nested blocks from all instances const allNestedBlocks: any[] = []; const nestedBlockSignatures = new Set(); - sameUidBlocks.forEach((block, index) => { + sameUidBlocks?.forEach((block, index) => { console.log(` Processing block ${index + 1}/${sameUidBlocks.length}`); - if (block.blocks && Array.isArray(block.blocks)) { + if (block?.blocks && Array.isArray(block?.blocks)) { block.blocks.forEach((nestedBlock: any) => { // Track component type const nestedType = nestedBlock.uid || nestedBlock.contentstackFieldUid || ''; From 9c6b736d3b4911774436e5de375b0e14dff97929 Mon Sep 17 00:00:00 2001 From: shobhit upadhyay Date: Tue, 28 Oct 2025 16:40:39 +0530 Subject: [PATCH 4/5] snyk resolve --- package-lock.json | 23 ++++++++++++++++++++++- package.json | 3 ++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f9f3ef2b5..c513dec99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@contentstack/cli-utilities": "^1.0.3" + "@contentstack/cli-utilities": "^1.0.3", + "express-validator": "^7.3.0" }, "devDependencies": { "@types/estree": "^1.0.7", @@ -1304,6 +1305,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/express-validator": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.0.tgz", + "integrity": "sha512-ujK2BX5JUun5NR4JuBo83YSXoDDIpoGz3QxgHTzQcHFevkKnwV1in4K7YNuuXQ1W3a2ObXB/P4OTnTZpUyGWiw==", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.15.15" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/external-editor": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", @@ -4410,6 +4423,14 @@ } } }, + "node_modules/validator": { + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index 73b1cbf93..ad8f2546a 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "errorMsg": "Please add valid branch name!" }, "dependencies": { - "@contentstack/cli-utilities": "^1.0.3" + "@contentstack/cli-utilities": "^1.0.3", + "express-validator": "^7.3.0" } } From 01ff4b176566a6615fdfe87950a23461ee59773a Mon Sep 17 00:00:00 2001 From: shobhit upadhyay Date: Tue, 28 Oct 2025 17:19:32 +0530 Subject: [PATCH 5/5] snyk resolve --- api/package-lock.json | 16 ++++++++-------- api/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index c75ff6019..22590281d 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -18,7 +18,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.21.0", - "express-validator": "^7.0.1", + "express-validator": "^7.3.0", "express-winston": "^4.2.0", "fs-extra": "^11.2.0", "fs-readdir-recursive": "^1.1.0", @@ -7035,12 +7035,12 @@ } }, "node_modules/express-validator": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", - "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.0.tgz", + "integrity": "sha512-ujK2BX5JUun5NR4JuBo83YSXoDDIpoGz3QxgHTzQcHFevkKnwV1in4K7YNuuXQ1W3a2ObXB/P4OTnTZpUyGWiw==", "dependencies": { "lodash": "^4.17.21", - "validator": "~13.12.0" + "validator": "~13.15.15" }, "engines": { "node": ">= 8.0.0" @@ -16059,9 +16059,9 @@ } }, "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", "engines": { "node": ">= 0.10" } diff --git a/api/package.json b/api/package.json index 7a0c749c8..188ec849d 100644 --- a/api/package.json +++ b/api/package.json @@ -34,7 +34,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.21.0", - "express-validator": "^7.0.1", + "express-validator": "^7.3.0", "express-winston": "^4.2.0", "fs-extra": "^11.2.0", "fs-readdir-recursive": "^1.1.0",