Skip to content

Commit 5925b94

Browse files
committed
enhc: Added taxonomy publish details for all taxonomy
1 parent de3413c commit 5925b94

2 files changed

Lines changed: 205 additions & 1 deletion

File tree

packages/contentstack-export/src/export/modules/taxonomies.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import cloneDeep from 'lodash/cloneDeep';
12
import omit from 'lodash/omit';
23
import keys from 'lodash/keys';
34
import isEmpty from 'lodash/isEmpty';
@@ -16,8 +17,12 @@ import {
1617
import { ModuleClassParams, ExportConfig } from '../../types';
1718

1819
export default class ExportTaxonomies extends BaseClass {
20+
private static readonly PUBLISH_DETAILS_DEFAULT_LOCALE = '_default';
21+
1922
private taxonomies: Record<string, Record<string, string>>;
2023
private taxonomiesByLocale: Record<string, Set<string>>;
24+
/** List API `publish_details` keyed by non-localized bucket or locale code, then taxonomy uid */
25+
private publishDetailsByLocale: Record<string, Record<string, unknown>>;
2126
private taxonomiesConfig: ExportConfig['modules']['taxonomies'];
2227
private isLocaleBasedExportSupported: boolean = true; // Flag to track if locale-based export is supported
2328
private qs: {
@@ -39,6 +44,7 @@ export default class ExportTaxonomies extends BaseClass {
3944
super({ exportConfig, stackAPIClient });
4045
this.taxonomies = {};
4146
this.taxonomiesByLocale = {};
47+
this.publishDetailsByLocale = {};
4248
this.taxonomiesConfig = exportConfig.modules.taxonomies;
4349
this.qs = {
4450
include_count: true,
@@ -143,6 +149,7 @@ export default class ExportTaxonomies extends BaseClass {
143149
log.debug('Falling back to legacy export (non-localized)', this.exportConfig.context);
144150
this.taxonomies = {};
145151
this.taxonomiesByLocale = {};
152+
this.publishDetailsByLocale = {};
146153
} else {
147154
log.debug('Localization enabled, proceeding with locale-based export', this.exportConfig.context);
148155
}
@@ -330,11 +337,21 @@ export default class ExportTaxonomies extends BaseClass {
330337
log.debug(`Processing ${taxonomies.length} taxonomies${localeInfo}`, this.exportConfig.context);
331338

332339
for (const taxonomy of taxonomies) {
340+
const taxonomyRow = taxonomy as Record<string, unknown>;
333341
const taxonomyUID = taxonomy.uid;
334342
const taxonomyName = taxonomy.name;
335343

336344
log.debug(`Processing taxonomy: ${taxonomyName} (${taxonomyUID})${localeInfo}`, this.exportConfig.context);
337345

346+
// Store list API publish_details for merge into per-uid export files (per locale or default bucket)
347+
if (taxonomyRow.publish_details != null) {
348+
const bucket = localeCode ?? ExportTaxonomies.PUBLISH_DETAILS_DEFAULT_LOCALE;
349+
if (!this.publishDetailsByLocale[bucket]) {
350+
this.publishDetailsByLocale[bucket] = {};
351+
}
352+
this.publishDetailsByLocale[bucket][taxonomyUID] = taxonomyRow.publish_details;
353+
}
354+
338355
// Store taxonomy metadata (only once per taxonomy)
339356
if (!this.taxonomies[taxonomyUID]) {
340357
this.taxonomies[taxonomyUID] = omit(taxonomy, this.taxonomiesConfig.invalidKeys);
@@ -374,8 +391,9 @@ export default class ExportTaxonomies extends BaseClass {
374391
const onSuccess = ({ response, uid }: any) => {
375392
const taxonomyName = this.taxonomies[uid]?.name;
376393
const filePath = pResolve(exportFolderPath, `${uid}.json`);
394+
const merged = this.mergeListPublishDetailsIntoExportPayload(response, uid, localeCode);
377395
log.debug(`Writing detailed taxonomy data to: ${filePath}`, this.exportConfig.context);
378-
fsUtil.writeFile(filePath, response);
396+
fsUtil.writeFile(filePath, merged);
379397

380398
// Track progress for each exported taxonomy
381399
this.progressManager?.tick(
@@ -463,6 +481,60 @@ export default class ExportTaxonomies extends BaseClass {
463481
return localesToExport;
464482
}
465483

484+
/**
485+
* List `find` may include `publish_details` while `export` may not; we copy list data into the
486+
* written file when export omits or has an empty `taxonomy.publish_details`.
487+
*/
488+
private getListPublishDetailsForExport(taxonomyUid: string, localeCode?: string): unknown | undefined {
489+
const bucket = localeCode ?? ExportTaxonomies.PUBLISH_DETAILS_DEFAULT_LOCALE;
490+
return this.publishDetailsByLocale[bucket]?.[taxonomyUid];
491+
}
492+
493+
private isPublishDetailsValueEmpty(publishDetails: unknown): boolean {
494+
if (publishDetails == null) {
495+
return true;
496+
}
497+
if (Array.isArray(publishDetails)) {
498+
return publishDetails.length === 0;
499+
}
500+
if (typeof publishDetails === 'object') {
501+
return Object.keys(publishDetails as object).length === 0;
502+
}
503+
return false;
504+
}
505+
506+
private mergeListPublishDetailsIntoExportPayload(
507+
response: any,
508+
taxonomyUid: string,
509+
localeCode?: string,
510+
): any {
511+
const fromList = this.getListPublishDetailsForExport(taxonomyUid, localeCode);
512+
if (fromList == null) {
513+
return response;
514+
}
515+
516+
const merged = cloneDeep(response);
517+
const applyToTaxonomyObject = (tax: Record<string, unknown> | undefined | null) => {
518+
if (!tax || typeof tax !== 'object') {
519+
return;
520+
}
521+
if (this.isPublishDetailsValueEmpty(tax.publish_details)) {
522+
tax.publish_details = fromList;
523+
}
524+
};
525+
526+
if (merged && typeof merged === 'object' && 'taxonomy' in merged && (merged as any).taxonomy) {
527+
applyToTaxonomyObject((merged as any).taxonomy);
528+
return merged;
529+
}
530+
531+
log.debug(
532+
'Taxonomy export response has no taxonomy object; skipping publish_details merge from list',
533+
this.exportConfig.context,
534+
);
535+
return merged;
536+
}
537+
466538
private isLocalePlanLimitationError(error: any): boolean {
467539
return (
468540
error?.status === 403 &&

packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,4 +826,136 @@ describe('ExportTaxonomies', () => {
826826
mockFetchTaxonomies.restore();
827827
});
828828
});
829+
830+
describe('Detail file: merge list publish_details into export payload', () => {
831+
it('should write merged file with list publish_details when export response omits them (legacy)', async () => {
832+
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
833+
const listPublish = [{ environment: 'bltEnv1', locale: 'en-us' }];
834+
835+
exportTaxonomies.taxonomies = { 'tax-1': { uid: 'tax-1', name: 'T' } };
836+
exportTaxonomies.publishDetailsByLocale = {
837+
_default: { 'tax-1': listPublish },
838+
} as any;
839+
exportTaxonomies.taxonomiesFolderPath = '/test/export/taxonomies';
840+
841+
const makeAPICallStub = sinon.stub(exportTaxonomies, 'makeAPICall').callsFake((opts: any) => {
842+
return Promise.resolve(
843+
opts.resolve({
844+
response: { taxonomy: { uid: 'tax-1', name: 'T' }, terms: {} },
845+
uid: 'tax-1',
846+
}),
847+
);
848+
});
849+
850+
await exportTaxonomies.exportTaxonomies();
851+
852+
const detailWrite = writeFileStub
853+
.getCalls()
854+
.find((c) => typeof c.args[0] === 'string' && c.args[0].endsWith('tax-1.json'));
855+
expect(detailWrite, 'writeFile for tax-1.json').to.exist;
856+
const payload = detailWrite!.args[1];
857+
expect(payload.taxonomy.publish_details).to.deep.equal(listPublish);
858+
859+
makeAPICallStub.restore();
860+
});
861+
862+
it('should prefer export taxonomy.publish_details when already present and non-empty', async () => {
863+
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
864+
const fromExport = [{ environment: 'from-export' }];
865+
866+
exportTaxonomies.taxonomies = { 'tax-1': { uid: 'tax-1' } };
867+
exportTaxonomies.publishDetailsByLocale = {
868+
_default: { 'tax-1': [{ from: 'list' }] },
869+
} as any;
870+
exportTaxonomies.taxonomiesFolderPath = '/test/export/taxonomies';
871+
872+
const makeAPICallStub = sinon.stub(exportTaxonomies, 'makeAPICall').callsFake((opts: any) => {
873+
return Promise.resolve(
874+
opts.resolve({
875+
response: { taxonomy: { uid: 'tax-1', publish_details: fromExport }, terms: {} },
876+
uid: 'tax-1',
877+
}),
878+
);
879+
});
880+
881+
await exportTaxonomies.exportTaxonomies();
882+
883+
const detailWrite = writeFileStub
884+
.getCalls()
885+
.find((c) => typeof c.args[0] === 'string' && c.args[0].endsWith('tax-1.json'));
886+
const payload = detailWrite!.args[1];
887+
expect(payload.taxonomy.publish_details).to.deep.equal(fromExport);
888+
889+
makeAPICallStub.restore();
890+
});
891+
892+
it('should fill from list when export has empty array publish_details', async () => {
893+
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
894+
const fromList = [{ from: 'list' }];
895+
896+
exportTaxonomies.taxonomies = { 'tax-1': { uid: 'tax-1' } };
897+
exportTaxonomies.publishDetailsByLocale = { _default: { 'tax-1': fromList } } as any;
898+
exportTaxonomies.taxonomiesFolderPath = '/test/export/taxonomies';
899+
900+
const makeAPICallStub = sinon.stub(exportTaxonomies, 'makeAPICall').callsFake((opts: any) => {
901+
return Promise.resolve(
902+
opts.resolve({
903+
response: { taxonomy: { uid: 'tax-1', publish_details: [] }, terms: {} },
904+
uid: 'tax-1',
905+
}),
906+
);
907+
});
908+
909+
await exportTaxonomies.exportTaxonomies();
910+
911+
const detailWrite = writeFileStub
912+
.getCalls()
913+
.find((c) => typeof c.args[0] === 'string' && c.args[0].endsWith('tax-1.json'));
914+
expect(detailWrite!.args[1].taxonomy.publish_details).to.deep.equal(fromList);
915+
916+
makeAPICallStub.restore();
917+
});
918+
919+
it('should use per-locale list publish_details when exporting locale folder', async () => {
920+
const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub;
921+
const frPublish = [{ locale: 'fr-fr' }];
922+
923+
exportTaxonomies.taxonomies = { 'tax-1': { uid: 'tax-1' } };
924+
exportTaxonomies.taxonomiesByLocale['fr-fr'] = new Set(['tax-1']);
925+
exportTaxonomies.publishDetailsByLocale = { 'fr-fr': { 'tax-1': frPublish } } as any;
926+
exportTaxonomies.taxonomiesFolderPath = '/test/export/taxonomies';
927+
928+
const makeAPICallStub = sinon.stub(exportTaxonomies, 'makeAPICall').callsFake((opts: any) => {
929+
return Promise.resolve(
930+
opts.resolve({
931+
response: { taxonomy: { uid: 'tax-1' }, terms: {} },
932+
uid: 'tax-1',
933+
}),
934+
);
935+
});
936+
937+
await exportTaxonomies.exportTaxonomies('fr-fr');
938+
939+
const detailWrite = writeFileStub
940+
.getCalls()
941+
.find((c) => typeof c.args[0] === 'string' && c.args[0].includes('fr-fr') && c.args[0].endsWith('tax-1.json'));
942+
expect(detailWrite, 'fr-fr/tax-1.json').to.exist;
943+
expect(detailWrite!.args[1].taxonomy.publish_details).to.deep.equal(frPublish);
944+
945+
makeAPICallStub.restore();
946+
});
947+
948+
it('should store publish_details per locale bucket in sanitizeTaxonomiesAttribs', () => {
949+
const listPd = [{ env: 'a' }];
950+
const taxonomies = [
951+
{ uid: 't-loc', name: 'L', publish_details: listPd },
952+
];
953+
exportTaxonomies.publishDetailsByLocale = {};
954+
exportTaxonomies.taxonomiesByLocale['es-es'] = new Set<string>();
955+
956+
exportTaxonomies.sanitizeTaxonomiesAttribs(taxonomies, 'es-es');
957+
958+
expect((exportTaxonomies as any).publishDetailsByLocale['es-es']['t-loc']).to.deep.equal(listPd);
959+
});
960+
});
829961
});

0 commit comments

Comments
 (0)