From 5925b9468316b7eedd71ee735cbb497ab4b4f79f Mon Sep 17 00:00:00 2001 From: raj pandey Date: Mon, 27 Apr 2026 11:05:58 +0530 Subject: [PATCH] enhc: Added taxonomy publish details for all taxonomy --- .../src/export/modules/taxonomies.ts | 74 +++++++++- .../unit/export/modules/taxonomies.test.ts | 132 ++++++++++++++++++ 2 files changed, 205 insertions(+), 1 deletion(-) diff --git a/packages/contentstack-export/src/export/modules/taxonomies.ts b/packages/contentstack-export/src/export/modules/taxonomies.ts index 27be33d4..fc50966f 100644 --- a/packages/contentstack-export/src/export/modules/taxonomies.ts +++ b/packages/contentstack-export/src/export/modules/taxonomies.ts @@ -1,3 +1,4 @@ +import cloneDeep from 'lodash/cloneDeep'; import omit from 'lodash/omit'; import keys from 'lodash/keys'; import isEmpty from 'lodash/isEmpty'; @@ -16,8 +17,12 @@ import { import { ModuleClassParams, ExportConfig } from '../../types'; export default class ExportTaxonomies extends BaseClass { + private static readonly PUBLISH_DETAILS_DEFAULT_LOCALE = '_default'; + private taxonomies: Record>; private taxonomiesByLocale: Record>; + /** List API `publish_details` keyed by non-localized bucket or locale code, then taxonomy uid */ + private publishDetailsByLocale: Record>; private taxonomiesConfig: ExportConfig['modules']['taxonomies']; private isLocaleBasedExportSupported: boolean = true; // Flag to track if locale-based export is supported private qs: { @@ -39,6 +44,7 @@ export default class ExportTaxonomies extends BaseClass { super({ exportConfig, stackAPIClient }); this.taxonomies = {}; this.taxonomiesByLocale = {}; + this.publishDetailsByLocale = {}; this.taxonomiesConfig = exportConfig.modules.taxonomies; this.qs = { include_count: true, @@ -143,6 +149,7 @@ export default class ExportTaxonomies extends BaseClass { log.debug('Falling back to legacy export (non-localized)', this.exportConfig.context); this.taxonomies = {}; this.taxonomiesByLocale = {}; + this.publishDetailsByLocale = {}; } else { log.debug('Localization enabled, proceeding with locale-based export', this.exportConfig.context); } @@ -330,11 +337,21 @@ export default class ExportTaxonomies extends BaseClass { log.debug(`Processing ${taxonomies.length} taxonomies${localeInfo}`, this.exportConfig.context); for (const taxonomy of taxonomies) { + const taxonomyRow = taxonomy as Record; const taxonomyUID = taxonomy.uid; const taxonomyName = taxonomy.name; log.debug(`Processing taxonomy: ${taxonomyName} (${taxonomyUID})${localeInfo}`, this.exportConfig.context); + // Store list API publish_details for merge into per-uid export files (per locale or default bucket) + if (taxonomyRow.publish_details != null) { + const bucket = localeCode ?? ExportTaxonomies.PUBLISH_DETAILS_DEFAULT_LOCALE; + if (!this.publishDetailsByLocale[bucket]) { + this.publishDetailsByLocale[bucket] = {}; + } + this.publishDetailsByLocale[bucket][taxonomyUID] = taxonomyRow.publish_details; + } + // Store taxonomy metadata (only once per taxonomy) if (!this.taxonomies[taxonomyUID]) { this.taxonomies[taxonomyUID] = omit(taxonomy, this.taxonomiesConfig.invalidKeys); @@ -374,8 +391,9 @@ export default class ExportTaxonomies extends BaseClass { const onSuccess = ({ response, uid }: any) => { const taxonomyName = this.taxonomies[uid]?.name; const filePath = pResolve(exportFolderPath, `${uid}.json`); + const merged = this.mergeListPublishDetailsIntoExportPayload(response, uid, localeCode); log.debug(`Writing detailed taxonomy data to: ${filePath}`, this.exportConfig.context); - fsUtil.writeFile(filePath, response); + fsUtil.writeFile(filePath, merged); // Track progress for each exported taxonomy this.progressManager?.tick( @@ -463,6 +481,60 @@ export default class ExportTaxonomies extends BaseClass { return localesToExport; } + /** + * List `find` may include `publish_details` while `export` may not; we copy list data into the + * written file when export omits or has an empty `taxonomy.publish_details`. + */ + private getListPublishDetailsForExport(taxonomyUid: string, localeCode?: string): unknown | undefined { + const bucket = localeCode ?? ExportTaxonomies.PUBLISH_DETAILS_DEFAULT_LOCALE; + return this.publishDetailsByLocale[bucket]?.[taxonomyUid]; + } + + private isPublishDetailsValueEmpty(publishDetails: unknown): boolean { + if (publishDetails == null) { + return true; + } + if (Array.isArray(publishDetails)) { + return publishDetails.length === 0; + } + if (typeof publishDetails === 'object') { + return Object.keys(publishDetails as object).length === 0; + } + return false; + } + + private mergeListPublishDetailsIntoExportPayload( + response: any, + taxonomyUid: string, + localeCode?: string, + ): any { + const fromList = this.getListPublishDetailsForExport(taxonomyUid, localeCode); + if (fromList == null) { + return response; + } + + const merged = cloneDeep(response); + const applyToTaxonomyObject = (tax: Record | undefined | null) => { + if (!tax || typeof tax !== 'object') { + return; + } + if (this.isPublishDetailsValueEmpty(tax.publish_details)) { + tax.publish_details = fromList; + } + }; + + if (merged && typeof merged === 'object' && 'taxonomy' in merged && (merged as any).taxonomy) { + applyToTaxonomyObject((merged as any).taxonomy); + return merged; + } + + log.debug( + 'Taxonomy export response has no taxonomy object; skipping publish_details merge from list', + this.exportConfig.context, + ); + return merged; + } + private isLocalePlanLimitationError(error: any): boolean { return ( error?.status === 403 && diff --git a/packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts b/packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts index 276e0281..b946d464 100644 --- a/packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts @@ -826,4 +826,136 @@ describe('ExportTaxonomies', () => { mockFetchTaxonomies.restore(); }); }); + + describe('Detail file: merge list publish_details into export payload', () => { + it('should write merged file with list publish_details when export response omits them (legacy)', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const listPublish = [{ environment: 'bltEnv1', locale: 'en-us' }]; + + exportTaxonomies.taxonomies = { 'tax-1': { uid: 'tax-1', name: 'T' } }; + exportTaxonomies.publishDetailsByLocale = { + _default: { 'tax-1': listPublish }, + } as any; + exportTaxonomies.taxonomiesFolderPath = '/test/export/taxonomies'; + + const makeAPICallStub = sinon.stub(exportTaxonomies, 'makeAPICall').callsFake((opts: any) => { + return Promise.resolve( + opts.resolve({ + response: { taxonomy: { uid: 'tax-1', name: 'T' }, terms: {} }, + uid: 'tax-1', + }), + ); + }); + + await exportTaxonomies.exportTaxonomies(); + + const detailWrite = writeFileStub + .getCalls() + .find((c) => typeof c.args[0] === 'string' && c.args[0].endsWith('tax-1.json')); + expect(detailWrite, 'writeFile for tax-1.json').to.exist; + const payload = detailWrite!.args[1]; + expect(payload.taxonomy.publish_details).to.deep.equal(listPublish); + + makeAPICallStub.restore(); + }); + + it('should prefer export taxonomy.publish_details when already present and non-empty', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const fromExport = [{ environment: 'from-export' }]; + + exportTaxonomies.taxonomies = { 'tax-1': { uid: 'tax-1' } }; + exportTaxonomies.publishDetailsByLocale = { + _default: { 'tax-1': [{ from: 'list' }] }, + } as any; + exportTaxonomies.taxonomiesFolderPath = '/test/export/taxonomies'; + + const makeAPICallStub = sinon.stub(exportTaxonomies, 'makeAPICall').callsFake((opts: any) => { + return Promise.resolve( + opts.resolve({ + response: { taxonomy: { uid: 'tax-1', publish_details: fromExport }, terms: {} }, + uid: 'tax-1', + }), + ); + }); + + await exportTaxonomies.exportTaxonomies(); + + const detailWrite = writeFileStub + .getCalls() + .find((c) => typeof c.args[0] === 'string' && c.args[0].endsWith('tax-1.json')); + const payload = detailWrite!.args[1]; + expect(payload.taxonomy.publish_details).to.deep.equal(fromExport); + + makeAPICallStub.restore(); + }); + + it('should fill from list when export has empty array publish_details', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const fromList = [{ from: 'list' }]; + + exportTaxonomies.taxonomies = { 'tax-1': { uid: 'tax-1' } }; + exportTaxonomies.publishDetailsByLocale = { _default: { 'tax-1': fromList } } as any; + exportTaxonomies.taxonomiesFolderPath = '/test/export/taxonomies'; + + const makeAPICallStub = sinon.stub(exportTaxonomies, 'makeAPICall').callsFake((opts: any) => { + return Promise.resolve( + opts.resolve({ + response: { taxonomy: { uid: 'tax-1', publish_details: [] }, terms: {} }, + uid: 'tax-1', + }), + ); + }); + + await exportTaxonomies.exportTaxonomies(); + + const detailWrite = writeFileStub + .getCalls() + .find((c) => typeof c.args[0] === 'string' && c.args[0].endsWith('tax-1.json')); + expect(detailWrite!.args[1].taxonomy.publish_details).to.deep.equal(fromList); + + makeAPICallStub.restore(); + }); + + it('should use per-locale list publish_details when exporting locale folder', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const frPublish = [{ locale: 'fr-fr' }]; + + exportTaxonomies.taxonomies = { 'tax-1': { uid: 'tax-1' } }; + exportTaxonomies.taxonomiesByLocale['fr-fr'] = new Set(['tax-1']); + exportTaxonomies.publishDetailsByLocale = { 'fr-fr': { 'tax-1': frPublish } } as any; + exportTaxonomies.taxonomiesFolderPath = '/test/export/taxonomies'; + + const makeAPICallStub = sinon.stub(exportTaxonomies, 'makeAPICall').callsFake((opts: any) => { + return Promise.resolve( + opts.resolve({ + response: { taxonomy: { uid: 'tax-1' }, terms: {} }, + uid: 'tax-1', + }), + ); + }); + + await exportTaxonomies.exportTaxonomies('fr-fr'); + + const detailWrite = writeFileStub + .getCalls() + .find((c) => typeof c.args[0] === 'string' && c.args[0].includes('fr-fr') && c.args[0].endsWith('tax-1.json')); + expect(detailWrite, 'fr-fr/tax-1.json').to.exist; + expect(detailWrite!.args[1].taxonomy.publish_details).to.deep.equal(frPublish); + + makeAPICallStub.restore(); + }); + + it('should store publish_details per locale bucket in sanitizeTaxonomiesAttribs', () => { + const listPd = [{ env: 'a' }]; + const taxonomies = [ + { uid: 't-loc', name: 'L', publish_details: listPd }, + ]; + exportTaxonomies.publishDetailsByLocale = {}; + exportTaxonomies.taxonomiesByLocale['es-es'] = new Set(); + + exportTaxonomies.sanitizeTaxonomiesAttribs(taxonomies, 'es-es'); + + expect((exportTaxonomies as any).publishDetailsByLocale['es-es']['t-loc']).to.deep.equal(listPd); + }); + }); });