Skip to content

Commit 68c32c7

Browse files
committed
feat(import): publish taxonomies after import
1 parent 0f16186 commit 68c32c7

5 files changed

Lines changed: 433 additions & 39 deletions

File tree

packages/contentstack-import/src/import/modules/base-class.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export type ApiModuleType =
5858
| 'create-entries'
5959
| 'update-entries'
6060
| 'publish-entries'
61+
| 'publish-taxonomies'
6162
| 'delete-entries'
6263
| 'create-taxonomies'
6364
| 'create-terms'
@@ -343,6 +344,8 @@ export default abstract class BaseClass {
343344
if (
344345
!apiData ||
345346
(entity === 'publish-entries' && !apiData.entryUid) ||
347+
(entity === 'publish-taxonomies' &&
348+
(!apiData.environments?.length || !apiData.locales?.length || !apiData.items?.length)) ||
346349
(entity === 'update-extensions' && !apiData.uid)
347350
) {
348351
return Promise.resolve();
@@ -489,6 +492,14 @@ export default abstract class BaseClass {
489492
})
490493
.then(onSuccess)
491494
.catch(onReject);
495+
case 'publish-taxonomies': {
496+
const publishParams = this.importConfig.branchName ? { branch: this.importConfig.branchName } : {};
497+
return (this.stack as any)
498+
.taxonomy()
499+
.publish(apiData, '3.2', publishParams)
500+
.then(onSuccess)
501+
.catch(onReject);
502+
}
492503
case 'delete-entries':
493504
return this.stack
494505
.contentType(apiData.cTUid)

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

Lines changed: 243 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { join } from 'node:path';
22
import values from 'lodash/values';
33
import isEmpty from 'lodash/isEmpty';
4-
import { log, handleAndLogError } from '@contentstack/cli-utilities';
4+
import { log, handleAndLogError, CLIProgressManager } from '@contentstack/cli-utilities';
55
import { PATH_CONSTANTS } from '../../constants';
66

77
import BaseClass, { ApiOptions } from './base-class';
@@ -19,6 +19,8 @@ export default class ImportTaxonomies extends BaseClass {
1919
private termsSuccessPath: string;
2020
private termsFailsPath: string;
2121
private localesFilePath: string;
22+
private envUidMapperPath: string;
23+
private envUidMapper: Record<string, string> = {};
2224
private isLocaleBasedStructure: boolean = false;
2325
public createdTaxonomies: Record<string, unknown> = {};
2426
public failedTaxonomies: Record<string, unknown> = {};
@@ -46,8 +48,16 @@ export default class ImportTaxonomies extends BaseClass {
4648
importConfig.modules.locales.dirName,
4749
importConfig.modules.locales.fileName,
4850
);
51+
this.envUidMapperPath = join(
52+
importConfig.backupDir,
53+
PATH_CONSTANTS.MAPPER,
54+
PATH_CONSTANTS.MAPPER_MODULES.ENVIRONMENTS,
55+
PATH_CONSTANTS.FILES.UID_MAPPING,
56+
);
4957
}
5058

59+
// --- Lifecycle ---
60+
5161
/**
5262
* @method start
5363
* @returns {Promise<void>} Promise<void>
@@ -56,7 +66,7 @@ export default class ImportTaxonomies extends BaseClass {
5666
try {
5767
log.debug('Starting taxonomies import process...', this.importConfig.context);
5868

59-
const [taxonomiesCount] = await this.analyzeTaxonomies();
69+
const [taxonomiesCount, publishJobCount] = await this.analyzeTaxonomies();
6070
if (taxonomiesCount === 0) {
6171
log.info('No taxonomies found to import', this.importConfig.context);
6272
return;
@@ -67,8 +77,12 @@ export default class ImportTaxonomies extends BaseClass {
6777
// Check if locale-based structure exists before import
6878
this.isLocaleBasedStructure = this.detectAndScanLocaleStructure();
6979

70-
const progress = this.createSimpleProgress(this.currentModuleName, taxonomiesCount);
71-
progress.updateStatus(PROCESS_STATUS[PROCESS_NAMES.TAXONOMIES_IMPORT].IMPORTING);
80+
const progress = this.createNestedProgress(this.currentModuleName);
81+
this.initializeTaxonomiesProgress(progress, taxonomiesCount, publishJobCount);
82+
83+
progress
84+
.startProcess(PROCESS_NAMES.TAXONOMIES_IMPORT)
85+
.updateStatus(PROCESS_STATUS[PROCESS_NAMES.TAXONOMIES_IMPORT].IMPORTING, PROCESS_NAMES.TAXONOMIES_IMPORT);
7286
log.debug('Starting taxonomies import', this.importConfig.context);
7387

7488
if (this.isLocaleBasedStructure) {
@@ -79,6 +93,19 @@ export default class ImportTaxonomies extends BaseClass {
7993
await this.importTaxonomiesLegacy();
8094
}
8195

96+
progress.completeProcess(PROCESS_NAMES.TAXONOMIES_IMPORT, true);
97+
98+
if (publishJobCount > 0) {
99+
progress
100+
.startProcess(PROCESS_NAMES.TAXONOMIES_PUBLISH)
101+
.updateStatus(
102+
PROCESS_STATUS[PROCESS_NAMES.TAXONOMIES_PUBLISH].PUBLISHING,
103+
PROCESS_NAMES.TAXONOMIES_PUBLISH,
104+
);
105+
await this.processTaxonomyPublishing();
106+
progress.completeProcess(PROCESS_NAMES.TAXONOMIES_PUBLISH, true);
107+
}
108+
82109
this.createSuccessAndFailedFile();
83110
this.completeProgressWithMessage();
84111
} catch (error) {
@@ -87,6 +114,8 @@ export default class ImportTaxonomies extends BaseClass {
87114
}
88115
}
89116

117+
// --- Import ---
118+
90119
/**
91120
* create taxonomy and enter success & failure related data into taxonomies mapper file
92121
* @method importTaxonomies
@@ -344,6 +373,191 @@ export default class ImportTaxonomies extends BaseClass {
344373
return true;
345374
}
346375

376+
// --- Progress ---
377+
378+
/**
379+
* Registers nested progress for taxonomy import and optional taxonomy publish when publish jobs exist.
380+
*/
381+
initializeTaxonomiesProgress(progress: CLIProgressManager, taxonomyCount: number, publishJobCount: number): void {
382+
progress.addProcess(PROCESS_NAMES.TAXONOMIES_IMPORT, taxonomyCount);
383+
if (publishJobCount > 0) {
384+
progress.addProcess(PROCESS_NAMES.TAXONOMIES_PUBLISH, publishJobCount);
385+
}
386+
}
387+
388+
// --- Publish ---
389+
390+
/**
391+
* Reads source env UID → destination stack env UID map produced during environments import.
392+
*/
393+
private readEnvUidMapperSync(): Record<string, string> {
394+
if (!fileHelper.fileExistsSync(this.envUidMapperPath)) {
395+
log.debug(`Environment UID mapper not found at ${this.envUidMapperPath}`, this.importConfig.context);
396+
return {};
397+
}
398+
399+
try {
400+
const raw = fsUtil.readFile(this.envUidMapperPath, true) as Record<string, unknown>;
401+
const out: Record<string, string> = {};
402+
for (const [k, v] of Object.entries(raw || {})) {
403+
if (v !== undefined && v !== null && String(v).trim() !== '') {
404+
out[k] = String(v);
405+
}
406+
}
407+
return out;
408+
} catch {
409+
log.debug('Failed to read environment UID mapper', this.importConfig.context);
410+
return {};
411+
}
412+
}
413+
414+
private countPublishEligibleTaxonomies(envMapper: Record<string, string>): number {
415+
let count = 0;
416+
for (const key of Object.keys(this.taxonomies || {})) {
417+
const meta = this.taxonomies[key] as Record<string, any>;
418+
const taxonomyUid = meta?.uid || key;
419+
const filePath = this.findTaxonomyFilePath(taxonomyUid);
420+
if (!filePath) continue;
421+
422+
const details = this.loadTaxonomyFile(filePath);
423+
const tax = details?.taxonomy as Record<string, any> | undefined;
424+
if (!tax?.publish_details?.length || !tax?.locale) continue;
425+
426+
const hasMapped = (tax.publish_details as any[]).some(
427+
(p: any) => p?.environment && envMapper[String(p.environment)],
428+
);
429+
if (hasMapped) count++;
430+
}
431+
return count;
432+
}
433+
434+
private collectTaxonomyPublishJobs(): Array<{ taxonomy: Record<string, any> }> {
435+
const jobs: Array<{ taxonomy: Record<string, any> }> = [];
436+
const seen = new Set<string>();
437+
438+
for (const key of Object.keys(this.taxonomies || {})) {
439+
const meta = this.taxonomies[key] as Record<string, any>;
440+
const taxonomyUid = meta?.uid || key;
441+
if (seen.has(taxonomyUid)) continue;
442+
443+
const filePath = this.findTaxonomyFilePath(taxonomyUid);
444+
if (!filePath) continue;
445+
446+
const details = this.loadTaxonomyFile(filePath);
447+
const tax = details?.taxonomy as Record<string, any> | undefined;
448+
if (!tax?.publish_details?.length || !tax?.locale) continue;
449+
450+
seen.add(taxonomyUid);
451+
jobs.push({ taxonomy: tax });
452+
}
453+
454+
return jobs;
455+
}
456+
457+
private loadEnvUidMapper(): void {
458+
this.envUidMapper = this.readEnvUidMapperSync();
459+
if (isEmpty(this.envUidMapper)) {
460+
log.warn(
461+
'Environment UID mapper is empty; taxonomy publishing is skipped. Import environments first or ensure mapper/environments/uid-mapping.json exists.',
462+
this.importConfig.context,
463+
);
464+
}
465+
}
466+
467+
async processTaxonomyPublishing(): Promise<void> {
468+
this.loadEnvUidMapper();
469+
const jobs = this.collectTaxonomyPublishJobs();
470+
471+
if (jobs.length === 0) {
472+
log.debug('No taxonomies with publish_details to publish', this.importConfig.context);
473+
return;
474+
}
475+
476+
log.info('Starting taxonomy publishing process', this.importConfig.context);
477+
478+
const onSuccess = ({ apiData }: any) => {
479+
const taxonomyUid = apiData?.items?.[0]?.uid;
480+
this.progressManager?.tick(
481+
true,
482+
`taxonomy published: ${taxonomyUid}`,
483+
null,
484+
PROCESS_NAMES.TAXONOMIES_PUBLISH,
485+
);
486+
log.success(`Published taxonomy '${taxonomyUid}'`, this.importConfig.context);
487+
};
488+
489+
const onReject = ({ error, apiData }: any) => {
490+
const taxonomyUid = apiData?.items?.[0]?.uid;
491+
handleAndLogError(
492+
error,
493+
{ ...this.importConfig.context, taxonomyUid },
494+
`Failed to publish taxonomy '${taxonomyUid}'`,
495+
);
496+
this.progressManager?.tick(
497+
false,
498+
`taxonomy publish: ${taxonomyUid}`,
499+
(error as Error)?.message || `Failed to publish taxonomy '${taxonomyUid}'`,
500+
PROCESS_NAMES.TAXONOMIES_PUBLISH,
501+
);
502+
};
503+
504+
await this.makeConcurrentCall(
505+
{
506+
apiContent: jobs as unknown as Record<string, any>[],
507+
processName: 'publish taxonomies',
508+
apiParams: {
509+
serializeData: this.serializePublishTaxonomies.bind(this),
510+
reject: onReject,
511+
resolve: onSuccess,
512+
entity: 'publish-taxonomies',
513+
includeParamOnCompletion: true,
514+
},
515+
concurrencyLimit: this.importConfig.concurrency || this.importConfig.fetchConcurrency || 1,
516+
},
517+
undefined,
518+
false,
519+
);
520+
}
521+
522+
/**
523+
* Builds taxonomy publish payload: destination env UIDs from mapper, locales from taxonomy.locale, items: [{ uid }].
524+
*/
525+
serializePublishTaxonomies(apiOptions: ApiOptions): ApiOptions {
526+
const job = apiOptions.apiData as { taxonomy?: Record<string, any> };
527+
const taxonomy = job?.taxonomy;
528+
529+
if (!taxonomy?.publish_details?.length || !taxonomy?.locale) {
530+
apiOptions.apiData = undefined;
531+
return apiOptions;
532+
}
533+
534+
const environments: string[] = [];
535+
for (const pub of taxonomy.publish_details as any[]) {
536+
const sourceEnvUid = pub?.environment;
537+
if (!sourceEnvUid) continue;
538+
const destUid = this.envUidMapper[String(sourceEnvUid)];
539+
if (destUid && !environments.includes(destUid)) {
540+
environments.push(destUid);
541+
}
542+
}
543+
544+
if (environments.length === 0) {
545+
apiOptions.apiData = undefined;
546+
return apiOptions;
547+
}
548+
549+
const locales = [String(taxonomy.locale)];
550+
apiOptions.apiData = {
551+
environments,
552+
locales,
553+
items: [{ uid: taxonomy.uid }],
554+
};
555+
556+
return apiOptions;
557+
}
558+
559+
// --- Mapper output ---
560+
347561
/**
348562
* create taxonomies success and fail in (mapper/taxonomies)
349563
* create terms success and fail in (mapper/taxonomies/terms)
@@ -396,25 +610,36 @@ export default class ImportTaxonomies extends BaseClass {
396610
}
397611
}
398612

399-
private async analyzeTaxonomies(): Promise<[number]> {
613+
// --- Analyze & prepare ---
614+
615+
private async analyzeTaxonomies(): Promise<[number, number]> {
400616
return this.withLoadingSpinner('TAXONOMIES: Analyzing import data...', async () => {
401617
log.debug('Checking for taxonomies folder existence', this.importConfig.context);
402618

403-
if (fileHelper.fileExistsSync(this.taxonomiesFolderPath)) {
404-
log.debug(`Found taxonomies folder: ${this.taxonomiesFolderPath}`, this.importConfig.context);
405-
406-
this.taxonomies = fsUtil.readFile(join(this.taxonomiesFolderPath, 'taxonomies.json'), true) as Record<
407-
string,
408-
unknown
409-
>;
410-
411-
const taxonomyCount = Object.keys(this.taxonomies || {}).length;
412-
log.debug(`Loaded ${taxonomyCount} taxonomy items from file`, this.importConfig.context);
413-
return [taxonomyCount];
414-
} else {
619+
if (!fileHelper.fileExistsSync(this.taxonomiesFolderPath)) {
415620
log.info(`No Taxonomies Found! - '${this.taxonomiesFolderPath}'`, this.importConfig.context);
416-
return [0];
621+
return [0, 0];
417622
}
623+
624+
log.debug(`Found taxonomies folder: ${this.taxonomiesFolderPath}`, this.importConfig.context);
625+
626+
this.taxonomies = fsUtil.readFile(join(this.taxonomiesFolderPath, 'taxonomies.json'), true) as Record<
627+
string,
628+
unknown
629+
>;
630+
631+
this.isLocaleBasedStructure = this.detectAndScanLocaleStructure();
632+
633+
const taxonomyCount = Object.keys(this.taxonomies || {}).length;
634+
const envMapper = this.readEnvUidMapperSync();
635+
const publishJobCount = this.countPublishEligibleTaxonomies(envMapper);
636+
637+
log.debug(
638+
`Loaded ${taxonomyCount} taxonomy items; ${publishJobCount} eligible for publish (mapped environments).`,
639+
this.importConfig.context,
640+
);
641+
642+
return [taxonomyCount, publishJobCount];
418643
});
419644
}
420645

packages/contentstack-import/src/utils/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const PROCESS_NAMES = {
5757
CONTENT_TYPES_EXT_UPDATE: 'Content Types Ext Update',
5858
WEBHOOKS_IMPORT: 'Webhooks Import',
5959
TAXONOMIES_IMPORT: 'Taxonomies Import',
60+
TAXONOMIES_PUBLISH: 'Taxonomies Publish',
6061
PERSONALIZE_PROJECTS: 'Projects',
6162
} as const;
6263

@@ -267,6 +268,10 @@ export const PROCESS_STATUS = {
267268
IMPORTING: 'Importing taxonomies...',
268269
FAILED: 'Failed to import taxonomies.',
269270
},
271+
[PROCESS_NAMES.TAXONOMIES_PUBLISH]: {
272+
PUBLISHING: 'Publishing taxonomies...',
273+
FAILED: 'Failed to publish taxonomies.',
274+
},
270275
[PROCESS_NAMES.PERSONALIZE_PROJECTS]: {
271276
IMPORTING: 'Importing personalization projects...',
272277
FAILED: 'Failed to import personalization projects.',

0 commit comments

Comments
 (0)