Skip to content

Commit 324590e

Browse files
authored
Merge pull request #124 from contentstack/feat/DX-6003
feat(import): publish taxonomies after import
2 parents 0f16186 + 20a3e9c commit 324590e

8 files changed

Lines changed: 472 additions & 40 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: 183 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
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';
8-
import { fsUtil, fileHelper, MODULE_CONTEXTS, MODULE_NAMES, PROCESS_STATUS, PROCESS_NAMES } from '../../utils';
8+
import {
9+
fsUtil,
10+
fileHelper,
11+
MODULE_CONTEXTS,
12+
MODULE_NAMES,
13+
PROCESS_STATUS,
14+
PROCESS_NAMES,
15+
readEnvUidMapperSync,
16+
warnIfEnvMapperEmpty,
17+
serializePublishTaxonomies,
18+
} from '../../utils';
919
import { ModuleClassParams, TaxonomiesConfig } from '../../types';
1020

1121
export default class ImportTaxonomies extends BaseClass {
@@ -19,6 +29,7 @@ export default class ImportTaxonomies extends BaseClass {
1929
private termsSuccessPath: string;
2030
private termsFailsPath: string;
2131
private localesFilePath: string;
32+
private envUidMapperPath: string;
2233
private isLocaleBasedStructure: boolean = false;
2334
public createdTaxonomies: Record<string, unknown> = {};
2435
public failedTaxonomies: Record<string, unknown> = {};
@@ -46,8 +57,16 @@ export default class ImportTaxonomies extends BaseClass {
4657
importConfig.modules.locales.dirName,
4758
importConfig.modules.locales.fileName,
4859
);
60+
this.envUidMapperPath = join(
61+
importConfig.backupDir,
62+
PATH_CONSTANTS.MAPPER,
63+
PATH_CONSTANTS.MAPPER_MODULES.ENVIRONMENTS,
64+
PATH_CONSTANTS.FILES.UID_MAPPING,
65+
);
4966
}
5067

68+
// --- Lifecycle ---
69+
5170
/**
5271
* @method start
5372
* @returns {Promise<void>} Promise<void>
@@ -56,7 +75,7 @@ export default class ImportTaxonomies extends BaseClass {
5675
try {
5776
log.debug('Starting taxonomies import process...', this.importConfig.context);
5877

59-
const [taxonomiesCount] = await this.analyzeTaxonomies();
78+
const [taxonomiesCount, publishJobCount] = await this.analyzeTaxonomies();
6079
if (taxonomiesCount === 0) {
6180
log.info('No taxonomies found to import', this.importConfig.context);
6281
return;
@@ -67,8 +86,12 @@ export default class ImportTaxonomies extends BaseClass {
6786
// Check if locale-based structure exists before import
6887
this.isLocaleBasedStructure = this.detectAndScanLocaleStructure();
6988

70-
const progress = this.createSimpleProgress(this.currentModuleName, taxonomiesCount);
71-
progress.updateStatus(PROCESS_STATUS[PROCESS_NAMES.TAXONOMIES_IMPORT].IMPORTING);
89+
const progress = this.createNestedProgress(this.currentModuleName);
90+
this.initializeTaxonomiesProgress(progress, taxonomiesCount, publishJobCount);
91+
92+
progress
93+
.startProcess(PROCESS_NAMES.TAXONOMIES_IMPORT)
94+
.updateStatus(PROCESS_STATUS[PROCESS_NAMES.TAXONOMIES_IMPORT].IMPORTING, PROCESS_NAMES.TAXONOMIES_IMPORT);
7295
log.debug('Starting taxonomies import', this.importConfig.context);
7396

7497
if (this.isLocaleBasedStructure) {
@@ -79,6 +102,19 @@ export default class ImportTaxonomies extends BaseClass {
79102
await this.importTaxonomiesLegacy();
80103
}
81104

105+
progress.completeProcess(PROCESS_NAMES.TAXONOMIES_IMPORT, true);
106+
107+
if (publishJobCount > 0) {
108+
progress
109+
.startProcess(PROCESS_NAMES.TAXONOMIES_PUBLISH)
110+
.updateStatus(
111+
PROCESS_STATUS[PROCESS_NAMES.TAXONOMIES_PUBLISH].PUBLISHING,
112+
PROCESS_NAMES.TAXONOMIES_PUBLISH,
113+
);
114+
await this.processTaxonomyPublishing();
115+
progress.completeProcess(PROCESS_NAMES.TAXONOMIES_PUBLISH, true);
116+
}
117+
82118
this.createSuccessAndFailedFile();
83119
this.completeProgressWithMessage();
84120
} catch (error) {
@@ -87,6 +123,8 @@ export default class ImportTaxonomies extends BaseClass {
87123
}
88124
}
89125

126+
// --- Import ---
127+
90128
/**
91129
* create taxonomy and enter success & failure related data into taxonomies mapper file
92130
* @method importTaxonomies
@@ -344,6 +382,121 @@ export default class ImportTaxonomies extends BaseClass {
344382
return true;
345383
}
346384

385+
// --- Progress ---
386+
387+
/**
388+
* Registers nested progress for taxonomy import and optional taxonomy publish when publish jobs exist.
389+
*/
390+
initializeTaxonomiesProgress(progress: CLIProgressManager, taxonomyCount: number, publishJobCount: number): void {
391+
progress.addProcess(PROCESS_NAMES.TAXONOMIES_IMPORT, taxonomyCount);
392+
if (publishJobCount > 0) {
393+
progress.addProcess(PROCESS_NAMES.TAXONOMIES_PUBLISH, publishJobCount);
394+
}
395+
}
396+
397+
// --- Publish ---
398+
399+
private countPublishEligibleTaxonomies(envMapper: Record<string, string>): number {
400+
let count = 0;
401+
for (const key of Object.keys(this.taxonomies || {})) {
402+
const meta = this.taxonomies[key] as Record<string, any>;
403+
const taxonomyUid = meta?.uid || key;
404+
const filePath = this.findTaxonomyFilePath(taxonomyUid);
405+
if (!filePath) continue;
406+
407+
const details = this.loadTaxonomyFile(filePath);
408+
const tax = details?.taxonomy as Record<string, any> | undefined;
409+
if (!tax?.publish_details?.length || !tax?.locale) continue;
410+
411+
const hasMapped = (tax.publish_details as any[]).some(
412+
(p: any) => p?.environment && envMapper[String(p.environment)],
413+
);
414+
if (hasMapped) count++;
415+
}
416+
return count;
417+
}
418+
419+
private collectTaxonomyPublishJobs(): Array<{ taxonomy: Record<string, any> }> {
420+
const jobs: Array<{ taxonomy: Record<string, any> }> = [];
421+
const seen = new Set<string>();
422+
423+
for (const key of Object.keys(this.taxonomies || {})) {
424+
const meta = this.taxonomies[key] as Record<string, any>;
425+
const taxonomyUid = meta?.uid || key;
426+
if (seen.has(taxonomyUid)) continue;
427+
428+
const filePath = this.findTaxonomyFilePath(taxonomyUid);
429+
if (!filePath) continue;
430+
431+
const details = this.loadTaxonomyFile(filePath);
432+
const tax = details?.taxonomy as Record<string, any> | undefined;
433+
if (!tax?.publish_details?.length || !tax?.locale) continue;
434+
435+
seen.add(taxonomyUid);
436+
jobs.push({ taxonomy: tax });
437+
}
438+
439+
return jobs;
440+
}
441+
442+
async processTaxonomyPublishing(): Promise<void> {
443+
const envUidMapper = readEnvUidMapperSync(this.envUidMapperPath, this.importConfig.context);
444+
warnIfEnvMapperEmpty(envUidMapper, this.importConfig.context);
445+
const jobs = this.collectTaxonomyPublishJobs();
446+
447+
if (jobs.length === 0) {
448+
log.debug('No taxonomies with publish_details to publish', this.importConfig.context);
449+
return;
450+
}
451+
452+
log.info('Starting taxonomy publishing process', this.importConfig.context);
453+
454+
const onSuccess = ({ apiData }: any) => {
455+
const taxonomyUid = apiData?.items?.[0]?.uid;
456+
this.progressManager?.tick(
457+
true,
458+
`taxonomy published: ${taxonomyUid}`,
459+
null,
460+
PROCESS_NAMES.TAXONOMIES_PUBLISH,
461+
);
462+
log.success(`Published taxonomy '${taxonomyUid}'`, this.importConfig.context);
463+
};
464+
465+
const onReject = ({ error, apiData }: any) => {
466+
const taxonomyUid = apiData?.items?.[0]?.uid;
467+
handleAndLogError(
468+
error,
469+
{ ...this.importConfig.context, taxonomyUid },
470+
`Failed to publish taxonomy '${taxonomyUid}'`,
471+
);
472+
this.progressManager?.tick(
473+
false,
474+
`taxonomy publish: ${taxonomyUid}`,
475+
(error as Error)?.message || `Failed to publish taxonomy '${taxonomyUid}'`,
476+
PROCESS_NAMES.TAXONOMIES_PUBLISH,
477+
);
478+
};
479+
480+
await this.makeConcurrentCall(
481+
{
482+
apiContent: jobs as unknown as Record<string, any>[],
483+
processName: 'publish taxonomies',
484+
apiParams: {
485+
serializeData: (opts: ApiOptions) => serializePublishTaxonomies(opts, envUidMapper),
486+
reject: onReject,
487+
resolve: onSuccess,
488+
entity: 'publish-taxonomies',
489+
includeParamOnCompletion: true,
490+
},
491+
concurrencyLimit: this.importConfig.concurrency || this.importConfig.fetchConcurrency || 1,
492+
},
493+
undefined,
494+
false,
495+
);
496+
}
497+
498+
// --- Mapper output ---
499+
347500
/**
348501
* create taxonomies success and fail in (mapper/taxonomies)
349502
* create terms success and fail in (mapper/taxonomies/terms)
@@ -396,25 +549,36 @@ export default class ImportTaxonomies extends BaseClass {
396549
}
397550
}
398551

399-
private async analyzeTaxonomies(): Promise<[number]> {
552+
// --- Analyze & prepare ---
553+
554+
private async analyzeTaxonomies(): Promise<[number, number]> {
400555
return this.withLoadingSpinner('TAXONOMIES: Analyzing import data...', async () => {
401556
log.debug('Checking for taxonomies folder existence', this.importConfig.context);
402557

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 {
558+
if (!fileHelper.fileExistsSync(this.taxonomiesFolderPath)) {
415559
log.info(`No Taxonomies Found! - '${this.taxonomiesFolderPath}'`, this.importConfig.context);
416-
return [0];
560+
return [0, 0];
417561
}
562+
563+
log.debug(`Found taxonomies folder: ${this.taxonomiesFolderPath}`, this.importConfig.context);
564+
565+
this.taxonomies = fsUtil.readFile(join(this.taxonomiesFolderPath, 'taxonomies.json'), true) as Record<
566+
string,
567+
unknown
568+
>;
569+
570+
this.isLocaleBasedStructure = this.detectAndScanLocaleStructure();
571+
572+
const taxonomyCount = Object.keys(this.taxonomies || {}).length;
573+
const envMapper = readEnvUidMapperSync(this.envUidMapperPath, this.importConfig.context);
574+
const publishJobCount = this.countPublishEligibleTaxonomies(envMapper);
575+
576+
log.debug(
577+
`Loaded ${taxonomyCount} taxonomy items; ${publishJobCount} eligible for publish (mapped environments).`,
578+
this.importConfig.context,
579+
);
580+
581+
return [taxonomyCount, publishJobCount];
418582
});
419583
}
420584

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.',

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ export {
3131
} from './entries-helper';
3232
export * from './common-helper';
3333
export { lookUpTaxonomy, lookUpTerms } from './taxonomies-helper';
34+
export {
35+
readEnvUidMapperSync,
36+
warnIfEnvMapperEmpty,
37+
serializePublishTaxonomies,
38+
} from './taxonomy-publish-utils';
3439
export { MODULE_CONTEXTS, MODULE_NAMES, PROCESS_NAMES, PROCESS_STATUS } from './constants';

0 commit comments

Comments
 (0)