diff --git a/packages/contentstack-import/src/import/modules/taxonomies.ts b/packages/contentstack-import/src/import/modules/taxonomies.ts index 3a0ad7596..ae8443e5c 100644 --- a/packages/contentstack-import/src/import/modules/taxonomies.ts +++ b/packages/contentstack-import/src/import/modules/taxonomies.ts @@ -84,11 +84,35 @@ export default class ImportTaxonomies extends BaseClass { log.debug('Using legacy folder structure for taxonomies', this.importConfig.context); } - //Step 5 create taxonomy & related terms success & failure file + // Step 5: Flag taxonomies that were never processed (no matching export data + // found in any locale/legacy path), so they don't silently disappear. + for (const taxonomyUID of Object.keys(this.taxonomies || {})) { + if (!(taxonomyUID in this.createdTaxonomies) && !(taxonomyUID in this.failedTaxonomies)) { + log.error( + `Taxonomy '${taxonomyUID}' could not be imported: no matching export data found`, + this.importConfig.context, + ); + this.failedTaxonomies[taxonomyUID] = this.taxonomies[taxonomyUID]; + } + } + + //Step 6 create taxonomy & related terms success & failure file log.debug('Creating success and failure files...', this.importConfig.context); this.createSuccessAndFailedFile(); - log.success('Taxonomies imported successfully!', this.importConfig.context); + const createdCount = Object.keys(this.createdTaxonomies).length; + const failedCount = Object.keys(this.failedTaxonomies).length; + + if (failedCount > 0) { + log.error( + `Taxonomies import completed with errors: ${createdCount} succeeded, ${failedCount} failed`, + this.importConfig.context, + ); + } else if (createdCount > 0) { + log.success('Taxonomies imported successfully!', this.importConfig.context); + } else { + log.info('No taxonomies to import.', this.importConfig.context); + } } /** @@ -367,13 +391,22 @@ export default class ImportTaxonomies extends BaseClass { const masterLocaleFolder = join(this.taxonomiesFolderPath, masterLocaleCode); // Check if master locale folder exists (indicates new locale-based structure) - if (!fileHelper.fileExistsSync(masterLocaleFolder)) { - log.debug('No locale-based folder structure detected', this.importConfig.context); - return false; + if (fileHelper.fileExistsSync(masterLocaleFolder)) { + log.debug('Locale-based folder structure detected', this.importConfig.context); + return true; } - log.debug('Locale-based folder structure detected', this.importConfig.context); + // The master locale may not have any localized taxonomies (so its folder was + // never exported), but other locales can still use the locale-based structure. + const locales = this.loadAvailableLocales(); + for (const localeCode of Object.keys(locales)) { + if (fileHelper.fileExistsSync(join(this.taxonomiesFolderPath, localeCode))) { + log.debug('Locale-based folder structure detected', this.importConfig.context); + return true; + } + } - return true; + log.debug('No locale-based folder structure detected', this.importConfig.context); + return false; } } diff --git a/packages/contentstack-migration/examples/change-master-locale/02-change-master-locale-new-file-structure.js b/packages/contentstack-migration/examples/change-master-locale/02-change-master-locale-new-file-structure.js index f4a2255d0..de98e16fa 100644 --- a/packages/contentstack-migration/examples/change-master-locale/02-change-master-locale-new-file-structure.js +++ b/packages/contentstack-migration/examples/change-master-locale/02-change-master-locale-new-file-structure.js @@ -25,7 +25,10 @@ module.exports = async ({ migration, config }) => { } async function tailorData() { - let locales = await fs.readFile(pathValidator(path.resolve(sanitizePath(config.data_dir), 'locales/locales.json')), 'utf-8'); + let locales = await fs.readFile( + pathValidator(path.resolve(sanitizePath(config.data_dir), 'locales/locales.json')), + 'utf-8', + ); let masterLocale = await fs.readFile( pathValidator(path.resolve(sanitizePath(config.data_dir), 'locales/master-locale.json')), 'utf-8', @@ -34,12 +37,14 @@ module.exports = async ({ migration, config }) => { if (masterLocale) { masterLocale = JSON.parse(masterLocale); masterLocale = Object.values(masterLocale); - masterLocale = masterLocale[0] - + masterLocale = masterLocale[0]; + // Validate that we have a valid master locale code - if (!masterLocale) { + if (!masterLocale || !masterLocale.code) { throw new Error('Unable to determine master locale code from master-locale.json'); } + + masterLocale = masterLocale.code; } locales = JSON.parse(locales); let id = crypto.randomBytes(8).toString('hex'); @@ -60,6 +65,7 @@ module.exports = async ({ migration, config }) => { locales[id].fallback_locale = config.target_locale; await handleEntries(masterLocale); + await handleTaxonomies(masterLocale); await fs.writeFile( pathValidator(path.resolve(sanitizePath(config.data_dir), 'locales/locales.json')), JSON.stringify(locales), @@ -84,21 +90,23 @@ module.exports = async ({ migration, config }) => { let sourceMasterLocaleEntries, targetMasterLocaleEntries; // Check if index.json exists (if no entries, index.json won't be created) - const indexFilePath = pathValidator(path.resolve(sanitizePath(config.data_dir), sanitizePath(`entries/${contentType}/${masterLocale}/index.json`))); + const indexFilePath = pathValidator( + path.resolve( + sanitizePath(config.data_dir), + sanitizePath(`entries/${contentType}/${masterLocale}/index.json`), + ), + ); if (!existsSync(indexFilePath)) { console.log(`Skipping ${contentType} - no index.json found (likely no entries)`); continue; } - sourceMasterLocaleEntries = await fs.readFile( - indexFilePath, - { encoding: 'utf8' }, - ); + sourceMasterLocaleEntries = await fs.readFile(indexFilePath, { encoding: 'utf8' }); // Parse the index.json to get the entries file name const indexData = JSON.parse(sourceMasterLocaleEntries); const entriesFileName = Object.values(indexData)[0]; - + // Check if we have a valid entries file name if (!entriesFileName) { console.log(`Skipping ${contentType} - no entries file found in index.json`); @@ -112,10 +120,7 @@ module.exports = async ({ migration, config }) => { ), ); - sourceMasterLocaleEntries = await fs.readFile( - entriesFilePath, - { encoding: 'utf8' }, - ); + sourceMasterLocaleEntries = await fs.readFile(entriesFilePath, { encoding: 'utf8' }); sourceMasterLocaleEntries = JSON.parse(sourceMasterLocaleEntries); if ( existsSync(pathValidator(path.resolve(config.data_dir, `entries/${contentType}/${config.target_locale}`))) @@ -127,7 +132,7 @@ module.exports = async ({ migration, config }) => { if (targetMasterLocaleEntries) { const targetIndexData = JSON.parse(targetMasterLocaleEntries); const targetEntriesFileName = Object.values(targetIndexData)[0]; - + if (targetEntriesFileName) { targetMasterLocaleEntries = await fs.readFile( pathValidator( @@ -152,7 +157,7 @@ module.exports = async ({ migration, config }) => { Object.keys(sourceMasterLocaleEntries).forEach((uid) => { if (!targetMasterLocaleEntries[uid]) { targetMasterLocaleEntries[uid] = JSON.parse(JSON.stringify(sourceMasterLocaleEntries[uid])); - delete targetMasterLocaleEntries[uid]['publish_details']; + targetMasterLocaleEntries[uid]['publish_details'] = []; targetMasterLocaleEntries[uid].locale = config.target_locale; } }); @@ -164,10 +169,10 @@ module.exports = async ({ migration, config }) => { pathValidator(path.resolve(config.data_dir, `entries/${contentType}/${config.target_locale}/index.json`)), { encoding: 'utf8', flag: 'a+' }, ); - + const existingIndexData = JSON.parse(exsitingTargetMasterLocalEntries); const existingEntriesFileName = Object.values(existingIndexData)[0]; - + if (existingEntriesFileName) { await fs.writeFile( pathValidator( @@ -194,6 +199,88 @@ module.exports = async ({ migration, config }) => { } } + async function handleTaxonomies(masterLocale) { + const taxonomiesDirPath = pathValidator(path.resolve(sanitizePath(config.data_dir), 'taxonomies')); + const taxonomiesIndexPath = pathValidator(path.resolve(taxonomiesDirPath, 'taxonomies.json')); + + if (!existsSync(taxonomiesIndexPath)) { + console.log('Skipping taxonomies - no taxonomies.json found'); + return; + } + + let taxonomiesIndex = await fs.readFile(taxonomiesIndexPath, { encoding: 'utf8' }); + taxonomiesIndex = JSON.parse(taxonomiesIndex); + + const targetLocaleDirPath = pathValidator(path.resolve(taxonomiesDirPath, sanitizePath(config.target_locale))); + + for (const taxonomyUid of Object.keys(taxonomiesIndex)) { + const fileName = `${sanitizePath(taxonomyUid)}.json`; + const targetFilePath = pathValidator(path.resolve(targetLocaleDirPath, fileName)); + + // Prefer the old master locale's taxonomy data, then the locale recorded at export time, + // then fall back to any other locale that has it + const exportedLocale = taxonomiesIndex[taxonomyUid]?.locale; + let sourceFilePath; + for (const localeCode of [masterLocale, exportedLocale]) { + if (!localeCode) { + continue; + } + const candidatePath = pathValidator(path.resolve(taxonomiesDirPath, sanitizePath(localeCode), fileName)); + if (existsSync(candidatePath)) { + sourceFilePath = candidatePath; + break; + } + } + + if (!sourceFilePath) { + const localeEntries = await fs.readdir(taxonomiesDirPath, { withFileTypes: true }); + for (const localeEntry of localeEntries) { + if (!localeEntry.isDirectory() || localeEntry.name === config.target_locale) { + continue; + } + const candidatePath = pathValidator( + path.resolve(taxonomiesDirPath, sanitizePath(localeEntry.name), fileName), + ); + if (existsSync(candidatePath)) { + sourceFilePath = candidatePath; + break; + } + } + } + + if (!sourceFilePath) { + console.log(`Skipping taxonomy '${taxonomyUid}' - no source locale data found`); + continue; + } + + let sourceTaxonomy = await fs.readFile(sourceFilePath, { encoding: 'utf8' }); + sourceTaxonomy = JSON.parse(sourceTaxonomy); + + if (existsSync(targetFilePath)) { + let targetTaxonomy = await fs.readFile(targetFilePath, { encoding: 'utf8' }); + targetTaxonomy = JSON.parse(targetTaxonomy); + targetTaxonomy.terms = targetTaxonomy.terms || []; + + const existingTermUids = new Set(targetTaxonomy.terms.map((term) => term.uid)); + for (const term of sourceTaxonomy.terms || []) { + if (!existingTermUids.has(term.uid)) { + targetTaxonomy.terms.push(JSON.parse(JSON.stringify(term))); + } + } + + await fs.writeFile(targetFilePath, JSON.stringify(targetTaxonomy)); + } else { + await fs.mkdir(targetLocaleDirPath, { recursive: true }); + + const targetTaxonomy = JSON.parse(JSON.stringify(sourceTaxonomy)); + targetTaxonomy.taxonomy = targetTaxonomy.taxonomy || {}; + targetTaxonomy.taxonomy.locale = config.target_locale; + + await fs.writeFile(targetFilePath, JSON.stringify(targetTaxonomy)); + } + } + } + await tailorData(); }, };