From 49112eeef91a5315a3c94c80046b45b1479bb523 Mon Sep 17 00:00:00 2001 From: AishDani Date: Mon, 16 Mar 2026 17:14:42 +0530 Subject: [PATCH 1/3] refactor: update drupal service to handle existing mapping content type and enhanced the flow --- api/src/services/drupal.service.ts | 18 +- api/src/services/drupal/assets.service.ts | 149 +++--- api/src/services/drupal/entries.service.ts | 471 ++++++------------ .../services/drupal/field-analysis.service.ts | 182 +++---- api/src/services/drupal/interface.ts | 12 + api/src/services/migration.service.ts | 156 +++--- 6 files changed, 432 insertions(+), 556 deletions(-) create mode 100644 api/src/services/drupal/interface.ts diff --git a/api/src/services/drupal.service.ts b/api/src/services/drupal.service.ts index 9cd1d5f2b..4374e119b 100644 --- a/api/src/services/drupal.service.ts +++ b/api/src/services/drupal.service.ts @@ -6,7 +6,7 @@ import { createRefrence } from './drupal/references.service.js'; import { createTaxonomy } from './drupal/taxonomy.service.js'; import { createVersionFile } from './drupal/version.service.js'; import { createQuery, createQueryConfig } from './drupal/query.service.js'; -import { generateContentTypeSchemas } from './drupal/content-types.service.js'; +import type { DbConfig, AssetsConfig } from './drupal/interface.js'; /** * Drupal migration service with SQL-based data extraction. @@ -27,13 +27,13 @@ import { generateContentTypeSchemas } from './drupal/content-types.service.js'; export const drupalService = { createQuery, // Generate dynamic queries from database analysis (MUST RUN FIRST) createQueryConfig, // Helper: Create query configuration file for dynamic SQL - generateContentTypeSchemas, // Convert upload-api schema to API content types (MUST RUN AFTER upload-api) + createAssets: ( - dbConfig: any, + dbConfig: DbConfig, destination_stack_id: string, projectId: string, isTest = false, - assetsConfig?: any + assetsConfig?: AssetsConfig ) => { return createAssets( dbConfig, @@ -47,13 +47,13 @@ export const drupalService = { createRefrence, // Create reference mappings for relationships (run before entries) createTaxonomy, // Extract and process Drupal taxonomies (vocabularies and terms) createEntry: ( - dbConfig: any, + dbConfig: DbConfig, destination_stack_id: string, projectId: string, isTest = false, masterLocale = 'en-us', - contentTypeMapping: any[] = [], - project: any = null + project: Record | null = null, + contentTypes: Record[] = [] ) => { return createEntry( dbConfig, @@ -61,8 +61,8 @@ export const drupalService = { projectId, isTest, masterLocale, - contentTypeMapping, - project + project, + contentTypes ); }, createLocale, // Create locale configurations diff --git a/api/src/services/drupal/assets.service.ts b/api/src/services/drupal/assets.service.ts index fe4301ab2..ef436357b 100644 --- a/api/src/services/drupal/assets.service.ts +++ b/api/src/services/drupal/assets.service.ts @@ -11,6 +11,7 @@ import customLogger from '../../utils/custom-logger.utils.js'; import { getDbConnection } from '../../helper/index.js'; import { processBatches } from '../../utils/batch-processor.utils.js'; + const { DATA, ASSETS_DIR_NAME, @@ -93,7 +94,7 @@ const executeQuery = ( query: string ): Promise => { return new Promise((resolve, reject) => { - connection.query(query, (error, results) => { + connection?.query?.(query, (error, results) => { if (error) { reject(error); } else { @@ -123,11 +124,11 @@ const detectPublicPath = async ( try { const configResults = await executeQuery(connection, configQuery); - if (configResults.length > 0) { - const config = JSON.parse(configResults[0].value); - if (config.path && config.path.public) { - const detectedPath = config.path.public; - return detectedPath.endsWith('/') ? detectedPath : `${detectedPath}/`; + if (configResults?.length > 0) { + const config = JSON.parse(configResults?.[0]?.value); + if (config?.path && config?.path?.public) { + const detectedPath = config?.path?.public; + return detectedPath?.endsWith('/') ? detectedPath : `${detectedPath}/`; } } } catch (configErr) { @@ -143,7 +144,7 @@ const detectPublicPath = async ( `; const sampleResults = await executeQuery(connection, sampleFileQuery); - if (sampleResults.length > 0) { + if (sampleResults?.length > 0) { // Try common Drupal paths with the user-provided baseUrl const commonPaths = [ '/sites/default/files/', @@ -153,10 +154,10 @@ const detectPublicPath = async ( // Also try to extract path patterns from the database URIs for (const sampleFile of sampleResults) { - const sampleUri = sampleFile.uri; + const sampleUri = sampleFile?.uri; for (const testPath of commonPaths) { - const testUrl = `${baseUrl}${testPath}${sampleUri.replace( + const testUrl = `${baseUrl}${testPath}${sampleUri?.replace( 'public://', '' )}`; @@ -168,7 +169,7 @@ const detectPublicPath = async ( 'User-Agent': 'Contentstack-Drupal-Migration/1.0', }, }); - if (response.status === 200) { + if (response?.status === 200) { const message = getLogMessage( srcFunc, `Auto-detected public path: ${testPath}`, @@ -200,14 +201,14 @@ const detectPublicPath = async ( const uriResults = await executeQuery(connection, uriPatternQuery); const pathPatterns = new Set(); - if (uriResults && Array.isArray(uriResults)) { - uriResults.forEach((row) => { + if (uriResults && Array?.isArray(uriResults)) { + uriResults?.forEach((row) => { const uri = row?.uri; if (!uri) return; // Extract potential path patterns from URIs - const matches = uri.match(/public:\/\/(?:sites\/([^\/]+)\/)?files\//); + const matches = uri?.match(/public:\/\/(?:sites\/([^\/]+)\/)?files\//); if (matches) { - pathPatterns.add(`/sites/${matches[1]}/files/`); + pathPatterns?.add(`/sites/${matches?.[1]}/files/`); } }); } @@ -219,7 +220,7 @@ const detectPublicPath = async ( for (const sampleFile of sampleSlice) { if (!sampleFile?.uri) continue; // Test with fewer files - const testUrl = `${baseUrl}${patternStr}${sampleFile.uri.replace( + const testUrl = `${baseUrl}${patternStr}${sampleFile?.uri?.replace( 'public://', '' )}`; @@ -231,7 +232,7 @@ const detectPublicPath = async ( 'User-Agent': 'Contentstack-Drupal-Migration/1.0', }, }); - if (response.status === 200) { + if (response?.status === 200) { const message = getLogMessage( srcFunc, `Auto-detected public path from patterns: ${patternStr}`, @@ -264,7 +265,7 @@ const detectPublicPath = async ( } catch (error: any) { const message = getLogMessage( srcFunc, - `Error detecting public path: ${error.message}. Using default.`, + `Error detecting public path: ${error?.message}. Using default.`, {}, error ); @@ -286,16 +287,16 @@ const normalizeUrlConfig = ( } // Normalize baseUrl (handle empty case) - let normalizedBaseUrl = baseUrl ? baseUrl.trim() : ''; + let normalizedBaseUrl = baseUrl ? baseUrl?.trim() : ''; if (normalizedBaseUrl) { // Remove trailing slash from baseUrl - normalizedBaseUrl = normalizedBaseUrl.replace(/\/+$/, ''); + normalizedBaseUrl = normalizedBaseUrl?.replace(/\/+$/, ''); // Ensure baseUrl has protocol if ( - !normalizedBaseUrl.startsWith('http://') && - !normalizedBaseUrl.startsWith('https://') + !normalizedBaseUrl?.startsWith('http://') && + !normalizedBaseUrl?.startsWith('https://') ) { normalizedBaseUrl = `https://${normalizedBaseUrl}`; } @@ -311,26 +312,26 @@ const normalizeUrlConfig = ( } // Normalize publicPath (handle empty case) - let normalizedPublicPath = publicPath ? publicPath.trim() : ''; + let normalizedPublicPath = publicPath ? publicPath?.trim() : ''; if (normalizedPublicPath) { // Ensure publicPath starts with / - if (!normalizedPublicPath.startsWith('/')) { + if (!normalizedPublicPath?.startsWith('/')) { normalizedPublicPath = `/${normalizedPublicPath}`; } // Ensure publicPath ends with / - if (!normalizedPublicPath.endsWith('/')) { + if (!normalizedPublicPath?.endsWith('/')) { normalizedPublicPath = `${normalizedPublicPath}/`; } // Remove duplicate slashes - normalizedPublicPath = normalizedPublicPath.replace(/\/+/g, '/'); + normalizedPublicPath = normalizedPublicPath?.replace(/\/+/g, '/'); // Validate publicPath doesn't contain invalid characters if ( - normalizedPublicPath.includes('..') || - normalizedPublicPath.includes('//') + normalizedPublicPath?.includes('..') || + normalizedPublicPath?.includes('//') ) { throw new Error( `Invalid publicPath format: "${publicPath}" → "${normalizedPublicPath}". Path contains invalid characters.` @@ -356,13 +357,13 @@ const constructAssetUrl = ( normalizeUrlConfig(baseUrl, publicPath); // Already a full URL - return as is - if (uri.startsWith('http://') || uri.startsWith('https://')) { + if (uri?.startsWith('http://') || uri?.startsWith('https://')) { return uri; } // Handle public:// scheme - if (uri.startsWith('public://')) { - const relativePath = uri.replace('public://', ''); + if (uri?.startsWith('public://')) { + const relativePath = uri?.replace('public://', ''); // Check if we have valid baseUrl and publicPath if (!cleanBaseUrl || !cleanPublicPath) { @@ -376,8 +377,8 @@ const constructAssetUrl = ( } // Handle private:// scheme - if (uri.startsWith('private://')) { - const relativePath = uri.replace('private://', ''); + if (uri?.startsWith('private://')) { + const relativePath = uri?.replace('private://', ''); if (!cleanBaseUrl) { throw new Error( @@ -389,7 +390,7 @@ const constructAssetUrl = ( } // Handle relative paths - const path = uri.startsWith('/') ? uri : `/${uri}`; + const path = uri?.startsWith('/') ? uri : `/${uri}`; if (!cleanBaseUrl) { throw new Error( @@ -399,8 +400,8 @@ const constructAssetUrl = ( return `${cleanBaseUrl}${path}`; } catch (error: any) { - console.error(`❌ URL Construction Error: ${error.message}`); - throw new Error(`Failed to construct asset URL: ${error.message}`); + console.error(`❌ URL Construction Error: ${error?.message}`); + throw new Error(`Failed to construct asset URL: ${error?.message}`); } }; @@ -428,8 +429,15 @@ const saveAsset = async ( 'files' ); - const assetId = `assets_${assets.fid}`; - const fileName = assets.filename; + const safeFid = String(assets.fid).replace(/[^a-zA-Z0-9_-]/g, ''); + if (!safeFid) { + throw new Error(`Asset has an invalid fid: ${assets.fid}`); + } + const assetId = `assets_${safeFid}`; + const fileName = path.basename(assets?.filename || ''); + if (!fileName) { + throw new Error(`Asset ${safeFid} has an invalid or empty filename`); + } const fileUrl = constructAssetUrl(assets.uri, baseUrl, publicPath); // Check if asset already exists @@ -456,8 +464,8 @@ const saveAsset = async ( uid: assetId, urlPath: `/assets/${assetId}`, status: true, - content_type: assets.filemime || 'application/octet-stream', - file_size: assets.filesize.toString(), + content_type: assets?.filemime || 'application/octet-stream', + file_size: assets?.filesize?.toString(), tag: [], filename: fileName, url: fileUrl, @@ -484,20 +492,20 @@ const saveAsset = async ( // Track successful download if (urlTracker) { - urlTracker.success.push({ + urlTracker?.success?.push({ uid: assetId, url: fileUrl, filename: fileName, }); } - if (failedJSON[assetId]) { - delete failedJSON[assetId]; + if (failedJSON?.[assetId]) { + delete failedJSON?.[assetId]; } const message = getLogMessage( srcFunc, - `✅ Asset "${fileName}" (${assets.fid}) downloaded successfully.`, + `✅ Asset "${fileName}" (${assets?.fid}) downloaded successfully.`, {} ); await customLogger(projectId, destination_stack_id, 'info', message); @@ -559,7 +567,7 @@ const saveAsset = async ( ); // Check if asset was actually saved (exists in assetData) - if (assetData[assetId]) { + if (assetData?.[assetId]) { return result; // Successfully downloaded with fallback path } } catch (fallbackErr) { @@ -571,34 +579,34 @@ const saveAsset = async ( // All attempts failed - log failure const errorDetails = { - status: err.response?.status, + status: err?.response?.status, statusText: err.response?.statusText, - message: err.message, + message: err?.message, url: fileUrl, }; // Use user-provided public path for the failed URL const failedUrl = constructAssetUrl( - assets.uri, + assets?.uri, baseUrl, userProvidedPublicPath || publicPath ); failedJSON[assetId] = { - failedUid: assets.fid, + failedUid: assets?.fid, name: fileName, url: failedUrl, - file_size: assets.filesize, + file_size: assets?.filesize, reason_for_error: JSON.stringify(errorDetails), }; // Track failed download with user-provided URL if (urlTracker) { - urlTracker.failed.push({ + urlTracker?.failed?.push({ uid: assetId, url: failedUrl, filename: fileName, - reason: `${err.response?.status || 'Network error'}: ${ + reason: `${err?.response?.status || 'Network error'}: ${ err.message }`, }); @@ -606,7 +614,7 @@ const saveAsset = async ( const message = getLogMessage( srcFunc, - `❌ Failed to download "${fileName}" (${assets.fid}) after all attempts: ${err.message}`, + `❌ Failed to download "${fileName}" (${assets?.fid}) after all attempts: ${err?.message}`, {}, err ); @@ -617,7 +625,7 @@ const saveAsset = async ( } } catch (error) { console.error('❌ Error in saveAsset:', error); - return `assets_${assets.fid}`; + return `assets_${assets?.fid}`; } }; @@ -646,7 +654,7 @@ const fetchAssetsFromDB = async ( const message = getLogMessage( srcFunc, - `Fetched ${results.length} total assets from database.`, + `Fetched ${results?.length} total assets from database.`, {} ); await customLogger(projectId, destination_stack_id, 'info', message); @@ -655,7 +663,7 @@ const fetchAssetsFromDB = async ( } catch (error: any) { const message = getLogMessage( srcFunc, - `Failed to fetch assets from database: ${error.message}`, + `Failed to fetch assets from database: ${error?.message}`, {}, error ); @@ -693,11 +701,12 @@ const retryFailedAssets = async ( )})`; const results = await executeQuery(connection, assetsFIDQuery); - if (results && Array.isArray(results) && results.length > 0) { + if (results && Array?.isArray(results) && results?.length > 0) { const limit = pLimit(1); // Reduce to 1 for large datasets to prevent EMFILE errors const tasks = results - .filter((asset: DrupalAsset) => asset != null) - .map((asset: DrupalAsset) => + ?.filter((asset: DrupalAsset) => asset != null) + ?.filter((asset: DrupalAsset) => asset?.fid != null) + ?.map((asset: DrupalAsset) => limit(() => saveAsset( asset, @@ -720,7 +729,7 @@ const retryFailedAssets = async ( const message = getLogMessage( srcFunc, - `Retried ${results.length} failed assets.`, + `Retried ${results?.length} failed assets.`, {} ); await customLogger(projectId, destination_stack_id, 'info', message); @@ -728,7 +737,7 @@ const retryFailedAssets = async ( } catch (error: any) { const message = getLogMessage( srcFunc, - `Error retrying failed assets: ${error.message}`, + `Error retrying failed assets: ${error?.message}`, {}, error ); @@ -758,7 +767,7 @@ export const createAssets = async ( // Auto-detect public path if not provided or empty let detectedPublicPath = publicPath; - if (!publicPath || publicPath.trim() === '') { + if (!publicPath || publicPath?.trim() === '') { detectedPublicPath = await detectPublicPath( connection, baseUrl, @@ -805,13 +814,13 @@ export const createAssets = async ( destination_stack_id ); - if (assetsData && assetsData.length > 0) { + if (assetsData && assetsData?.length > 0) { let assets = assetsData; if (isTest) { - assets = assets.slice(0, 10); + assets = assets?.slice(0, 10); } - const batchSize = assets.length > 10000 ? 100 : 1000; + const batchSize = assets?.length > 10000 ? 100 : 1000; const results = await processBatches( assets, async (asset: DrupalAsset) => { @@ -831,7 +840,7 @@ export const createAssets = async ( publicPath || detectedPublicPath // Use original user-provided path for tracking ); } catch (error) { - failedAssetIds.push(asset.fid.toString()); + failedAssetIds?.push(asset?.fid?.toString()); return `assets_${asset.fid}`; } }, @@ -847,7 +856,7 @@ export const createAssets = async ( ); // Retry failed assets - if (failedAssetIds.length > 0) { + if (failedAssetIds?.length > 0) { await retryFailedAssets( connection, failedAssetIds, @@ -867,7 +876,7 @@ export const createAssets = async ( await writeFile(assetsSave, ASSETS_SCHEMA_FILE, assetData); await writeFile(assetsSave, ASSETS_FILE_NAME, fileMeta); - if (Object.keys(failedJSON).length > 0) { + if (Object?.keys(failedJSON)?.length > 0) { await writeFile(assetMasterFolderPath, ASSETS_FAILED_FILE, failedJSON); } @@ -877,8 +886,8 @@ export const createAssets = async ( const successMessage = getLogMessage( srcFunc, `Successfully processed ${ - Object.keys(assetData).length - } assets out of ${assets.length} total assets.`, + Object?.keys(assetData)?.length + } assets out of ${assets?.length} total assets.`, {} ); await customLogger( @@ -908,4 +917,4 @@ export const createAssets = async ( connection.end(); } } -}; +} \ No newline at end of file diff --git a/api/src/services/drupal/entries.service.ts b/api/src/services/drupal/entries.service.ts index 88c95f165..b19466514 100644 --- a/api/src/services/drupal/entries.service.ts +++ b/api/src/services/drupal/entries.service.ts @@ -25,8 +25,6 @@ import { } from './field-analysis.service.js'; import FieldFetcherService from './field-fetcher.service.js'; import { mapDrupalLocales } from './locales.service.js'; -import FieldMapperModel from '../../models/FieldMapper.js'; -import ContentTypesMapperModel from '../../models/contentTypesMapper-lowdb.js'; // Dynamic import for phpUnserialize will be used in the function // Local utility functions (extracted from entries-field-creator.utils.ts patterns) @@ -215,13 +213,13 @@ const fetchFieldConfigs = async ( for (const row of results) { try { const { unserialize } = await import('php-serialize'); - const configData = unserialize(row.data); + const configData = unserialize(row?.data); if (configData && typeof configData === 'object') { fieldConfigs.push(configData as DrupalFieldConfig); } } catch (parseError) { console.error( - `Failed to parse field config for ${row.name}:`, + `Failed to parse field config for ${row?.name}:`, parseError, ); } @@ -238,7 +236,7 @@ const fetchFieldConfigs = async ( } catch (error: any) { const message = getLogMessage( srcFunc, - `Failed to fetch field configurations: ${error.message}`, + `Failed to fetch field configurations: ${error?.message}`, {}, error, ); @@ -332,7 +330,7 @@ const convertTextToJson = (text: string): any => { // Use the same JSDOM + htmlToJson pattern as WordPress/AEM const dom = new JSDOM(htmlContent); - const htmlDoc = dom.window.document.querySelector('body'); + const htmlDoc = dom?.window?.document?.querySelector('body'); return htmlToJson(htmlDoc); } catch (error) { console.error('Failed to convert text to JSON RTE:', error); @@ -352,7 +350,7 @@ const processFieldByType = ( assetId: any, referenceId: any, ): any => { - if (!fieldMapping || !fieldMapping.contentstackFieldType) { + if (!fieldMapping || !fieldMapping?.contentstackFieldType) { return value; } @@ -361,7 +359,7 @@ const processFieldByType = ( return value; } - const targetType = fieldMapping.contentstackFieldType; + const targetType = fieldMapping?.contentstackFieldType; // Simple switch based on target type (like WordPress/Contentful) switch (targetType) { @@ -447,9 +445,9 @@ const processFieldByType = ( // Multiple files if (Array.isArray(value)) { const validAssets = value - .map((assetRef) => { + ?.map((assetRef) => { const assetKey = `assets_${assetRef}`; - const assetReference = assetId[assetKey]; + const assetReference = assetId?.[assetKey]; if (assetReference && typeof assetReference === 'object') { return assetReference; @@ -460,14 +458,14 @@ const processFieldByType = ( ); return null; }) - .filter((asset) => asset !== null); // Remove null entries + ?.filter((asset) => asset !== null); // Remove null entries - return validAssets.length > 0 ? validAssets : undefined; // Return undefined if no valid assets + return validAssets?.length > 0 ? validAssets : undefined; // Return undefined if no valid assets } } else { // Single file const assetKey = `assets_${value}`; - const assetReference = assetId[assetKey]; + const assetReference = assetId?.[assetKey]; if (assetReference && typeof assetReference === 'object') { return assetReference; @@ -484,14 +482,14 @@ const processFieldByType = ( if (fieldMapping.advanced?.multiple) { // Multiple references if (Array.isArray(value)) { - return value.map( + return value?.map( (refId) => - referenceId[`content_type_entries_title_${refId}`] || refId, + referenceId?.[`content_type_entries_title_${refId}`] || refId, ); } } else { // Single reference - return [referenceId[`content_type_entries_title_${value}`] || value]; + return [referenceId?.[`content_type_entries_title_${value}`] || value]; } return value; } @@ -554,7 +552,7 @@ const processFieldByType = ( if (typeof value === 'string' && /<\/?[a-z][\s\S]*>/i.test(value)) { try { const dom = new JSDOM(value); - const htmlDoc = dom.window.document.querySelector('body'); + const htmlDoc = dom?.window?.document?.querySelector('body'); return htmlToJson(htmlDoc); } catch (error) { return value; @@ -595,23 +593,23 @@ const consolidateTaxonomyFields = ( if ( taxonomyItem && typeof taxonomyItem === 'object' && - taxonomyItem.taxonomy_uid && - taxonomyItem.term_uid + taxonomyItem?.taxonomy_uid && + taxonomyItem?.term_uid ) { // Check for unique term_uid (avoid duplicates) - if (!seenTermUids.has(taxonomyItem.term_uid)) { - consolidatedTaxonomies.push({ - taxonomy_uid: taxonomyItem.taxonomy_uid, - term_uid: taxonomyItem.term_uid, + if (!seenTermUids?.has(taxonomyItem?.term_uid)) { + consolidatedTaxonomies?.push({ + taxonomy_uid: taxonomyItem?.taxonomy_uid, + term_uid: taxonomyItem?.term_uid, }); - seenTermUids.add(taxonomyItem.term_uid); + seenTermUids?.add(taxonomyItem?.term_uid); } } } } // Mark this field for removal - fieldsToRemove.push(fieldKey); + fieldsToRemove?.push(fieldKey); } } @@ -620,11 +618,11 @@ const consolidateTaxonomyFields = ( // Remove original taxonomy fields for (const fieldKey of fieldsToRemove) { - delete consolidatedEntry[fieldKey]; + delete consolidatedEntry?.[fieldKey]; } // Add consolidated taxonomy field if we have any taxonomies - if (consolidatedTaxonomies.length > 0) { + if (consolidatedTaxonomies?.length > 0) { consolidatedEntry.taxonomies = consolidatedTaxonomies; } @@ -636,7 +634,7 @@ const consolidateTaxonomyFields = ( */ const processFieldData = async ( entryData: DrupalEntry, - fieldConfigs: DrupalFieldConfig[], + fieldConfigs: any, assetId: any, referenceId: any, taxonomyId: any, @@ -657,34 +655,34 @@ const processFieldData = async ( for (const [dataKey, value] of Object.entries(entryData)) { // Extract field name from dataKey (remove _target_id suffix) const fieldName = dataKey - .replace(/_target_id$/, '') - .replace(/_value$/, '') - .replace(/_status$/, '') - .replace(/_uri$/, ''); + ?.replace(/_target_id$/, '') + ?.replace(/_value$/, '') + ?.replace(/_status$/, '') + ?.replace(/_uri$/, ''); // Handle asset fields using field analysis if ( - dataKey.endsWith('_target_id') && + dataKey?.endsWith('_target_id') && isAssetField(fieldName, contentType, assetFieldMapping) ) { const assetKey = `assets_${value}`; - if (assetKey in assetId) { + if (assetId && assetKey in assetId) { // Transform to proper Contentstack asset reference format - const assetReference = assetId[assetKey]; + const assetReference = assetId?.[assetKey]; if (assetReference && typeof assetReference === 'object') { processedData[dataKey] = assetReference; } // If asset reference is not properly structured, skip the field } // If asset not found in assets index, mark field as skipped - skippedFields.add(dataKey); + skippedFields?.add(dataKey); continue; // Skip further processing for this field } // Handle entity references (taxonomy and node references) using field analysis // NOTE: value can be a number (single reference) or string (GROUP_CONCAT comma-separated IDs) if ( - dataKey.endsWith('_target_id') && + dataKey?.endsWith('_target_id') && (typeof value === 'number' || typeof value === 'string') ) { // Check if this is a taxonomy field using our field analysis @@ -693,9 +691,9 @@ const processFieldData = async ( const targetIds = typeof value === 'string' ? value - .split(',') - .map((id) => parseInt(id.trim())) - .filter((id) => !isNaN(id)) + ?.split(',') + ?.map((id) => parseInt(id?.trim())) + ?.filter((id) => !isNaN(id)) : [value]; const transformedTaxonomies: Array<{ @@ -705,12 +703,12 @@ const processFieldData = async ( for (const tid of targetIds) { // Look up taxonomy reference using drupal_term_id - const taxonomyRef = taxonomyReferenceLookup[tid]; + const taxonomyRef = taxonomyReferenceLookup?.[tid]; if (taxonomyRef) { transformedTaxonomies.push({ - taxonomy_uid: taxonomyRef.taxonomy_uid, - term_uid: taxonomyRef.term_uid, + taxonomy_uid: taxonomyRef?.taxonomy_uid, + term_uid: taxonomyRef?.term_uid, }); } else { console.warn( @@ -719,7 +717,7 @@ const processFieldData = async ( } } - if (transformedTaxonomies.length > 0) { + if (transformedTaxonomies?.length > 0) { processedData[dataKey] = transformedTaxonomies; } else { // Fallback to original value if no lookups succeeded @@ -727,8 +725,8 @@ const processFieldData = async ( } // Mark field as processed so it doesn't get overwritten by ctValue loop - processedFields.add(dataKey); - skippedFields.add(dataKey); // Also skip in ctValue loop + processedFields?.add(dataKey); + skippedFields?.add(dataKey); // Also skip in ctValue loop continue; // Skip further processing for this field } else if ( @@ -748,8 +746,8 @@ const processFieldData = async ( for (const nid of targetIds) { const referenceKey = `content_type_entries_title_${nid}`; - if (referenceKey in referenceId) { - transformedReferences.push(referenceId[referenceKey]); + if (referenceId && referenceKey in referenceId) { + transformedReferences?.push(referenceId?.[referenceKey]); } } @@ -769,56 +767,56 @@ const processFieldData = async ( } // Handle other field types by checking field configs - const matchingFieldConfig = fieldConfigs.find( - (fc) => - dataKey === `${fc.field_name}_value` || - dataKey === `${fc.field_name}_status` || - dataKey === fc.field_name, + const matchingFieldConfig = fieldConfigs?.find( + (fc: any) => + dataKey === `${fc?.field_name}_value` || + dataKey === `${fc?.field_name}_status` || + dataKey === fc?.field_name, ); if (matchingFieldConfig) { // Handle datetime and timestamps if ( - matchingFieldConfig.field_type === 'datetime' || - matchingFieldConfig.field_type === 'timestamp' + matchingFieldConfig?.field_type === 'datetime' || + matchingFieldConfig?.field_type === 'timestamp' ) { - if (dataKey === `${matchingFieldConfig.field_name}_value`) { + if (dataKey === `${matchingFieldConfig?.field_name}_value`) { if (typeof value === 'number') { processedData[dataKey] = new Date(value * 1000).toISOString(); } else { - processedData[dataKey] = isoDate.toISOString(); + processedData[dataKey] = isoDate?.toISOString(); } // Mark field as processed to avoid duplicate processing in second loop - processedFields.add(dataKey); - processedFields.add(matchingFieldConfig.field_name); + processedFields?.add(dataKey); + processedFields?.add(matchingFieldConfig?.field_name); continue; } } // Handle boolean fields - if (matchingFieldConfig.field_type === 'boolean') { + if (matchingFieldConfig?.field_type === 'boolean') { if ( - dataKey === `${matchingFieldConfig.field_name}_value` && + dataKey === `${matchingFieldConfig?.field_name}_value` && typeof value === 'number' ) { processedData[dataKey] = value === 1; // Mark field as processed to avoid duplicate processing in second loop - processedFields.add(dataKey); - processedFields.add(matchingFieldConfig.field_name); + processedFields?.add(dataKey); + processedFields?.add(matchingFieldConfig?.field_name); continue; } } // Handle comment fields - if (matchingFieldConfig.field_type === 'comment') { + if (matchingFieldConfig?.field_type === 'comment') { if ( - dataKey === `${matchingFieldConfig.field_name}_status` && + dataKey === `${matchingFieldConfig?.field_name}_status` && typeof value === 'number' ) { processedData[dataKey] = `${value}`; // Mark field as processed to avoid duplicate processing in second loop - processedFields.add(dataKey); - processedFields.add(matchingFieldConfig.field_name); + processedFields?.add(dataKey); + processedFields?.add(matchingFieldConfig?.field_name); continue; } } @@ -845,7 +843,7 @@ const processFieldData = async ( continue; } - const value = entryData[fieldName]; + const value = entryData?.[fieldName]; if (fieldName === 'created') { ctValue[fieldName] = new Date(value * 1000).toISOString(); @@ -871,7 +869,7 @@ const processFieldData = async ( const titleFieldName = `${baseFieldName}_title`; // Check if we also have title data - const titleValue = entryData[titleFieldName]; + const titleValue = entryData?.[titleFieldName]; if (value) { ctValue[baseFieldName] = { @@ -899,7 +897,7 @@ const processFieldData = async ( const baseFieldName = fieldName.replace('_title', ''); const uriFieldName = `${baseFieldName}_uri`; - if (entryData[uriFieldName]) { + if (entryData?.[uriFieldName]) { // URI field will handle this, skip processing here continue; } else { @@ -965,21 +963,21 @@ const processFieldData = async ( if (isValueField) { const baseFieldName = key.replace('_value', ''); // Only include the _value field if the base field doesn't exist - if (!mergedData.hasOwnProperty(baseFieldName)) { + if (!mergedData?.hasOwnProperty(baseFieldName)) { cleanedEntry[key] = val; } // If base field exists, skip the _value field (base field takes priority) } else if (isStatusField) { - const baseFieldName = key.replace('_status', ''); + const baseFieldName = key?.replace('_status', ''); // Only include the _status field if the base field doesn't exist - if (!mergedData.hasOwnProperty(baseFieldName)) { + if (!mergedData?.hasOwnProperty(baseFieldName)) { cleanedEntry[key] = val; } // If base field exists, skip the _status field (base field takes priority) } else if (isUriField) { - const baseFieldName = key.replace('_uri', ''); + const baseFieldName = key?.replace('_uri', ''); // Only include the _uri field if the base field doesn't exist - if (!mergedData.hasOwnProperty(baseFieldName)) { + if (!mergedData?.hasOwnProperty(baseFieldName)) { cleanedEntry[key] = val; } // If base field exists, skip the _uri field (base field takes priority) @@ -998,10 +996,9 @@ const processFieldData = async ( */ const processEntries = async ( connection: mysql.Connection, - contentType: string, + contentType: any, skip: number, queryPageConfig: QueryConfig, - fieldConfigs: DrupalFieldConfig[], assetId: any, referenceId: any, taxonomyId: any, @@ -1012,33 +1009,33 @@ const processEntries = async ( projectId: string, destination_stack_id: string, masterLocale: string, - contentTypeMapping: any[] = [], isTest: boolean = false, project: any = null, ): Promise<{ [key: string]: any } | null> => { const srcFunc = 'processEntries'; try { + const keyMapper = project?.mapperKeys || {}; // Following original pattern: queryPageConfig['page']['' + pagename + ''] - const baseQuery = queryPageConfig['page'][contentType]; + const baseQuery = queryPageConfig?.['page']?.[contentType?.otherCmsUid]; if (!baseQuery) { - throw new Error(`No query found for content type: ${contentType}`); + throw new Error(`No query found for content type: ${contentType?.otherCmsUid}`); } // Check if this is an optimized query (content type with many fields) - const isOptimizedQuery = baseQuery.includes('/* OPTIMIZED_NO_JOINS:'); + const isOptimizedQuery = baseQuery?.includes('/* OPTIMIZED_NO_JOINS:'); let entries: any[] = []; if (isOptimizedQuery) { // Handle content types with many fields using optimized approach - const fieldCountMatch = baseQuery.match( + const fieldCountMatch = baseQuery?.match( /\/\* OPTIMIZED_NO_JOINS:(\d+) \*\//, ); - const fieldCount = fieldCountMatch ? parseInt(fieldCountMatch[1]) : 0; + const fieldCount = fieldCountMatch ? parseInt(fieldCountMatch?.[1]) : 0; const optimizedMessage = getLogMessage( srcFunc, - `Processing ${contentType} with optimized field fetching (${fieldCount} fields)`, + `Processing ${contentType?.otherCmsUid} with optimized field fetching (${fieldCount} fields)`, {}, ); await customLogger( @@ -1051,12 +1048,12 @@ const processEntries = async ( // Execute base query without field JOINs const effectiveLimit = isTest ? 1 : LIMIT; const cleanBaseQuery = baseQuery - .replace(/\/\* OPTIMIZED_NO_JOINS:\d+ \*\//, '') - .trim(); + ?.replace(/\/\* OPTIMIZED_NO_JOINS:\d+ \*\//, '') + ?.trim(); const query = cleanBaseQuery + ` LIMIT ${skip}, ${effectiveLimit}`; const baseEntries = await executeQuery(connection, query); - if (baseEntries.length === 0) { + if (baseEntries?.length === 0) { return null; } @@ -1066,24 +1063,24 @@ const processEntries = async ( projectId, destination_stack_id, ); - const nodeIds = baseEntries.map((entry) => entry.nid); - const fieldsForType = await fieldFetcher.getFieldsForContentType( + const nodeIds = baseEntries?.map((entry) => entry?.nid); + const fieldsForType = await fieldFetcher?.getFieldsForContentType( contentType, ); - if (fieldsForType.length > 0) { - const fieldData = await fieldFetcher.fetchFieldDataForContentType( + if (fieldsForType?.length > 0) { + const fieldData = await fieldFetcher?.fetchFieldDataForContentType( contentType, nodeIds, fieldsForType, ); // Merge base entries with field data - entries = fieldFetcher.mergeNodeAndFieldData(baseEntries, fieldData); + entries = fieldFetcher?.mergeNodeAndFieldData(baseEntries, fieldData); const mergeMessage = getLogMessage( srcFunc, - `Merged ${baseEntries.length} base entries with field data for ${contentType}`, + `Merged ${baseEntries?.length} base entries with field data for ${contentType}`, {}, ); await customLogger( @@ -1101,7 +1098,7 @@ const processEntries = async ( const query = baseQuery + ` LIMIT ${skip}, ${effectiveLimit}`; entries = await executeQuery(connection, query); - if (entries.length === 0) { + if (entries?.length === 0) { return null; } } @@ -1110,23 +1107,23 @@ const processEntries = async ( const entriesByLocale: { [locale: string]: any[] } = {}; // Group entries by their langcode - if (entries && Array.isArray(entries)) { - entries.forEach((entry) => { + if (entries && Array?.isArray(entries)) { + entries?.forEach((entry) => { if (!entry) return; const entryLocale = entry?.langcode || masterLocale; // fallback to masterLocale if no langcode - if (!entriesByLocale[entryLocale]) { + if (!entriesByLocale?.[entryLocale]) { entriesByLocale[entryLocale] = []; } - entriesByLocale[entryLocale].push(entry); + entriesByLocale?.[entryLocale]?.push(entry); }); } // Map source locales to destination locales using user-selected mapping from UI // This replaces the old hardcoded transformation rules with dynamic user mapping const transformedEntriesByLocale: { [locale: string]: any[] } = {}; - const allLocales = Object.keys(entriesByLocale); - const hasEn = allLocales.includes('en'); - const hasEnUs = allLocales.includes('en-us'); + const allLocales = Object?.keys(entriesByLocale); + const hasEn = allLocales?.includes('en'); + const hasEnUs = allLocales?.includes('en-us'); // Get locale mapping configuration from project const localeMapping = project?.localeMapping || {}; @@ -1144,13 +1141,13 @@ const processEntries = async ( const masterLocaleKey = `${sourceMasterLocale}-master_locale`; const destinationMasterLocale = localeMapping?.[masterLocaleKey] || - Object.values(project?.master_locale || {})?.[0] || // ✅ Use values() not keys()! + Object?.values(project?.master_locale || {})?.[0] || // ✅ Use values() not keys()! project?.stackDetails?.master_locale || masterLocale || 'en-us'; // Apply source locale transformation rules first (und → en-us, etc.) // Then map the transformed source locale to destination locale using user's selection - Object.entries(entriesByLocale).forEach(([originalLocale, entries]) => { + Object?.entries(entriesByLocale)?.forEach(([originalLocale, entries]) => { // Step 1: Apply Drupal-specific transformation rules (same as before) let transformedSourceLocale = originalLocale; @@ -1190,9 +1187,9 @@ const processEntries = async ( }); // Merge entries if destination locale already has entries - if (transformedEntriesByLocale[destinationLocale]) { + if (transformedEntriesByLocale?.[destinationLocale]) { transformedEntriesByLocale[destinationLocale] = [ - ...transformedEntriesByLocale[destinationLocale], + ...transformedEntriesByLocale?.[destinationLocale], ...entries, ]; } else { @@ -1200,61 +1197,22 @@ const processEntries = async ( } }); - // Find content type mapping for field type switching - const currentContentTypeMapping = contentTypeMapping.find( - (ct) => - ct.otherCmsUid === contentType || ct.contentstackUid === contentType, - ); + const allProcessedContent: { [key: string]: any } = {}; - // Pre-load FieldMapper and ContentTypesMapper databases once before processing - // (avoids repeated filesystem/lowdb reads inside the per-entry loop) - await FieldMapperModel.read(); - const allFieldMappings = FieldMapperModel.data?.field_mapper || []; - - await ContentTypesMapperModel.read(); - const contentTypesMappers = - ContentTypesMapperModel.data?.ContentTypesMappers || []; - const ctMapper = contentTypesMappers.find( - (ct: any) => - ct?.projectId === projectId && - (ct?.contentstackUid === contentType || - ct?.otherCmsUid === contentType), - ); - const currentContentTypeId = ctMapper?.id; - - // Pre-load and parse combined schema.json once before processing - // (avoids repeated file reads inside the per-field loop) - let cachedAllSchemas: any[] | null = null; - try { - const combinedSchemaPath = path.join( - MIGRATION_DATA_CONFIG.DATA, - destination_stack_id, - 'content_types', - MIGRATION_DATA_CONFIG.CONTENT_TYPES_SCHEMA_FILE, // schema.json - ); - cachedAllSchemas = JSON.parse( - await fs.promises.readFile(combinedSchemaPath, 'utf8'), - ); - } catch { - // Schema file may not exist yet; fallback handled in field loop - cachedAllSchemas = null; - } - const cachedContentTypeSchema = Array.isArray(cachedAllSchemas) - ? cachedAllSchemas.find((ct: any) => ct.uid === contentType) - : null; // Process entries for each transformed locale separately for (const [currentLocale, localeEntries] of Object.entries( transformedEntriesByLocale, )) { + const contentTypeUid = keyMapper?.[contentType?.otherCmsUid] ? keyMapper?.[contentType?.otherCmsUid] : contentType?.contentstackUid; // Create folder structure: entries/contentType/locale/ const contentTypeFolderPath = path.join( MIGRATION_DATA_CONFIG.DATA, destination_stack_id, MIGRATION_DATA_CONFIG.ENTRIES_DIR_NAME, - contentType, + contentTypeUid, ); const localeFolderPath = path.join(contentTypeFolderPath, currentLocale); await fs.promises.mkdir(localeFolderPath, { recursive: true }); @@ -1269,9 +1227,10 @@ const processEntries = async ( // Process each entry in this locale for (const entry of localeEntries) { + let processedEntry = await processFieldData( entry, - fieldConfigs, + contentType?.fieldMapping, assetId, referenceId, taxonomyId, @@ -1279,154 +1238,56 @@ const processEntries = async ( referenceFieldMapping, assetFieldMapping, taxonomyReferenceLookup, - contentType, + contentTypeUid, prefix, ); // 🏷️ TAXONOMY CONSOLIDATION: Merge all taxonomy fields into single 'taxonomies' field processedEntry = consolidateTaxonomyFields( processedEntry, - contentType, + contentTypeUid, taxonomyFieldMapping, ); // Apply field type switching based on user's UI selections (from content type schema) const enhancedEntry: any = {}; + const fieldMappings = contentType?.fieldMapping; - // Process each field with type switching support - // (FieldMapper & ContentTypesMapper data already loaded above the loop) for (const [fieldName, fieldValue] of Object.entries(processedEntry)) { - let fieldMapping = null; - - // PRIORITY 1: Read DIRECTLY from FieldMapper database (most accurate source of user's UI selections) - // This ensures we always use the latest field type even if schema hasn't been regenerated const cleanedFieldName = fieldName - .replace(/_target_id$/, '') - .replace(/_value$/, ''); - - // Only trust FieldMapper rows when we can positively identify the current content type. - // If currentContentTypeId is not resolved, skip DB mappings and fall back to schema/UI mapping - // to avoid matching rows from a different content type (wrong field type or stale isDeleted). - let dbFieldMapping: any = null; - if (currentContentTypeId) { - dbFieldMapping = allFieldMappings.find( - (fm: any) => - fm?.projectId === projectId && - fm?.contentTypeId === currentContentTypeId && - (fm?.uid === fieldName || - fm?.uid === cleanedFieldName || - fm?.contentstackFieldUid === fieldName || - fm?.contentstackFieldUid === cleanedFieldName), - ); - } - - if (dbFieldMapping) { - // Skip fields that were unselected by the user in the UI (isDeleted: true) - if (dbFieldMapping.isDeleted === true) { - continue; // Do not include this field in the migrated entry - } - - if (dbFieldMapping.contentstackFieldType) { - // Use field type directly from database (user's latest UI selection) - fieldMapping = { - uid: fieldName, - contentstackFieldType: dbFieldMapping.contentstackFieldType, - backupFieldType: - dbFieldMapping.backupFieldType || - dbFieldMapping.contentstackFieldType, - advanced: dbFieldMapping.advanced || {}, - }; - } - } - - // PRIORITY 2: If not in FieldMapper DB, try schema.json (pre-loaded above the loop) - if (!fieldMapping) { - try { - // Find field in schema - use exact matching only to avoid false matches - // (e.g. "field_subtitle".includes("title") would wrongly match the "title" field) - const schemaField = cachedContentTypeSchema?.schema?.find( - (field: any) => - field.uid === fieldName || - field.uid === fieldName.replace(/_target_id$/, '') || - field.uid === fieldName.replace(/_value$/, ''), - ); - - if (schemaField) { - // Determine the proper field type based on schema configuration - let targetFieldType = schemaField.data_type; - - // Handle HTML RTE fields (text with allow_rich_text: true) - if ( - schemaField.data_type === 'text' && - schemaField.field_metadata?.allow_rich_text === true - ) { - targetFieldType = 'html'; // ✅ HTML RTE field - } - // Handle JSON RTE fields - else if (schemaField.data_type === 'json') { - targetFieldType = 'json'; // ✅ JSON RTE field - } - // Handle text fields with multiline metadata - else if ( - schemaField.data_type === 'text' && - schemaField.field_metadata?.multiline - ) { - targetFieldType = 'multi_line_text'; // ✅ Multi-line text field - } - - // Create a mapping from schema field - fieldMapping = { - uid: fieldName, - contentstackFieldType: targetFieldType, - backupFieldType: schemaField.data_type, - advanced: schemaField, - }; - } - } catch (error: any) { - // Schema not found, will try fallback below - } - } - - // FALLBACK: If neither DB nor schema found, try UI content type mapping - if ( - !fieldMapping && - currentContentTypeMapping && - currentContentTypeMapping.fieldMapping - ) { - const fallbackMapping = currentContentTypeMapping.fieldMapping.find( - (fm: any) => - fm.uid === fieldName || fm.otherCmsField === fieldName, - ); - - // Skip fields that were unselected by the user in the UI (isDeleted: true) - if (fallbackMapping?.isDeleted === true) { - continue; // Do not include this field in the migrated entry - } - - fieldMapping = fallbackMapping; + ?.replace(/_target_id$/, '') + ?.replace(/_value$/, ''); + + // Find the specific mapping item that matches this field + const matchingMapping = fieldMappings?.find( + (item: any) => + item?.uid === cleanedFieldName || + item?.contentstackFieldUid === cleanedFieldName || + item?.otherCmsField === cleanedFieldName, + ); + if(matchingMapping?.isDeleted){ + continue; } - if (fieldMapping) { - // Apply field type processing based on user's selection + if (matchingMapping) { const processedValue = processFieldByType( fieldValue, - fieldMapping, + matchingMapping, assetId, referenceId, ); - // Only add field if processed value is not undefined (undefined means remove field) if (processedValue !== undefined) { - enhancedEntry[fieldName] = processedValue; + const entryFieldKey = matchingMapping?.contentstackFieldUid || fieldName; + enhancedEntry[entryFieldKey] = processedValue; - // Log field type processing if ( - fieldMapping.contentstackFieldType !== - fieldMapping.backupFieldType + matchingMapping?.contentstackFieldType !== + matchingMapping?.backupFieldType ) { const message = getLogMessage( srcFunc, - `Field ${fieldName} processed as ${fieldMapping.contentstackFieldType} (switched from ${fieldMapping.backupFieldType})`, + `Field ${fieldName} processed as ${matchingMapping?.contentstackFieldType} (switched from ${matchingMapping?.backupFieldType})`, {}, ); await customLogger( @@ -1437,7 +1298,6 @@ const processEntries = async ( ); } } else { - // Log field removal const message = getLogMessage( srcFunc, `Field ${fieldName} removed due to missing or invalid asset reference`, @@ -1451,7 +1311,6 @@ const processEntries = async ( ); } } else { - // Keep original value if no mapping found enhancedEntry[fieldName] = fieldValue; } } @@ -1461,9 +1320,9 @@ const processEntries = async ( // Add publish_details as an empty array to the end of entry creation processedEntry.publish_details = []; - if (typeof entry.nid === 'number') { + if (typeof entry?.nid === 'number') { const entryUid = uidCorrector({ - id: `content_type_entries_title_${entry.nid}`, + id: `content_type_entries_title_${entry?.nid}`, prefix, }); existingLocaleContent[entryUid] = processedEntry; @@ -1473,7 +1332,7 @@ const processEntries = async ( // Log each entry transformation const message = getLogMessage( srcFunc, - `Entry with uid ${entry.nid} (locale: ${currentLocale}) for content type ${contentType} has been successfully transformed.`, + `Entry with uid ${entry?.nid} (locale: ${currentLocale}) for content type ${contentType?.otherCmsUid} has been successfully transformed.`, {}, ); await customLogger(projectId, destination_stack_id, 'info', message); @@ -1484,7 +1343,7 @@ const processEntries = async ( const localeMessage = getLogMessage( srcFunc, - `Successfully processed ${localeEntries.length} entries for locale ${currentLocale} in content type ${contentType}`, + `Successfully processed ${localeEntries?.length} entries for locale ${currentLocale} in content type ${contentType?.otherCmsUid}`, {}, ); await customLogger( @@ -1496,17 +1355,18 @@ const processEntries = async ( } // 📁 Create mandatory index.json files for each transformed locale directory - for (const [currentLocale, localeEntries] of Object.entries( + for (const [currentLocale, localeEntries] of Object?.entries( transformedEntriesByLocale, )) { - if (localeEntries.length > 0) { - const contentTypeFolderPath = path.join( + if (localeEntries?.length > 0) { + const contentTypeUid = keyMapper?.[contentType?.otherCmsUid] ? keyMapper?.[contentType?.otherCmsUid] : contentType?.contentstackUid; + const contentTypeFolderPath = path?.join( MIGRATION_DATA_CONFIG.DATA, destination_stack_id, MIGRATION_DATA_CONFIG.ENTRIES_DIR_NAME, - contentType, + contentTypeUid, ); - const localeFolderPath = path.join( + const localeFolderPath = path?.join( contentTypeFolderPath, currentLocale, ); @@ -1522,7 +1382,7 @@ const processEntries = async ( } catch (error: any) { const message = getLogMessage( srcFunc, - `Error processing entries for ${contentType}: ${error.message}`, + `Error processing entries for ${contentType?.otherCmsUid || contentType?.contentstackUid}: ${error?.message}`, {}, error, ); @@ -1536,9 +1396,8 @@ const processEntries = async ( */ const processContentType = async ( connection: mysql.Connection, - contentType: string, + contentType: any, queryPageConfig: QueryConfig, - fieldConfigs: DrupalFieldConfig[], assetId: any, referenceId: any, taxonomyId: any, @@ -1549,7 +1408,6 @@ const processContentType = async ( projectId: string, destination_stack_id: string, masterLocale: string, - contentTypeMapping: any[] = [], isTest: boolean = false, project: any = null, ): Promise => { @@ -1557,23 +1415,23 @@ const processContentType = async ( try { // Get total count for pagination (if count query exists) - const countKey = `${contentType}Count`; + const countKey = `${contentType?.otherCmsUid}Count`; let totalCount = 1; // Default to process at least one batch - if (queryPageConfig.count && queryPageConfig.count[countKey]) { - const countQuery = queryPageConfig.count[countKey]; + if (queryPageConfig?.count && queryPageConfig?.count[countKey]) { + const countQuery = queryPageConfig?.count?.[countKey]; const countResults = await executeQuery(connection, countQuery); - totalCount = countResults[0]?.countentry || 0; + totalCount = countResults?.[0]?.countentry ?? 0; } if (totalCount === 0) { const message = getLogMessage( srcFunc, - `No entries found for content type ${contentType}.`, + `No entries found for content type ${contentType?.otherCmsUid}.`, {}, ); await customLogger(projectId, destination_stack_id, 'info', message); - return; + //return; } // 🧪 Process entries in batches (test migration: single entry, main migration: all entries) @@ -1589,7 +1447,6 @@ const processContentType = async ( contentType, i, queryPageConfig, - fieldConfigs, assetId, referenceId, taxonomyId, @@ -1600,7 +1457,6 @@ const processContentType = async ( projectId, destination_stack_id, masterLocale, - contentTypeMapping, isTest, project, ); @@ -1613,7 +1469,7 @@ const processContentType = async ( } catch (error: any) { const message = getLogMessage( srcFunc, - `Error processing content type ${contentType}: ${error.message}`, + `Error processing content type ${contentType?.otherCmsUid || contentType?.contentstackUid}: ${error?.message}`, {}, error, ); @@ -1662,16 +1518,16 @@ export const createEntry = async ( projectId: string, isTest = false, masterLocale = 'en-us', - contentTypeMapping: any[] = [], project: any = null, + contentTypes: any[] = [] ): Promise => { const srcFunc = 'createEntry'; let connection: mysql.Connection | null = null; try { - const entriesSave = path.join(DATA, destination_stack_id, ENTRIES_DIR_NAME); - const assetsSave = path.join(DATA, destination_stack_id, ASSETS_DIR_NAME); - const referencesSave = path.join( + const entriesSave = path?.join(DATA, destination_stack_id, ENTRIES_DIR_NAME); + const assetsSave = path?.join(DATA, destination_stack_id, ASSETS_DIR_NAME); + const referencesSave = path?.join( DATA, destination_stack_id, REFERENCES_DIR_NAME, @@ -1724,18 +1580,22 @@ export const createEntry = async ( referencesSave, ); - // Process each content type from query config (like original) - const pageQuery = queryPageConfig.page; - const contentTypes = Object.keys(pageQuery); - // 🧪 Test migration: Process ALL content types but with limited data per content type - const typesToProcess = contentTypes; // Always process all content types - - for (const contentType of typesToProcess) { + // Use passed contentTypes if provided, otherwise fall back to query config keys + const pageQuery = queryPageConfig?.page; + const typesToProcess = + contentTypes?.length > 0 + ? contentTypes?.filter((ct: any) => { + const uid = ct?.otherCmsUid || ct?.contentstackUid || ct; + return ct; + }) + : Object?.keys(pageQuery); + + + for (const contentType of contentTypes || []) { await processContentType( connection, contentType, queryPageConfig, - fieldConfigs, assetId, referenceId, taxonomyId, @@ -1746,7 +1606,6 @@ export const createEntry = async ( projectId, destination_stack_id, masterLocale, - contentTypeMapping, isTest, project, ); @@ -1754,7 +1613,7 @@ export const createEntry = async ( const successMessage = getLogMessage( srcFunc, - `Successfully processed entries for ${typesToProcess.length} content types with multilingual support.`, + `Successfully processed entries for ${typesToProcess?.length} content types with multilingual support.`, {}, ); await customLogger(projectId, destination_stack_id, 'info', successMessage); diff --git a/api/src/services/drupal/field-analysis.service.ts b/api/src/services/drupal/field-analysis.service.ts index d7492b86d..029b66db9 100644 --- a/api/src/services/drupal/field-analysis.service.ts +++ b/api/src/services/drupal/field-analysis.service.ts @@ -58,11 +58,11 @@ const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']); const isSafeKey = (key: string): boolean => { return ( typeof key === 'string' && - key.length > 0 && - !DANGEROUS_KEYS.has(key) && - !key.includes('__proto__') && - !key.includes('constructor') && - !key.includes('prototype') + key?.length > 0 && + !DANGEROUS_KEYS?.has(key) && + !key?.includes('__proto__') && + !key?.includes('constructor') && + !key?.includes('prototype') ); }; @@ -70,7 +70,7 @@ const isSafeKey = (key: string): boolean => { * Creates a null-prototype object to prevent prototype pollution */ const createSafeMapping = (): Record => { - return Object.create(null) as Record; + return Object?.create(null) as Record; }; /** @@ -85,7 +85,7 @@ const safeSetMapping = ( if (!isSafeKey(contentType) || !isSafeKey(fieldName)) { return false; } - if (!Object.prototype.hasOwnProperty.call(mapping, contentType)) { + if (!Object?.prototype?.hasOwnProperty?.call(mapping, contentType)) { mapping[contentType] = createSafeMapping(); } mapping[contentType][fieldName] = value; @@ -163,15 +163,15 @@ export const analyzeFieldTypes = async ( try { // Unserialize the PHP data to get field details const { unserialize } = await import('php-serialize'); - const fieldData = unserialize(fieldConfig.data); + const fieldData = unserialize(fieldConfig?.data); - if (fieldData && fieldData.field_name && fieldData.bundle) { + if (fieldData && fieldData?.field_name && fieldData?.bundle) { totalFieldCount++; const fieldInfo: FieldInfo = { - field_name: fieldData.field_name, - content_types: fieldData.bundle, - field_type: fieldData.field_type || 'unknown', + field_name: fieldData?.field_name, + content_types: fieldData?.bundle, + field_type: fieldData?.field_type || 'unknown', content_handler: fieldData?.settings?.handler, target_type: fieldData?.settings?.target_type, handler_settings: fieldData?.settings?.handler_settings, @@ -179,12 +179,12 @@ export const analyzeFieldTypes = async ( // Validate keys to prevent prototype pollution if ( - !isSafeKey(fieldInfo.content_types) || - !isSafeKey(fieldInfo.field_name) + !isSafeKey(fieldInfo?.content_types) || + !isSafeKey(fieldInfo?.field_name) ) { const warnMessage = getLogMessage( srcFunc, - `Skipping field with unsafe key: ${fieldInfo.content_types}.${fieldInfo.field_name}`, + `Skipping field with unsafe key: ${fieldInfo?.content_types}.${fieldInfo?.field_name}`, {} ); await customLogger( @@ -200,55 +200,55 @@ export const analyzeFieldTypes = async ( if ( !Object.prototype.hasOwnProperty.call( taxonomyFieldMapping, - fieldInfo.content_types + fieldInfo?.content_types ) ) { - taxonomyFieldMapping[fieldInfo.content_types] = createSafeMapping(); + taxonomyFieldMapping[fieldInfo?.content_types] = createSafeMapping(); } if ( !Object.prototype.hasOwnProperty.call( referenceFieldMapping, - fieldInfo.content_types + fieldInfo?.content_types ) ) { - referenceFieldMapping[fieldInfo.content_types] = + referenceFieldMapping[fieldInfo?.content_types] = createSafeMapping(); } if ( !Object.prototype.hasOwnProperty.call( assetFieldMapping, - fieldInfo.content_types + fieldInfo?.content_types ) ) { - assetFieldMapping[fieldInfo.content_types] = createSafeMapping(); + assetFieldMapping[fieldInfo?.content_types] = createSafeMapping(); } // Check if this is a taxonomy reference field const isTaxonomyField = // Check handler for taxonomy references - (fieldInfo.content_handler && - fieldInfo.content_handler.includes('taxonomy_term')) || + (fieldInfo?.content_handler && + fieldInfo?.content_handler?.includes('taxonomy_term')) || // Check target_type for entity references to taxonomy terms - fieldInfo.target_type === 'taxonomy_term' || + fieldInfo?.target_type === 'taxonomy_term' || // Check field type for direct taxonomy reference fields - (fieldInfo.field_type === 'entity_reference' && - fieldInfo.target_type === 'taxonomy_term') || - fieldInfo.field_type === 'taxonomy_term_reference' || + (fieldInfo?.field_type === 'entity_reference' && + fieldInfo?.target_type === 'taxonomy_term') || + fieldInfo?.field_type === 'taxonomy_term_reference' || // Check handler settings for vocabulary restrictions (taxonomy specific) - (fieldInfo.handler_settings?.target_bundles && - Object.keys(fieldInfo.handler_settings.target_bundles).some( - (bundle) => fieldInfo.target_type === 'taxonomy_term' + (fieldInfo?.handler_settings?.target_bundles && + Object.keys(fieldInfo?.handler_settings?.target_bundles).some( + (bundle) => fieldInfo?.target_type === 'taxonomy_term' )); // Check if this is a node reference field (non-taxonomy entity reference) const isReferenceField = // Check for entity_reference field type - (fieldInfo.field_type === 'entity_reference' && + (fieldInfo?.field_type === 'entity_reference' && // Check handler for node references - fieldInfo.content_handler && - fieldInfo.content_handler.includes('node')) || + fieldInfo?.content_handler && + fieldInfo?.content_handler.includes('node')) || // Check target_type for entity references to nodes - (fieldInfo.target_type === 'node' && + (fieldInfo?.target_type === 'node' && // Make sure it's NOT a taxonomy field !isTaxonomyField); @@ -257,31 +257,31 @@ export const analyzeFieldTypes = async ( // Try to determine the vocabulary from handler settings let vocabulary = 'unknown'; - if (fieldInfo.handler_settings?.target_bundles) { + if (fieldInfo?.handler_settings?.target_bundles) { const vocabularies = Object.keys( - fieldInfo.handler_settings.target_bundles + fieldInfo?.handler_settings?.target_bundles ); vocabulary = - vocabularies.length === 1 - ? vocabularies[0] - : vocabularies.join(','); + vocabularies?.length === 1 + ? vocabularies?.[0] + : vocabularies?.join(','); } // Use safe setter to prevent prototype pollution safeSetMapping( taxonomyFieldMapping, - fieldInfo.content_types, - fieldInfo.field_name, + fieldInfo?.content_types, + fieldInfo?.field_name, { vocabulary, - handler: fieldInfo.content_handler || 'default:taxonomy_term', - field_type: fieldInfo.field_type, + handler: fieldInfo?.content_handler || 'default:taxonomy_term', + field_type: fieldInfo?.field_type, } ); const taxonomyMessage = getLogMessage( srcFunc, - `Found taxonomy field: ${fieldInfo.content_types}.${fieldInfo.field_name} → vocabulary: ${vocabulary}`, + `Found taxonomy field: ${fieldInfo?.content_types}.${fieldInfo?.field_name} → vocabulary: ${vocabulary}`, {} ); await customLogger( @@ -296,20 +296,20 @@ export const analyzeFieldTypes = async ( // Use safe setter to prevent prototype pollution safeSetMapping( referenceFieldMapping, - fieldInfo.content_types, - fieldInfo.field_name, + fieldInfo?.content_types, + fieldInfo?.field_name, { - target_type: fieldInfo.target_type || 'node', - handler: fieldInfo.content_handler || 'default:node', - field_type: fieldInfo.field_type, + target_type: fieldInfo?.target_type || 'node', + handler: fieldInfo?.content_handler || 'default:node', + field_type: fieldInfo?.field_type, } ); const referenceMessage = getLogMessage( srcFunc, - `Found reference field: ${fieldInfo.content_types}.${ - fieldInfo.field_name - } → target_type: ${fieldInfo.target_type || 'node'}`, + `Found reference field: ${fieldInfo?.content_types}.${ + fieldInfo?.field_name + } → target_type: ${fieldInfo?.target_type || 'node'}`, {} ); await customLogger( @@ -323,21 +323,21 @@ export const analyzeFieldTypes = async ( // Check if this is an asset/file field const isAssetField = // Check for file field type - fieldInfo.field_type === 'file' || + fieldInfo?.field_type === 'file' || // Check for image field type - fieldInfo.field_type === 'image' || + fieldInfo?.field_type === 'image' || // Check for managed_file field type - fieldInfo.field_type === 'managed_file' || + fieldInfo?.field_type === 'managed_file' || // Check for entity_reference to file entities - (fieldInfo.field_type === 'entity_reference' && - fieldInfo.target_type === 'file'); + (fieldInfo?.field_type === 'entity_reference' && + fieldInfo?.target_type === 'file'); if (isAssetField) { assetFieldCount++; // Extract file-related settings const fileExtensions = fieldData?.settings?.file_extensions - ? fieldData.settings.file_extensions.split(' ') + ? fieldData?.settings?.file_extensions?.split(' ') : []; const uploadLocation = fieldData?.settings?.file_directory || @@ -351,10 +351,10 @@ export const analyzeFieldTypes = async ( // Use safe setter to prevent prototype pollution safeSetMapping( assetFieldMapping, - fieldInfo.content_types, - fieldInfo.field_name, + fieldInfo?.content_types, + fieldInfo?.field_name, { - field_type: fieldInfo.field_type, + field_type: fieldInfo?.field_type, file_extensions: fileExtensions, upload_location: uploadLocation, max_filesize: maxFilesize, @@ -363,11 +363,11 @@ export const analyzeFieldTypes = async ( const assetMessage = getLogMessage( srcFunc, - `Found asset field: ${fieldInfo.content_types}.${ - fieldInfo.field_name + `Found asset field: ${fieldInfo?.content_types}.${ + fieldInfo?.field_name } → type: ${ - fieldInfo.field_type - }, extensions: [${fileExtensions.join(', ')}]`, + fieldInfo?.field_type + }, extensions: [${fileExtensions?.join(', ')}]`, {} ); await customLogger( @@ -382,7 +382,7 @@ export const analyzeFieldTypes = async ( // Log parsing error but continue with other fields const parseMessage = getLogMessage( srcFunc, - `Could not parse field config: ${parseError.message}`, + `Could not parse field config: ${parseError?.message}`, {}, parseError ); @@ -410,7 +410,7 @@ export const analyzeFieldTypes = async ( } catch (error: any) { const message = getLogMessage( srcFunc, - `Error analyzing field types: ${error.message}`, + `Error analyzing field types: ${error?.message}`, {}, error ); @@ -432,7 +432,7 @@ export const isTaxonomyField = ( taxonomyMapping: TaxonomyFieldMapping ): boolean => { return !!( - taxonomyMapping[contentType] && taxonomyMapping[contentType][fieldName] + taxonomyMapping?.[contentType] && taxonomyMapping?.[contentType]?.[fieldName] ); }; @@ -445,7 +445,7 @@ export const isReferenceField = ( referenceMapping: ReferenceFieldMapping ): boolean => { return !!( - referenceMapping[contentType] && referenceMapping[contentType][fieldName] + referenceMapping?.[contentType] && referenceMapping?.[contentType]?.[fieldName] ); }; @@ -457,7 +457,7 @@ export const isAssetField = ( contentType: string, assetMapping: AssetFieldMapping ): boolean => { - return !!(assetMapping[contentType] && assetMapping[contentType][fieldName]); + return !!(assetMapping?.[contentType] && assetMapping?.[contentType]?.[fieldName]); }; /** @@ -468,7 +468,7 @@ export const getTaxonomyFieldInfo = ( contentType: string, taxonomyMapping: TaxonomyFieldMapping ) => { - return taxonomyMapping[contentType]?.[fieldName] || null; + return taxonomyMapping?.[contentType]?.[fieldName] || null; }; /** @@ -479,7 +479,7 @@ export const getReferenceFieldInfo = ( contentType: string, referenceMapping: ReferenceFieldMapping ) => { - return referenceMapping[contentType]?.[fieldName] || null; + return referenceMapping?.[contentType]?.[fieldName] || null; }; /** @@ -490,7 +490,7 @@ export const getAssetFieldInfo = ( contentType: string, assetMapping: AssetFieldMapping ) => { - return assetMapping[contentType]?.[fieldName] || null; + return assetMapping?.[contentType]?.[fieldName] || null; }; /** @@ -524,12 +524,12 @@ export const transformTaxonomyValue = async ( typeof value === 'number' || (typeof value === 'string' && /^\d+$/.test(value)) ) { - const tid = parseInt(value.toString()); + const tid = parseInt(value?.toString()); try { // Try to determine which vocabulary to look in based on field info - const vocabularies = fieldInfo.vocabulary - ? fieldInfo.vocabulary.split(',') + const vocabularies = fieldInfo?.vocabulary + ? fieldInfo?.vocabulary?.split(',') : ['unknown']; for (const vocabulary of vocabularies) { @@ -542,15 +542,15 @@ export const transformTaxonomyValue = async ( `${vocabulary}.json` ); - if (fs.existsSync(taxonomyFilePath)) { + if (fs?.existsSync(taxonomyFilePath)) { const taxonomyContent = JSON.parse( - fs.readFileSync(taxonomyFilePath, 'utf8') + fs?.readFileSync(taxonomyFilePath, 'utf8') ); - if (taxonomyContent.terms && Array.isArray(taxonomyContent.terms)) { - for (const term of taxonomyContent.terms) { - if (term.drupal_term_id === tid) { - return term.uid; + if (taxonomyContent?.terms && Array.isArray(taxonomyContent?.terms)) { + for (const term of taxonomyContent?.terms) { + if (term?.drupal_term_id === tid) { + return term?.uid; } } } @@ -565,23 +565,23 @@ export const transformTaxonomyValue = async ( const fs = await import('fs'); const path = await import('path'); - if (fs.existsSync(taxonomyBasePath)) { + if (fs?.existsSync(taxonomyBasePath)) { const taxonomyFiles = fs - .readdirSync(taxonomyBasePath) - .filter( - (file) => file.endsWith('.json') && file !== 'taxonomies.json' + ?.readdirSync(taxonomyBasePath) + ?.filter( + (file) => file?.endsWith('.json') && file !== 'taxonomies.json' ); for (const file of taxonomyFiles) { try { const taxonomyContent = JSON.parse( - fs.readFileSync(path.join(taxonomyBasePath, file), 'utf8') + fs?.readFileSync(path?.join(taxonomyBasePath, file), 'utf8') ); - if (taxonomyContent.terms && Array.isArray(taxonomyContent.terms)) { - for (const term of taxonomyContent.terms) { - if (term.drupal_term_id === tid) { - return term.uid; + if (taxonomyContent?.terms && Array.isArray(taxonomyContent?.terms)) { + for (const term of taxonomyContent?.terms) { + if (term?.drupal_term_id === tid) { + return term?.uid; } } } diff --git a/api/src/services/drupal/interface.ts b/api/src/services/drupal/interface.ts new file mode 100644 index 000000000..ef3b61ff3 --- /dev/null +++ b/api/src/services/drupal/interface.ts @@ -0,0 +1,12 @@ +export interface DbConfig { + host: string; + user: string; + password: string; + database: string; + port: number | string; +} + +export interface AssetsConfig { + base_url?: string; + public_path?: string; +} \ No newline at end of file diff --git a/api/src/services/migration.service.ts b/api/src/services/migration.service.ts index ea6249c9c..a913c9610 100644 --- a/api/src/services/migration.service.ts +++ b/api/src/services/migration.service.ts @@ -114,72 +114,72 @@ const createTestStack = async (req: Request): Promise => { .findIndex({ id: projectId }) .value(); if (index > -1) { - // ✅ Generate queries for new test stack (Drupal only) - const project = ProjectModelLowdb.data.projects[index]; - if (project?.legacy_cms?.cms === CMS.DRUPAL) { - try { - const startMessage = getLogMessage( - srcFun, - `Generating dynamic queries for new test stack (${res?.data?.stack?.api_key})...`, - token_payload - ); - await customLogger( - projectId, - res?.data?.stack?.api_key, - 'info', - startMessage - ); - - // Get database configuration from project - const legacyCms = project?.legacy_cms as unknown as Record< - string, - unknown - >; - const mySQLDetails = legacyCms?.mySQLDetails as - | Record - | undefined; - const dbConfig = { - host: mySQLDetails?.host as string | undefined, - user: mySQLDetails?.user as string | undefined, - password: (mySQLDetails?.password as string) || '', - database: mySQLDetails?.database as string | undefined, - port: (mySQLDetails?.port as number) || 3306, - }; - - // Generate dynamic queries for the new test stack - await drupalService.createQuery( - dbConfig, - res?.data?.stack?.api_key, - projectId - ); - - const successMessage = getLogMessage( - srcFun, - `Successfully generated queries for test stack (${res?.data?.stack?.api_key})`, - token_payload - ); - await customLogger( - projectId, - res?.data?.stack?.api_key, - 'info', - successMessage - ); - } catch (error: any) { - const errorMessage = getLogMessage( - srcFun, - `Failed to generate queries for test stack: ${error.message}. Test migration may fail.`, - token_payload, - error - ); - await customLogger( - projectId, - res?.data?.stack?.api_key, - 'error', - errorMessage - ); - // Don't throw error - let test stack creation succeed even if query generation fails - } - } + // // ✅ Generate queries for new test stack (Drupal only) + // const project = ProjectModelLowdb.data.projects[index]; + // if (project?.legacy_cms?.cms === CMS.DRUPAL) { + // try { + // const startMessage = getLogMessage( + // srcFun, + // `Generating dynamic queries for new test stack (${res?.data?.stack?.api_key})...`, + // token_payload + // ); + // await customLogger( + // projectId, + // res?.data?.stack?.api_key, + // 'info', + // startMessage + // ); + + // // Get database configuration from project + // const legacyCms = project?.legacy_cms as unknown as Record< + // string, + // unknown + // >; + // const mySQLDetails = legacyCms?.mySQLDetails as + // | Record + // | undefined; + // const dbConfig = { + // host: mySQLDetails?.host as string | undefined, + // user: mySQLDetails?.user as string | undefined, + // password: (mySQLDetails?.password as string) || '', + // database: mySQLDetails?.database as string | undefined, + // port: (mySQLDetails?.port as number) || 3306, + // }; + + // // Generate dynamic queries for the new test stack + // await drupalService.createQuery( + // dbConfig, + // res?.data?.stack?.api_key, + // projectId + // ); + + // const successMessage = getLogMessage( + // srcFun, + // `Successfully generated queries for test stack (${res?.data?.stack?.api_key})`, + // token_payload + // ); + // await customLogger( + // projectId, + // res?.data?.stack?.api_key, + // 'info', + // successMessage + // ); + // } catch (error: any) { + // const errorMessage = getLogMessage( + // srcFun, + // `Failed to generate queries for test stack: ${error.message}. Test migration may fail.`, + // token_payload, + // error + // ); + // await customLogger( + // projectId, + // res?.data?.stack?.api_key, + // 'error', + // errorMessage + // ); + // // Don't throw error - let test stack creation succeed even if query generation fails + // } + // } ProjectModelLowdb.update((data: any) => { data.projects[index].current_step = STEPPER_STEPS['TESTING']; @@ -433,6 +433,7 @@ const startTestMigration = async (req: Request): Promise => { region, user_id, }); + await marketPlaceAppService?.createAppManifest({ orgId, destinationStackId: project?.current_test_stack_id, @@ -599,11 +600,6 @@ const startTestMigration = async (req: Request): Promise => { projectId ); - // Step 2: Generate content type schemas from upload-api (CRITICAL: Must run after upload-api generates schema) - await drupalService?.generateContentTypeSchemas( - project?.current_test_stack_id, - projectId - ); // Step 3: Create assets from Drupal database await drupalService?.createAssets( @@ -636,8 +632,8 @@ const startTestMigration = async (req: Request): Promise => { projectId, true, project?.stackDetails?.master_locale, - project?.content_mapper || [], - project + project, + contentTypes ); // Step 7: Create locale @@ -998,11 +994,11 @@ const startMigration = async (req: Request): Promise => { projectId ); - // Step 2: Generate content type schemas from upload-api - await drupalService?.generateContentTypeSchemas( - project?.destination_stack_id, - projectId - ); + // // Step 2: Generate content type schemas from upload-api + // await drupalService?.generateContentTypeSchemas( + // project?.destination_stack_id, + // projectId + // ); // Step 3: Create assets from Drupal database await drupalService?.createAssets( @@ -1035,8 +1031,8 @@ const startMigration = async (req: Request): Promise => { projectId, false, // Not a test migration project?.stackDetails?.master_locale, - project?.content_mapper || [], - project + project, + contentTypes ); // Step 7: Create locale From a3620209815a25939ede856126c2c0fc8a3a51fd Mon Sep 17 00:00:00 2001 From: AishDani Date: Mon, 16 Mar 2026 17:16:09 +0530 Subject: [PATCH 2/3] refactor:removed the commented code --- api/src/services/migration.service.ts | 73 +-------------------------- 1 file changed, 1 insertion(+), 72 deletions(-) diff --git a/api/src/services/migration.service.ts b/api/src/services/migration.service.ts index a913c9610..0ef0f3ef6 100644 --- a/api/src/services/migration.service.ts +++ b/api/src/services/migration.service.ts @@ -114,72 +114,7 @@ const createTestStack = async (req: Request): Promise => { .findIndex({ id: projectId }) .value(); if (index > -1) { - // // ✅ Generate queries for new test stack (Drupal only) - // const project = ProjectModelLowdb.data.projects[index]; - // if (project?.legacy_cms?.cms === CMS.DRUPAL) { - // try { - // const startMessage = getLogMessage( - // srcFun, - // `Generating dynamic queries for new test stack (${res?.data?.stack?.api_key})...`, - // token_payload - // ); - // await customLogger( - // projectId, - // res?.data?.stack?.api_key, - // 'info', - // startMessage - // ); - - // // Get database configuration from project - // const legacyCms = project?.legacy_cms as unknown as Record< - // string, - // unknown - // >; - // const mySQLDetails = legacyCms?.mySQLDetails as - // | Record - // | undefined; - // const dbConfig = { - // host: mySQLDetails?.host as string | undefined, - // user: mySQLDetails?.user as string | undefined, - // password: (mySQLDetails?.password as string) || '', - // database: mySQLDetails?.database as string | undefined, - // port: (mySQLDetails?.port as number) || 3306, - // }; - - // // Generate dynamic queries for the new test stack - // await drupalService.createQuery( - // dbConfig, - // res?.data?.stack?.api_key, - // projectId - // ); - - // const successMessage = getLogMessage( - // srcFun, - // `Successfully generated queries for test stack (${res?.data?.stack?.api_key})`, - // token_payload - // ); - // await customLogger( - // projectId, - // res?.data?.stack?.api_key, - // 'info', - // successMessage - // ); - // } catch (error: any) { - // const errorMessage = getLogMessage( - // srcFun, - // `Failed to generate queries for test stack: ${error.message}. Test migration may fail.`, - // token_payload, - // error - // ); - // await customLogger( - // projectId, - // res?.data?.stack?.api_key, - // 'error', - // errorMessage - // ); - // // Don't throw error - let test stack creation succeed even if query generation fails - // } - // } + ProjectModelLowdb.update((data: any) => { data.projects[index].current_step = STEPPER_STEPS['TESTING']; @@ -994,12 +929,6 @@ const startMigration = async (req: Request): Promise => { projectId ); - // // Step 2: Generate content type schemas from upload-api - // await drupalService?.generateContentTypeSchemas( - // project?.destination_stack_id, - // projectId - // ); - // Step 3: Create assets from Drupal database await drupalService?.createAssets( dbConfig, From 9073485b6770b0a4d0e1d16091220bffbc01c8b3 Mon Sep 17 00:00:00 2001 From: AishDani Date: Wed, 18 Mar 2026 13:54:06 +0530 Subject: [PATCH 3/3] refactor:added optional chaining as per PR comments --- api/src/services/drupal/assets.service.ts | 4 ++-- api/src/services/drupal/entries.service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/services/drupal/assets.service.ts b/api/src/services/drupal/assets.service.ts index ef436357b..6a458e43b 100644 --- a/api/src/services/drupal/assets.service.ts +++ b/api/src/services/drupal/assets.service.ts @@ -429,9 +429,9 @@ const saveAsset = async ( 'files' ); - const safeFid = String(assets.fid).replace(/[^a-zA-Z0-9_-]/g, ''); + const safeFid = String(assets?.fid)?.replace(/[^a-zA-Z0-9_-]/g, ''); if (!safeFid) { - throw new Error(`Asset has an invalid fid: ${assets.fid}`); + throw new Error(`Asset has an invalid fid: ${assets?.fid}`); } const assetId = `assets_${safeFid}`; const fileName = path.basename(assets?.filename || ''); diff --git a/api/src/services/drupal/entries.service.ts b/api/src/services/drupal/entries.service.ts index b19466514..e9b8cae7e 100644 --- a/api/src/services/drupal/entries.service.ts +++ b/api/src/services/drupal/entries.service.ts @@ -961,7 +961,7 @@ const processFieldData = async ( const isUriField = key.endsWith('_uri'); if (isValueField) { - const baseFieldName = key.replace('_value', ''); + const baseFieldName = key?.replace('_value', ''); // Only include the _value field if the base field doesn't exist if (!mergedData?.hasOwnProperty(baseFieldName)) { cleanedEntry[key] = val;