Skip to content

Commit 718bbb7

Browse files
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.
1 parent 2298f49 commit 718bbb7

File tree

2 files changed

+65
-12
lines changed

2 files changed

+65
-12
lines changed

api/src/services/aem.service.ts

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ interface FieldMapping {
4242
interface Project {
4343
master_locale?: object;
4444
locales?: object;
45+
// add other properties as needed
4546
}
4647

4748
interface ContentType {
@@ -91,42 +92,52 @@ interface AssetJSON {
9192
async function isAssetJsonCreated(assetJsonPath: string): Promise<boolean> {
9293
try {
9394
await fs.promises.access(assetJsonPath, fs.constants.F_OK);
94-
return true;
95+
return true; // File exists
9596
} catch {
96-
return false;
97+
return false; // File does not exist
9798
}
9899
}
99100

101+
// Helper function to sanitize data and remove unwanted HTML tags and other chars
100102
function stripHtmlTags(html: string): string {
101103
if (!html || typeof html !== 'string') return '';
102104

105+
// Use JSDOM to parse and extract text content
103106
const dom = new JSDOM(html);
104107
const text = dom.window.document.body.textContent || '';
105108

109+
// Clean up extra whitespace and newlines
106110
return text.trim().replace(/\s+/g, ' ');
107111
}
108112

113+
// Helper Function to extract value from items object based on the fieldName
109114
function getFieldValue(items: any, fieldName: string): any {
110115
if (!items || !fieldName) return undefined;
111116

117+
// Try exact match first
112118
if (items[fieldName] !== undefined) {
113119
return items[fieldName];
114120
}
115121

122+
// Try camelCase conversion (snake_case → camelCase)
123+
// Handle both single letter and multiple letter segments
116124
const camelCaseFieldName = fieldName?.replace(/_([a-z]+)/gi, (_, letters) => {
125+
// Capitalize first letter, keep rest as-is for acronyms
117126
return letters?.charAt(0)?.toUpperCase() + letters?.slice(1);
118127
});
119128
if (items[camelCaseFieldName] !== undefined) {
120129
return items[camelCaseFieldName];
121130
}
122131

132+
// Try all uppercase version for acronyms
123133
const acronymVersion = fieldName?.replace(/_([a-z]+)/gi, (_, letters) => {
124134
return letters?.toUpperCase();
125135
});
126136
if (items[acronymVersion] !== undefined) {
127137
return items[acronymVersion];
128138
}
129139

140+
// Try case-insensitive match as last resort
130141
const itemKeys = Object.keys(items);
131142
const matchedKey = itemKeys?.find(key => key.toLowerCase() === fieldName?.toLowerCase());
132143
if (matchedKey && items[matchedKey] !== undefined) {
@@ -146,6 +157,12 @@ function getActualFieldUid(uid: string, fieldUid: string): string {
146157
return uid;
147158
}
148159

160+
/**
161+
* Finds and returns the asset object from assetJsonData where assetPath matches the given string.
162+
* @param assetJsonData - The asset JSON data object.
163+
* @param assetPathToFind - The asset path string to match.
164+
* @returns The matching AssetJSON object, or undefined if not found.
165+
*/
149166
function findAssetByPath(
150167
assetJsonData: Record<string, AssetJSON>,
151168
assetPathToFind: string
@@ -204,11 +221,11 @@ function slugify(text: unknown): string {
204221
if (typeof text !== 'string') return '';
205222
return text
206223
.toLowerCase()
207-
.replace(/\|/g, '')
208-
.replace(/[^\w\s-]/g, '')
209-
.replace(/\s+/g, '-')
210-
.replace(/-+/g, '-')
211-
.replace(/^-+|-+$/g, '');
224+
.replace(/\|/g, '') // Remove pipe characters
225+
.replace(/[^\w\s-]/g, '') // Remove special characters
226+
.replace(/\s+/g, '-') // Replace spaces with hyphens
227+
.replace(/-+/g, '-') // Replace multiple hyphens with a single hyphen
228+
.replace(/^-+|-+$/g, ''); // Remove leading and trailing hyphens
212229
}
213230

214231
function addEntryToEntriesData(
@@ -226,6 +243,11 @@ function addEntryToEntriesData(
226243
entriesData[contentTypeUid][mappedLocale].push(entryObj);
227244
}
228245

246+
/**
247+
* Extracts the current locale from the given parseData object.
248+
* @param parseData The parsed data object from the JSON file.
249+
* @returns The locale string if found, otherwise undefined.
250+
*/
229251
function getCurrentLocale(parseData: any): string | undefined {
230252
if (parseData.language) {
231253
return parseData.language;
@@ -264,9 +286,12 @@ export function isImageType(path: string): boolean {
264286

265287
export function isExperienceFragment(data: any) {
266288
if (data?.templateType && data[':type']) {
289+
// Check templateType starts with 'xf-'
267290
const hasXfTemplate = data?.templateType?.startsWith('xf-');
291+
// Check :type contains 'components/xfpage'
268292
const hasXfComponent = data[':type']?.includes('components/xfpage');
269293

294+
// Return analysis
270295
return {
271296
isXF: hasXfTemplate || hasXfComponent,
272297
confidence: (hasXfTemplate && hasXfComponent) ? 'high'
@@ -280,17 +305,31 @@ export function isExperienceFragment(data: any) {
280305
return null;
281306
}
282307

308+
/**
309+
* Ensures the assets directory exists.
310+
* If it does not exist, creates it recursively.
311+
* @param assetsSave The path to the assets directory.
312+
*/
283313
async function ensureAssetsDirectory(assetsSave: string): Promise<void> {
284314
const fullPath = path.join(process.cwd(), assetsSave);
285315
try {
286316
await fs.promises.access(fullPath);
317+
// Directory exists, log it
287318
console.info(`Directory exists: ${fullPath}`);
288319
} catch (err) {
289320
await fs.promises.mkdir(fullPath, { recursive: true });
321+
// Directory created, log it
290322
console.info(`Directory created: ${fullPath}`);
291323
}
292324
}
293325

326+
327+
/**
328+
* Fetch files from a directory based on a query.
329+
* @param dirPath - The directory to search in.
330+
* @param query - The query string (filename, extension, or regex).
331+
* @returns Array of matching file paths.
332+
*/
294333
export async function fetchFilesByQuery(
295334
dirPath: string,
296335
query: string | RegExp
@@ -339,11 +378,12 @@ const createAssets = async ({
339378
await ensureAssetsDirectory(assetsSave);
340379
const assetsDir = path.resolve(packagePath);
341380

342-
const allAssetJSON: Record<string, AssetJSON> = {};
343-
const pathToUidMap: Record<string, string> = {};
381+
const allAssetJSON: Record<string, AssetJSON> = {}; // UID-based index.json
382+
const pathToUidMap: Record<string, string> = {}; // Path to UID mapping
344383
const seenFilenames = new Map<string, { uid: string; metadata: any; blobPath: string }>();
345384
const pathToFilenameMap = new Map<string, string>();
346385

386+
// Discover assets and deduplicate by filename
347387
for await (const fileName of read(assetsDir)) {
348388
const filePath = path.join(assetsDir, fileName);
349389
if (filePath?.startsWith?.(damPath)) {
@@ -367,9 +407,9 @@ const createAssets = async ({
367407
if (typeof contentAst === 'string') {
368408
const parseData = JSON.parse(contentAst);
369409
const filename = parseData?.asset?.name;
370-
410+
// Store mapping from this AEM path to filename
371411
pathToFilenameMap.set(value, filename);
372-
412+
// Only create asset ONCE per unique filename
373413
if (!seenFilenames?.has(filename)) {
374414
const uid = uuidv4?.()?.replace?.(/-/g, '');
375415
const blobPath = firstJson?.replace?.('.metadata.json', '');
@@ -391,6 +431,7 @@ const createAssets = async ({
391431
}
392432
}
393433

434+
// Create physical asset files (one per unique filename)
394435
for (const [filename, assetInfo] of seenFilenames?.entries()) {
395436
const { uid, metadata, blobPath } = assetInfo;
396437
const nameWithoutExt = typeof filename === 'string'
@@ -426,19 +467,23 @@ const createAssets = async ({
426467
}
427468
}
428469

470+
// Track first path for each asset and build mappings
429471
const assetFirstPath = new Map<string, string>();
430472

473+
// Build path-to-UID mapping (ALL paths map to the SAME deduplicated UID)
431474
for (const [aemPath, filename] of pathToFilenameMap.entries()) {
432475
const assetInfo = seenFilenames?.get(filename);
433476
if (assetInfo) {
434477
pathToUidMap[aemPath] = assetInfo.uid;
435478

479+
// Track first path for index.json
436480
if (!assetFirstPath.has(assetInfo.uid)) {
437481
assetFirstPath.set(assetInfo.uid, aemPath);
438482
}
439483
}
440484
}
441485

486+
// Create UID-based index.json
442487
for (const [filename, assetInfo] of seenFilenames?.entries()) {
443488
const { uid, metadata } = assetInfo;
444489
const nameWithoutExt = typeof filename === 'string'
@@ -463,17 +508,20 @@ const createAssets = async ({
463508
};
464509
}
465510

511+
// Write files
466512
const fileMeta = { '1': ASSETS_SCHEMA_FILE };
467513
await fs.promises.writeFile(
468514
path.join(process.cwd(), assetsSave, ASSETS_FILE_NAME),
469515
JSON.stringify(fileMeta)
470516
);
471517

518+
// index.json - UID-based
472519
await fs.promises.writeFile(
473520
path.join(process.cwd(), assetsSave, ASSETS_SCHEMA_FILE),
474521
JSON.stringify(allAssetJSON, null, 2)
475522
);
476523

524+
// path-mapping.json - For entry transformation
477525
await fs.promises.writeFile(
478526
path.join(process.cwd(), assetsSave, 'path-mapping.json'),
479527
JSON.stringify(pathToUidMap, null, 2)
@@ -1068,7 +1116,7 @@ function processFieldsRecursive(
10681116
break;
10691117
}
10701118
default: {
1071-
console.info("Unhandled field type:", field?.uid, field?.contentstackFieldType);
1119+
console.info("🚀 ~ processFieldsRecursive ~ childItems", field?.uid, field?.contentstackFieldType);
10721120
break;
10731121
}
10741122
}
@@ -1108,13 +1156,15 @@ const createEntry = async ({
11081156
let pathToUidMap: Record<string, string> = {};
11091157
let assetDetailsMap: Record<string, AssetJSON> = {};
11101158

1159+
// Load path-to-UID mapping
11111160
try {
11121161
const mappingData = await fs.promises.readFile(pathMappingFile, 'utf-8');
11131162
pathToUidMap = JSON.parse(mappingData);
11141163
} catch (err) {
11151164
console.warn('path-mapping.json not found, assets will not be attached');
11161165
}
11171166

1167+
// Load full asset details from index.json
11181168
try {
11191169
const assetIndexData = await fs.promises.readFile(assetIndexFile, 'utf-8');
11201170
assetDetailsMap = JSON.parse(assetIndexData);
@@ -1128,6 +1178,7 @@ const createEntry = async ({
11281178
const allLocales: object = { ...project?.master_locale, ...project?.locales };
11291179
const entryMapping: Record<string, string[]> = {};
11301180

1181+
// Process each entry file
11311182
for await (const fileName of read(entriesDir)) {
11321183
const filePath = path.join(entriesDir, fileName);
11331184
if (filePath?.startsWith?.(damPath)) {

upload-api/migration-aem/helper/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,12 @@ export function findComponentByType(contentstackComponents: any, type: string, e
242242
export function countComponentTypes(component: any, result: Record<string, number> = {}) {
243243
if (!component || typeof component !== "object") return result;
244244

245+
// Check for ':type' at current level
245246
const t = component[":type"];
246247
const typeField = typeof t === "string" ? t : t?.value;
247248
if (typeField) result[typeField] = (result[typeField] || 0) + 1;
248249

250+
// Recursively check nested properties
249251
for (const key in component) {
250252
if (component[key] && typeof component[key] === "object") {
251253
countComponentTypes(component[key], result);

0 commit comments

Comments
 (0)