From a44e47121538ef4f0c76b96f120ad065f3da5593 Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Fri, 19 Dec 2025 18:21:36 +0530 Subject: [PATCH] feat: add progress manager in import setup module --- .talismanrc | 6 +- .../src/commands/cm/stacks/import-setup.ts | 33 +++ .../src/import/modules/assets.ts | 47 +++++ .../src/import/modules/base-setup.ts | 47 +++++ .../src/import/modules/content-types.ts | 37 +++- .../src/import/modules/entries.ts | 10 + .../src/import/modules/extensions.ts | 55 +++-- .../src/import/modules/global-fields.ts | 37 +++- .../src/import/modules/marketplace-apps.ts | 56 ++++- .../src/import/modules/taxonomies.ts | 157 +++++++++----- .../src/types/import-config.ts | 14 ++ .../src/utils/constants.ts | 132 ++++++++++++ .../src/utils/index.ts | 1 + .../test/unit/modules/assets.test.ts | 17 +- .../test/unit/modules/base-setup.test.ts | 192 +++++++++++++++++- .../test/unit/modules/content-types.test.ts | 21 +- .../test/unit/modules/global-fields.test.ts | 21 +- .../src/constants/logging.ts | 2 +- 18 files changed, 785 insertions(+), 100 deletions(-) create mode 100644 packages/contentstack-import-setup/src/utils/constants.ts diff --git a/.talismanrc b/.talismanrc index 775859d95c..758cb4dca1 100644 --- a/.talismanrc +++ b/.talismanrc @@ -202,5 +202,9 @@ fileignoreconfig: - filename: packages/contentstack-import/test/unit/utils/logger.test.ts checksum: 794e06e657a7337c8f094d6042fb04c779683f97b860efae14e075098d2af024 - filename: packages/contentstack-import-setup/src/import/modules/taxonomies.ts - checksum: 49dd8e754a0d3635585a74e943ab097593f061089a7cddc22683ec6caddbb3c5 + checksum: c1bccc885b3f41f187f150c739b4bbd1608b01f09b0d9be0ad9214127cac071d + - filename: packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts + checksum: 06035980b36802260f190af6e63632efe167f5b336693163f59268f3e788fba1 + - filename: packages/contentstack-import-setup/src/utils/constants.ts + checksum: fcfabb4c53ee822e05903db77595413842d656b55e2869bae97bb6c0e0e209c3 version: '1.0' diff --git a/packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts b/packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts index 00b6b3e471..af1e0dd937 100644 --- a/packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts +++ b/packages/contentstack-import-setup/src/commands/cm/stacks/import-setup.ts @@ -9,6 +9,8 @@ import { ContentstackClient, pathValidator, formatError, + CLIProgressManager, + configHandler, } from '@contentstack/cli-utilities'; import { ImportConfig } from '../../../types'; @@ -71,9 +73,27 @@ export default class ImportSetupCommand extends Command { importSetupConfig.host = this.cmaHost; importSetupConfig.region = this.region; importSetupConfig.developerHubBaseUrl = this.developerHubUrl; + + // Prepare the context object + const context = this.createImportContext(importSetupConfig.apiKey, importSetupConfig.authenticationMethod); + importSetupConfig.context = { ...context }; + + if (flags.branch) { + CLIProgressManager.initializeGlobalSummary( + `IMPORT-SETUP-${flags.branch}`, + flags.branch, + `Setting up import for "${flags.branch}" branch...`, + ); + } else { + CLIProgressManager.initializeGlobalSummary(`IMPORT-SETUP`, flags.branch, 'Setting up import...'); + } + const managementAPIClient: ContentstackClient = await managementSDKClient(importSetupConfig); const importSetup = new ImportSetup(importSetupConfig, managementAPIClient); await importSetup.start(); + + CLIProgressManager.printGlobalSummary(); + log( importSetupConfig, `Backup folder and mapper files have been successfully created for the stack using the API key ${importSetupConfig.apiKey}.`, @@ -85,6 +105,7 @@ export default class ImportSetupCommand extends Command { 'success', ); } catch (error) { + CLIProgressManager.printGlobalSummary(); log( { data: '' } as ImportConfig, `Failed to create the backup folder and mapper files: ${formatError(error)}`, @@ -92,4 +113,16 @@ export default class ImportSetupCommand extends Command { ); } } + + private createImportContext(apiKey: string, authenticationMethod?: string): any { + return { + command: this.context?.info?.command || 'cm:stacks:import-setup', + module: '', + userId: configHandler.get('userUid') || '', + sessionId: this.context?.sessionId, + apiKey: apiKey || '', + orgId: configHandler.get('oauthOrgUid') || '', + authenticationMethod: authenticationMethod || 'Basic Auth', + }; + } } diff --git a/packages/contentstack-import-setup/src/import/modules/assets.ts b/packages/contentstack-import-setup/src/import/modules/assets.ts index e27081a1ae..70e3220304 100644 --- a/packages/contentstack-import-setup/src/import/modules/assets.ts +++ b/packages/contentstack-import-setup/src/import/modules/assets.ts @@ -5,6 +5,7 @@ import { AssetRecord, ImportConfig, ModuleClassParams } from '../../types'; import { isEmpty, orderBy, values } from 'lodash'; import { formatError, FsUtility, sanitizePath } from '@contentstack/cli-utilities'; import BaseImportSetup from './base-setup'; +import { MODULE_NAMES, MODULE_CONTEXTS, PROCESS_NAMES, PROCESS_STATUS } from '../../utils'; export default class AssetImportSetup extends BaseImportSetup { private assetsFilePath: string; @@ -20,6 +21,7 @@ export default class AssetImportSetup extends BaseImportSetup { constructor({ config, stackAPIClient, dependencies }: ModuleClassParams) { super({ config, stackAPIClient, dependencies }); + this.currentModuleName = MODULE_NAMES[MODULE_CONTEXTS.ASSETS]; this.assetsFolderPath = join(sanitizePath(this.config.contentDir), 'assets'); this.assetsFilePath = join(sanitizePath(this.config.contentDir), 'assets', 'assets.json'); this.assetsConfig = config.modules.assets; @@ -39,10 +41,51 @@ export default class AssetImportSetup extends BaseImportSetup { */ async start() { try { + const progress = this.createNestedProgress(this.currentModuleName); + + // Analyze to get chunk count + const indexerCount = await this.withLoadingSpinner('ASSETS: Analyzing import data...', async () => { + const basePath = this.assetsFolderPath; + const fs = new FsUtility({ basePath, indexFileName: 'assets.json' }); + const indexer = fs.indexFileContent; + return values(indexer).length; + }); + + if (indexerCount === 0) { + log(this.config, 'No assets found in the content folder.', 'info'); + return; + } + + // Add processes - use a large number for total assets since we don't know exact count + // The progress will update as we process each asset + progress.addProcess(PROCESS_NAMES.ASSETS_MAPPER_GENERATION, 1); + progress.addProcess(PROCESS_NAMES.ASSETS_FETCH_AND_MAP, indexerCount * 10); // Estimate: ~10 assets per chunk + + // Create mapper directory + progress + .startProcess(PROCESS_NAMES.ASSETS_MAPPER_GENERATION) + .updateStatus( + PROCESS_STATUS.ASSETS_MAPPER_GENERATION.GENERATING, + PROCESS_NAMES.ASSETS_MAPPER_GENERATION, + ); fsUtil.makeDirectory(this.mapperDirPath); + this.progressManager?.tick(true, 'mapper directory created', null, PROCESS_NAMES.ASSETS_MAPPER_GENERATION); + progress.completeProcess(PROCESS_NAMES.ASSETS_MAPPER_GENERATION, true); + + // Fetch and map assets + progress + .startProcess(PROCESS_NAMES.ASSETS_FETCH_AND_MAP) + .updateStatus( + PROCESS_STATUS.ASSETS_FETCH_AND_MAP.FETCHING, + PROCESS_NAMES.ASSETS_FETCH_AND_MAP, + ); await this.fetchAndMapAssets(); + progress.completeProcess(PROCESS_NAMES.ASSETS_FETCH_AND_MAP, true); + + this.completeProgress(true); log(this.config, `The required setup files for the asset have been generated successfully.`, 'success'); } catch (error) { + this.completeProgress(false, error?.message || 'Assets mapper generation failed'); log(this.config, `Error occurred while generating the asset mapper: ${formatError(error)}.`, 'error'); } } @@ -67,17 +110,21 @@ export default class AssetImportSetup extends BaseImportSetup { if (items.length === 1) { this.assetUidMapper[uid] = items[0].uid; this.assetUrlMapper[url] = items[0].url; + this.progressManager?.tick(true, `asset: ${title}`, null, PROCESS_NAMES.ASSETS_FETCH_AND_MAP); log(this.config, `Mapped asset successfully: '${title}'`, 'info'); } else if (items.length > 1) { this.duplicateAssets[uid] = items.map((asset: any) => { return { uid: asset.uid, title: asset.title, url: asset.url }; }); + this.progressManager?.tick(true, `asset: ${title} (duplicate)`, null, PROCESS_NAMES.ASSETS_FETCH_AND_MAP); log(this.config, `Multiple assets found with the title '${title}'.`, 'info'); } else { + this.progressManager?.tick(false, `asset: ${title}`, 'Not found in stack', PROCESS_NAMES.ASSETS_FETCH_AND_MAP); log(this.config, `Asset with title '${title}' not found in the stack!`, 'info'); } }; const onReject = ({ error, apiData: { title } = undefined }: any) => { + this.progressManager?.tick(false, `asset: ${title}`, formatError(error), PROCESS_NAMES.ASSETS_FETCH_AND_MAP); log(this.config, `Failed to map the asset '${title}'.`, 'error'); log(this.config, formatError(error), 'error'); }; diff --git a/packages/contentstack-import-setup/src/import/modules/base-setup.ts b/packages/contentstack-import-setup/src/import/modules/base-setup.ts index 5385a0ea49..69bea1cde1 100644 --- a/packages/contentstack-import-setup/src/import/modules/base-setup.ts +++ b/packages/contentstack-import-setup/src/import/modules/base-setup.ts @@ -1,11 +1,14 @@ import { log, fsUtil } from '../../utils'; import { ApiOptions, CustomPromiseHandler, EnvType, ImportConfig, ModuleClassParams } from '../../types'; import { chunk, entries, isEmpty, isEqual, last } from 'lodash'; +import { CLIProgressManager, configHandler } from '@contentstack/cli-utilities'; export default class BaseImportSetup { public config: ImportConfig; public stackAPIClient: ModuleClassParams['stackAPIClient']; public dependencies: ModuleClassParams['dependencies']; + protected progressManager: CLIProgressManager | null = null; + protected currentModuleName: string = ''; constructor({ config, stackAPIClient, dependencies }: ModuleClassParams) { this.config = config; @@ -205,4 +208,48 @@ export default class BaseImportSetup { return Promise.resolve(); } } + + /** + * Create simple progress manager + */ + protected createSimpleProgress(moduleName: string, total?: number): CLIProgressManager { + this.currentModuleName = moduleName; + const logConfig = configHandler.get('log') || {}; + const showConsoleLogs = logConfig.showConsoleLogs ?? false; + this.progressManager = CLIProgressManager.createSimple(moduleName, total, showConsoleLogs); + return this.progressManager; + } + + /** + * Create nested progress manager + */ + protected createNestedProgress(moduleName: string): CLIProgressManager { + this.currentModuleName = moduleName; + const logConfig = configHandler.get('log') || {}; + const showConsoleLogs = logConfig.showConsoleLogs ?? false; + this.progressManager = CLIProgressManager.createNested(moduleName, showConsoleLogs); + return this.progressManager; + } + + /** + * Complete progress manager + */ + protected completeProgress(success: boolean = true, error?: string): void { + this.progressManager?.complete(success, error); + this.progressManager = null; + } + + /** + * Show a loading spinner before initializing progress + */ + protected async withLoadingSpinner(message: string, action: () => Promise): Promise { + const logConfig = configHandler.get('log') || {}; + const showConsoleLogs = logConfig.showConsoleLogs ?? false; + + if (showConsoleLogs) { + // If console logs are enabled, don't show spinner, just execute the action + return await action(); + } + return await CLIProgressManager.withLoadingSpinner(message, action); + } } diff --git a/packages/contentstack-import-setup/src/import/modules/content-types.ts b/packages/contentstack-import-setup/src/import/modules/content-types.ts index 476615481c..c06036a33c 100644 --- a/packages/contentstack-import-setup/src/import/modules/content-types.ts +++ b/packages/contentstack-import-setup/src/import/modules/content-types.ts @@ -4,17 +4,52 @@ import { join } from 'path'; import { ImportConfig, ModuleClassParams } from '../../types'; import ExtensionImportSetup from './extensions'; import BaseImportSetup from './base-setup'; +import { MODULE_NAMES, MODULE_CONTEXTS, PROCESS_NAMES, PROCESS_STATUS } from '../../utils'; export default class ContentTypesImportSetup extends BaseImportSetup { constructor(options: ModuleClassParams) { super(options); + this.currentModuleName = MODULE_NAMES[MODULE_CONTEXTS.CONTENT_TYPES]; } async start() { try { - await this.setupDependencies(); + const progress = this.createNestedProgress(this.currentModuleName); + + // Add processes + progress.addProcess(PROCESS_NAMES.CONTENT_TYPES_DEPENDENCY_SETUP, this.dependencies?.length || 0); + progress.addProcess(PROCESS_NAMES.CONTENT_TYPES_MAPPER_GENERATION, 1); + + // Setup dependencies + if (this.dependencies && this.dependencies.length > 0) { + progress + .startProcess(PROCESS_NAMES.CONTENT_TYPES_DEPENDENCY_SETUP) + .updateStatus( + PROCESS_STATUS.CONTENT_TYPES_DEPENDENCY_SETUP.SETTING_UP, + PROCESS_NAMES.CONTENT_TYPES_DEPENDENCY_SETUP, + ); + + await this.setupDependencies(); + + this.progressManager?.tick(true, 'dependencies setup', null, PROCESS_NAMES.CONTENT_TYPES_DEPENDENCY_SETUP); + progress.completeProcess(PROCESS_NAMES.CONTENT_TYPES_DEPENDENCY_SETUP, true); + } + + // Mapper generation + progress + .startProcess(PROCESS_NAMES.CONTENT_TYPES_MAPPER_GENERATION) + .updateStatus( + PROCESS_STATUS.CONTENT_TYPES_MAPPER_GENERATION.GENERATING, + PROCESS_NAMES.CONTENT_TYPES_MAPPER_GENERATION, + ); + + this.progressManager?.tick(true, 'mapper generation', null, PROCESS_NAMES.CONTENT_TYPES_MAPPER_GENERATION); + progress.completeProcess(PROCESS_NAMES.CONTENT_TYPES_MAPPER_GENERATION, true); + + this.completeProgress(true); log(this.config, `The required setup files for content types have been generated successfully.`, 'success'); } catch (error) { + this.completeProgress(false, error?.message || 'Content types mapper generation failed'); log(this.config, `Error occurred while generating the content type mapper: ${error.message}.`, 'error'); } } diff --git a/packages/contentstack-import-setup/src/import/modules/entries.ts b/packages/contentstack-import-setup/src/import/modules/entries.ts index 93cde3ace8..412a5e6a76 100644 --- a/packages/contentstack-import-setup/src/import/modules/entries.ts +++ b/packages/contentstack-import-setup/src/import/modules/entries.ts @@ -1,17 +1,27 @@ import { log } from '../../utils'; import { ModuleClassParams } from '../../types'; import BaseImportSetup from './base-setup'; +import { MODULE_NAMES, MODULE_CONTEXTS, PROCESS_NAMES, PROCESS_STATUS } from '../../utils'; export default class EntriesImportSetup extends BaseImportSetup { constructor(options: ModuleClassParams) { super(options); + this.currentModuleName = MODULE_NAMES[MODULE_CONTEXTS.ENTRIES]; } async start() { try { + const progress = this.createSimpleProgress(this.currentModuleName, 1); + + progress.updateStatus('Setting up dependencies...'); await this.setupDependencies(); + + this.progressManager?.tick(true, 'entries mapper setup', null); + this.completeProgress(true); + log(this.config, `The required setup files for entries have been generated successfully.`, 'success'); } catch (error) { + this.completeProgress(false, error?.message || 'Entries mapper generation failed'); log(this.config, `Error occurred while generating the entry mapper: ${error.message}.`, 'error'); } } diff --git a/packages/contentstack-import-setup/src/import/modules/extensions.ts b/packages/contentstack-import-setup/src/import/modules/extensions.ts index f6794bd70e..31e108d7f4 100644 --- a/packages/contentstack-import-setup/src/import/modules/extensions.ts +++ b/packages/contentstack-import-setup/src/import/modules/extensions.ts @@ -4,21 +4,19 @@ import { join } from 'path'; import { ImportConfig, ModuleClassParams } from '../../types'; import { isEmpty } from 'lodash'; import { formatError, sanitizePath } from '@contentstack/cli-utilities'; +import BaseImportSetup from './base-setup'; +import { MODULE_NAMES, MODULE_CONTEXTS, PROCESS_NAMES, PROCESS_STATUS } from '../../utils'; -export default class ExtensionImportSetup { - private config: ImportConfig; +export default class ExtensionImportSetup extends BaseImportSetup { private extensionsFilePath: string; private extensionMapper: Record; - private stackAPIClient: ModuleClassParams['stackAPIClient']; - private dependencies: ModuleClassParams['dependencies']; private extensionsConfig: ImportConfig['modules']['extensions']; - private mapperDirPath: string; private extensionsFolderPath: string; private extUidMapperPath: string; constructor({ config, stackAPIClient }: ModuleClassParams) { - this.config = config; - this.stackAPIClient = stackAPIClient; + super({ config, stackAPIClient, dependencies: [] }); + this.currentModuleName = MODULE_NAMES[MODULE_CONTEXTS.EXTENSIONS]; this.extensionsFilePath = join(sanitizePath(this.config.contentDir), 'extensions', 'extensions.json'); this.extensionsConfig = config.modules.extensions; this.extUidMapperPath = join(sanitizePath(this.config.backupDir), 'mapper', 'extensions', 'uid-mapping.json'); @@ -32,28 +30,53 @@ export default class ExtensionImportSetup { */ async start() { try { - const extensions: any = await fsUtil.readFile(this.extensionsFilePath); + const extensions: any = await this.withLoadingSpinner('EXTENSIONS: Analyzing import data...', async () => { + return await fsUtil.readFile(this.extensionsFilePath); + }); + if (!isEmpty(extensions)) { - // 2. Create mapper directory + const extensionsArray = Object.values(extensions) as any[]; + const progress = this.createNestedProgress(this.currentModuleName); + + // Add process + progress.addProcess(PROCESS_NAMES.EXTENSIONS_MAPPER_GENERATION, extensionsArray.length); + + // Create mapper directory const mapperFilePath = join(sanitizePath(this.config.backupDir), 'mapper', 'extensions'); - fsUtil.makeDirectory(mapperFilePath); // Use fsUtil + fsUtil.makeDirectory(mapperFilePath); + + progress + .startProcess(PROCESS_NAMES.EXTENSIONS_MAPPER_GENERATION) + .updateStatus( + PROCESS_STATUS.EXTENSIONS_MAPPER_GENERATION.GENERATING, + PROCESS_NAMES.EXTENSIONS_MAPPER_GENERATION, + ); - for (const extension of Object.values(extensions) as any) { - const targetExtension: any = await this.getExtension(extension); - if (!targetExtension) { - log(this.config, `Extension with the title '${extension.title}' not found in the stack.`, 'info'); - continue; + for (const extension of extensionsArray) { + try { + const targetExtension: any = await this.getExtension(extension); + if (!targetExtension) { + log(this.config, `Extension with the title '${extension.title}' not found in the stack.`, 'info'); + this.progressManager?.tick(false, `extension: ${extension.title}`, 'Not found in stack', PROCESS_NAMES.EXTENSIONS_MAPPER_GENERATION); + continue; + } + this.extensionMapper[extension.uid] = targetExtension.uid; + this.progressManager?.tick(true, `extension: ${extension.title}`, null, PROCESS_NAMES.EXTENSIONS_MAPPER_GENERATION); + } catch (error) { + this.progressManager?.tick(false, `extension: ${extension.title}`, formatError(error), PROCESS_NAMES.EXTENSIONS_MAPPER_GENERATION); } - this.extensionMapper[extension.uid] = targetExtension.uid; } await fsUtil.writeFile(this.extUidMapperPath, this.extensionMapper); + progress.completeProcess(PROCESS_NAMES.EXTENSIONS_MAPPER_GENERATION, true); + this.completeProgress(true); log(this.config, `The required setup files for extensions have been generated successfully.`, 'success'); } else { log(this.config, 'No extensions found in the content folder.', 'info'); } } catch (error) { + this.completeProgress(false, error?.message || 'Extensions mapper generation failed'); log(this.config, `Error occurred while generating the extension mapper: ${formatError(error)}.`, 'error'); } } diff --git a/packages/contentstack-import-setup/src/import/modules/global-fields.ts b/packages/contentstack-import-setup/src/import/modules/global-fields.ts index 6c3e9cc68e..1ade770000 100644 --- a/packages/contentstack-import-setup/src/import/modules/global-fields.ts +++ b/packages/contentstack-import-setup/src/import/modules/global-fields.ts @@ -3,17 +3,52 @@ import { log, fsUtil } from '../../utils'; import { join } from 'path'; import { ImportConfig, ModuleClassParams } from '../../types'; import BaseImportSetup from './base-setup'; +import { MODULE_NAMES, MODULE_CONTEXTS, PROCESS_NAMES, PROCESS_STATUS } from '../../utils'; export default class GlobalFieldsImportSetup extends BaseImportSetup { constructor(options: ModuleClassParams) { super(options); + this.currentModuleName = MODULE_NAMES[MODULE_CONTEXTS.GLOBAL_FIELDS]; } async start() { try { - await this.setupDependencies(); + const progress = this.createNestedProgress(this.currentModuleName); + + // Add processes + progress.addProcess(PROCESS_NAMES.GLOBAL_FIELDS_DEPENDENCY_SETUP, this.dependencies?.length || 0); + progress.addProcess(PROCESS_NAMES.GLOBAL_FIELDS_MAPPER_GENERATION, 1); + + // Setup dependencies + if (this.dependencies && this.dependencies.length > 0) { + progress + .startProcess(PROCESS_NAMES.GLOBAL_FIELDS_DEPENDENCY_SETUP) + .updateStatus( + PROCESS_STATUS.GLOBAL_FIELDS_DEPENDENCY_SETUP.SETTING_UP, + PROCESS_NAMES.GLOBAL_FIELDS_DEPENDENCY_SETUP, + ); + + await this.setupDependencies(); + + this.progressManager?.tick(true, 'dependencies setup', null, PROCESS_NAMES.GLOBAL_FIELDS_DEPENDENCY_SETUP); + progress.completeProcess(PROCESS_NAMES.GLOBAL_FIELDS_DEPENDENCY_SETUP, true); + } + + // Mapper generation + progress + .startProcess(PROCESS_NAMES.GLOBAL_FIELDS_MAPPER_GENERATION) + .updateStatus( + PROCESS_STATUS.GLOBAL_FIELDS_MAPPER_GENERATION.GENERATING, + PROCESS_NAMES.GLOBAL_FIELDS_MAPPER_GENERATION, + ); + + this.progressManager?.tick(true, 'mapper generation', null, PROCESS_NAMES.GLOBAL_FIELDS_MAPPER_GENERATION); + progress.completeProcess(PROCESS_NAMES.GLOBAL_FIELDS_MAPPER_GENERATION, true); + + this.completeProgress(true); log(this.config, `The required setup files for global fields have been generated successfully.`, 'success'); } catch (error) { + this.completeProgress(false, error?.message || 'Global fields mapper generation failed'); log(this.config, `Error occurred while generating the global field mapper: ${error.message}.`, 'error'); } } diff --git a/packages/contentstack-import-setup/src/import/modules/marketplace-apps.ts b/packages/contentstack-import-setup/src/import/modules/marketplace-apps.ts index 4b379ddefa..7757a12acd 100644 --- a/packages/contentstack-import-setup/src/import/modules/marketplace-apps.ts +++ b/packages/contentstack-import-setup/src/import/modules/marketplace-apps.ts @@ -11,13 +11,12 @@ import { createDeveloperHubUrl, sanitizePath, } from '@contentstack/cli-utilities'; +import BaseImportSetup from './base-setup'; +import { MODULE_NAMES, MODULE_CONTEXTS, PROCESS_NAMES, PROCESS_STATUS } from '../../utils'; -export default class marketplaceAppImportSetup { - private config: ImportConfig; +export default class marketplaceAppImportSetup extends BaseImportSetup { private marketplaceAppsFilePath: string; private marketplaceAppMapper: any; - private stackAPIClient: ModuleClassParams['stackAPIClient']; - private dependencies: ModuleClassParams['dependencies']; private marketplaceAppsConfig: ImportConfig['modules']['marketplace-apps']; private mapperDirPath: string; private marketplaceAppsFolderPath: string; @@ -28,8 +27,8 @@ export default class marketplaceAppImportSetup { public appSdk: ContentstackMarketplaceClient; constructor({ config, stackAPIClient }: ModuleClassParams) { - this.config = config; - this.stackAPIClient = stackAPIClient; + super({ config, stackAPIClient, dependencies: [] }); + this.currentModuleName = MODULE_NAMES[MODULE_CONTEXTS.MARKETPLACE_APPS]; this.marketplaceAppsFilePath = join( sanitizePath(this.config.contentDir), 'marketplace_apps', @@ -47,22 +46,57 @@ export default class marketplaceAppImportSetup { */ async start() { try { - const sourceMarketplaceApps: any = await fsUtil.readFile(this.marketplaceAppsFilePath); + const sourceMarketplaceApps: any = await this.withLoadingSpinner('MARKETPLACE APPS: Analyzing import data...', async () => { + return await fsUtil.readFile(this.marketplaceAppsFilePath); + }); + if (!isEmpty(sourceMarketplaceApps)) { - fsUtil.makeDirectory(this.marketplaceAppsUidMapperPath); // Use fsUtil + const appsArray = Array.isArray(sourceMarketplaceApps) ? sourceMarketplaceApps : Object.values(sourceMarketplaceApps); + const progress = this.createNestedProgress(this.currentModuleName); + + // Add processes + progress.addProcess(PROCESS_NAMES.MARKETPLACE_APPS_MAPPER_GENERATION, 1); + progress.addProcess(PROCESS_NAMES.MARKETPLACE_APPS_FETCH, appsArray.length); + + // Create mapper directory + progress + .startProcess(PROCESS_NAMES.MARKETPLACE_APPS_MAPPER_GENERATION) + .updateStatus( + PROCESS_STATUS.MARKETPLACE_APPS_MAPPER_GENERATION.GENERATING, + PROCESS_NAMES.MARKETPLACE_APPS_MAPPER_GENERATION, + ); + fsUtil.makeDirectory(this.marketplaceAppsUidMapperPath); + this.progressManager?.tick(true, 'mapper directory created', null, PROCESS_NAMES.MARKETPLACE_APPS_MAPPER_GENERATION); + progress.completeProcess(PROCESS_NAMES.MARKETPLACE_APPS_MAPPER_GENERATION, true); + + // Fetch marketplace apps + progress + .startProcess(PROCESS_NAMES.MARKETPLACE_APPS_FETCH) + .updateStatus( + PROCESS_STATUS.MARKETPLACE_APPS_FETCH.FETCHING, + PROCESS_NAMES.MARKETPLACE_APPS_FETCH, + ); + this.developerHubBaseUrl = this.config.developerHubBaseUrl || (await createDeveloperHubUrl(this.config.host)); // NOTE init marketplace app sdk const host = this.developerHubBaseUrl.split('://').pop(); this.appSdk = await marketplaceSDKClient({ host }); const targetMarketplaceApps: any = await this.getMarketplaceApps(); + + this.progressManager?.tick(true, 'marketplace apps fetched', null, PROCESS_NAMES.MARKETPLACE_APPS_FETCH); + this.createMapper(sourceMarketplaceApps, targetMarketplaceApps); await fsUtil.writeFile(join(this.marketplaceAppsUidMapperPath, 'uid-mapping.json'), this.marketplaceAppMapper); + + progress.completeProcess(PROCESS_NAMES.MARKETPLACE_APPS_FETCH, true); + this.completeProgress(true); log(this.config, `The required setup files for Marketplace apps have been generated successfully.`, 'success'); } else { log(this.config, 'No Marketplace apps found in the content folder.', 'info'); } } catch (error) { + this.completeProgress(false, error?.message || 'Marketplace apps mapper generation failed'); log(this.config, `Error occurred while generating the Marketplace app mapper: ${error.message}.`, 'error'); } } @@ -83,7 +117,9 @@ export default class marketplaceAppImportSetup { } createMapper(sourceMarketplaceApps: any, targetMarketplaceApps: any) { - sourceMarketplaceApps.forEach((sourceApp: any) => { + const appsArray = Array.isArray(sourceMarketplaceApps) ? sourceMarketplaceApps : Object.values(sourceMarketplaceApps); + + appsArray.forEach((sourceApp: any) => { // Find matching target item based on manifest.name // TBD: This logic is not foolproof, need to find a better way to match source and target apps // Reason: While importing apps, if an app exist in the target with the same name, it will be a conflict and will not be imported @@ -119,7 +155,9 @@ export default class marketplaceAppImportSetup { }); } }); + this.progressManager?.tick(true, `app: ${sourceAppName}`, null, PROCESS_NAMES.MARKETPLACE_APPS_FETCH); } else { + this.progressManager?.tick(false, `app: ${sourceAppName}`, 'Not found in target stack', PROCESS_NAMES.MARKETPLACE_APPS_FETCH); log(this.config, `No matching Marketplace app found in the target stack with name ${sourceAppName}`, 'info'); } }); diff --git a/packages/contentstack-import-setup/src/import/modules/taxonomies.ts b/packages/contentstack-import-setup/src/import/modules/taxonomies.ts index 38dcad4c9c..10e1f8cf56 100644 --- a/packages/contentstack-import-setup/src/import/modules/taxonomies.ts +++ b/packages/contentstack-import-setup/src/import/modules/taxonomies.ts @@ -5,13 +5,12 @@ import isEmpty from 'lodash/isEmpty'; import { log, fsUtil, fileHelper } from '../../utils'; import { ImportConfig, ModuleClassParams, TaxonomyQueryParams } from '../../types'; import { sanitizePath } from '@contentstack/cli-utilities'; +import BaseImportSetup from './base-setup'; +import { MODULE_NAMES, MODULE_CONTEXTS, PROCESS_NAMES, PROCESS_STATUS } from '../../utils'; -export default class TaxonomiesImportSetup { - private config: ImportConfig; +export default class TaxonomiesImportSetup extends BaseImportSetup { private taxonomiesFilePath: string; private taxonomiesFolderPath: string; - private stackAPIClient: ModuleClassParams['stackAPIClient']; - private dependencies: ModuleClassParams['dependencies']; private taxonomiesConfig: ImportConfig['modules']['taxonomies']; private termsSuccessPath: string; private taxSuccessPath: string; @@ -24,8 +23,8 @@ export default class TaxonomiesImportSetup { public masterLocaleFilePath: string; constructor({ config, stackAPIClient }: ModuleClassParams) { - this.config = config; - this.stackAPIClient = stackAPIClient; + super({ config, stackAPIClient, dependencies: [] }); + this.currentModuleName = MODULE_NAMES[MODULE_CONTEXTS.TAXONOMIES]; this.taxonomiesFolderPath = join(sanitizePath(this.config.contentDir), 'taxonomies'); this.taxonomiesFilePath = join(this.taxonomiesFolderPath, 'taxonomies.json'); this.taxonomiesConfig = config.modules.taxonomies; @@ -55,23 +54,53 @@ export default class TaxonomiesImportSetup { */ async start(): Promise { try { - const taxonomies: any = fsUtil.readFile(this.taxonomiesFilePath); + const taxonomies: any = await this.withLoadingSpinner('TAXONOMIES: Analyzing import data...', async () => { + return fsUtil.readFile(this.taxonomiesFilePath); + }); + if (!isEmpty(taxonomies)) { - // 1. Detect locale-based structure + const taxonomiesArray = Object.values(taxonomies) as any[]; + const progress = this.createNestedProgress(this.currentModuleName); + + // Detect locale-based structure this.isLocaleBasedStructure = this.detectLocaleBasedStructure(); - // 2. Create mapper directory + // Add processes + progress.addProcess(PROCESS_NAMES.TAXONOMIES_MAPPER_GENERATION, 1); + progress.addProcess(PROCESS_NAMES.TAXONOMIES_FETCH, taxonomiesArray.length); + progress.addProcess(PROCESS_NAMES.TAXONOMIES_TERMS_FETCH, taxonomiesArray.length); + + // Create mapper directory + progress + .startProcess(PROCESS_NAMES.TAXONOMIES_MAPPER_GENERATION) + .updateStatus( + PROCESS_STATUS.TAXONOMIES_MAPPER_GENERATION.GENERATING, + PROCESS_NAMES.TAXONOMIES_MAPPER_GENERATION, + ); fsUtil.makeDirectory(this.taxonomiesMapperDirPath); fsUtil.makeDirectory(this.termsMapperDirPath); + this.progressManager?.tick(true, 'mapper directories created', null, PROCESS_NAMES.TAXONOMIES_MAPPER_GENERATION); + progress.completeProcess(PROCESS_NAMES.TAXONOMIES_MAPPER_GENERATION, true); + + // Fetch taxonomies + progress + .startProcess(PROCESS_NAMES.TAXONOMIES_FETCH) + .updateStatus( + PROCESS_STATUS.TAXONOMIES_FETCH.FETCHING, + PROCESS_NAMES.TAXONOMIES_FETCH, + ); if (this.isLocaleBasedStructure) { log(this.config, 'Detected locale-based folder structure for taxonomies', 'info'); - await this.setupTaxonomiesByLocale(taxonomies); + await this.setupTaxonomiesByLocale(taxonomies, progress); } else { log(this.config, 'Using legacy folder structure for taxonomies', 'info'); - await this.setupTaxonomiesLegacy(taxonomies); + await this.setupTaxonomiesLegacy(taxonomies, progress); } + progress.completeProcess(PROCESS_NAMES.TAXONOMIES_FETCH, true); + progress.completeProcess(PROCESS_NAMES.TAXONOMIES_TERMS_FETCH, true); + if (this.taxonomiesMapper !== undefined && !isEmpty(this.taxonomiesMapper)) { fsUtil.writeFile(this.taxSuccessPath, this.taxonomiesMapper); } @@ -79,11 +108,13 @@ export default class TaxonomiesImportSetup { fsUtil.writeFile(this.termsSuccessPath, this.termsMapper); } + this.completeProgress(true); log(this.config, `The required setup files for taxonomies have been generated successfully.`, 'success'); } else { log(this.config, 'No taxonomies found in the content folder.', 'info'); } } catch (error) { + this.completeProgress(false, error?.message || 'Taxonomies mapper generation failed'); log(this.config, `Error generating taxonomies mapper: ${error.message}`, 'error'); } } @@ -91,22 +122,31 @@ export default class TaxonomiesImportSetup { /** * Setup taxonomies using legacy format (root-level taxonomy files) */ - async setupTaxonomiesLegacy(taxonomies: any): Promise { + async setupTaxonomiesLegacy(taxonomies: any, progress: any): Promise { for (const taxonomy of Object.values(taxonomies) as any) { - let targetTaxonomy: any = await this.getTaxonomies(taxonomy); - if (!targetTaxonomy) { - log(this.config, `Taxonomies with uid '${taxonomy.uid}' not found in the stack!`, 'info'); - continue; - } - targetTaxonomy = this.sanitizeTaxonomyAttribs(targetTaxonomy); - this.taxonomiesMapper[taxonomy.uid] = targetTaxonomy; - const terms = await this.getAllTermsOfTaxonomy(targetTaxonomy); - if (Array.isArray(terms) && terms.length > 0) { - log(this.config, `Terms found for taxonomy '${taxonomy.uid}', processing...`, 'info'); - const sanitizedTerms = this.sanitizeTermsAttribs(terms); - this.termsMapper[taxonomy.uid] = sanitizedTerms; - } else { - log(this.config, `No terms found for taxonomy '${taxonomy.uid}', skipping...`, 'info'); + try { + let targetTaxonomy: any = await this.getTaxonomies(taxonomy); + if (!targetTaxonomy) { + this.progressManager?.tick(false, `taxonomy: ${taxonomy.uid}`, 'Not found in stack', PROCESS_NAMES.TAXONOMIES_FETCH); + log(this.config, `Taxonomies with uid '${taxonomy.uid}' not found in the stack!`, 'info'); + continue; + } + targetTaxonomy = this.sanitizeTaxonomyAttribs(targetTaxonomy); + this.taxonomiesMapper[taxonomy.uid] = targetTaxonomy; + this.progressManager?.tick(true, `taxonomy: ${taxonomy.uid}`, null, PROCESS_NAMES.TAXONOMIES_FETCH); + + const terms = await this.getAllTermsOfTaxonomy(targetTaxonomy); + if (Array.isArray(terms) && terms.length > 0) { + log(this.config, `Terms found for taxonomy '${taxonomy.uid}', processing...`, 'info'); + const sanitizedTerms = this.sanitizeTermsAttribs(terms); + this.termsMapper[taxonomy.uid] = sanitizedTerms; + this.progressManager?.tick(true, `terms: ${taxonomy.uid} (${terms.length})`, null, PROCESS_NAMES.TAXONOMIES_TERMS_FETCH); + } else { + log(this.config, `No terms found for taxonomy '${taxonomy.uid}', skipping...`, 'info'); + this.progressManager?.tick(true, `terms: ${taxonomy.uid} (none)`, null, PROCESS_NAMES.TAXONOMIES_TERMS_FETCH); + } + } catch (error) { + this.progressManager?.tick(false, `taxonomy: ${taxonomy.uid}`, error?.message || 'Failed to fetch', PROCESS_NAMES.TAXONOMIES_FETCH); } } } @@ -115,40 +155,49 @@ export default class TaxonomiesImportSetup { * Setup taxonomies using locale-based format (taxonomies organized by locale) * For locale-based structure, we query the target stack for each taxonomy+locale combination */ - async setupTaxonomiesByLocale(taxonomies: any): Promise { + async setupTaxonomiesByLocale(taxonomies: any, progress: any): Promise { const locales = this.loadAvailableLocales(); for (const localeCode of Object.keys(locales)) { log(this.config, `Processing taxonomies for locale: ${localeCode}`, 'info'); for (const taxonomy of Object.values(taxonomies) as any) { - // Query target stack for this taxonomy in this locale - let targetTaxonomy: any = await this.getTaxonomies(taxonomy, localeCode); - if (!targetTaxonomy) { - log(this.config, `Taxonomy '${taxonomy.uid}' not found in target stack for locale: ${localeCode}`, 'info'); - continue; - } - - targetTaxonomy = this.sanitizeTaxonomyAttribs(targetTaxonomy); - - // Store with composite key: taxonomyUID_locale - // const mapperKey = `${taxonomy.uid}_${localeCode}`; // TODO: Unsure about this required or not - this.taxonomiesMapper[taxonomy.uid] = targetTaxonomy; - const terms = await this.getAllTermsOfTaxonomy(targetTaxonomy, localeCode); - if (Array.isArray(terms) && terms.length > 0) { - log( - this.config, - `Terms found for taxonomy '${taxonomy.uid} for locale: ${localeCode}', processing...`, - 'info', - ); - const sanitizedTerms = this.sanitizeTermsAttribs(terms); - this.termsMapper[taxonomy.uid] = sanitizedTerms; - } else { - log( - this.config, - `No terms found for taxonomy '${taxonomy.uid} for locale: ${localeCode}', skipping...`, - 'info', - ); + try { + // Query target stack for this taxonomy in this locale + let targetTaxonomy: any = await this.getTaxonomies(taxonomy, localeCode); + if (!targetTaxonomy) { + this.progressManager?.tick(false, `taxonomy: ${taxonomy.uid} (${localeCode})`, 'Not found in stack', PROCESS_NAMES.TAXONOMIES_FETCH); + log(this.config, `Taxonomy '${taxonomy.uid}' not found in target stack for locale: ${localeCode}`, 'info'); + continue; + } + + targetTaxonomy = this.sanitizeTaxonomyAttribs(targetTaxonomy); + + // Store with composite key: taxonomyUID_locale + // const mapperKey = `${taxonomy.uid}_${localeCode}`; // TODO: Unsure about this required or not + this.taxonomiesMapper[taxonomy.uid] = targetTaxonomy; + this.progressManager?.tick(true, `taxonomy: ${taxonomy.uid} (${localeCode})`, null, PROCESS_NAMES.TAXONOMIES_FETCH); + + const terms = await this.getAllTermsOfTaxonomy(targetTaxonomy, localeCode); + if (Array.isArray(terms) && terms.length > 0) { + log( + this.config, + `Terms found for taxonomy '${taxonomy.uid} for locale: ${localeCode}', processing...`, + 'info', + ); + const sanitizedTerms = this.sanitizeTermsAttribs(terms); + this.termsMapper[taxonomy.uid] = sanitizedTerms; + this.progressManager?.tick(true, `terms: ${taxonomy.uid} (${localeCode}, ${terms.length})`, null, PROCESS_NAMES.TAXONOMIES_TERMS_FETCH); + } else { + log( + this.config, + `No terms found for taxonomy '${taxonomy.uid} for locale: ${localeCode}', skipping...`, + 'info', + ); + this.progressManager?.tick(true, `terms: ${taxonomy.uid} (${localeCode}, none)`, null, PROCESS_NAMES.TAXONOMIES_TERMS_FETCH); + } + } catch (error) { + this.progressManager?.tick(false, `taxonomy: ${taxonomy.uid} (${localeCode})`, error?.message || 'Failed to fetch', PROCESS_NAMES.TAXONOMIES_FETCH); } } } diff --git a/packages/contentstack-import-setup/src/types/import-config.ts b/packages/contentstack-import-setup/src/types/import-config.ts index cfa555c529..5c68eb773c 100644 --- a/packages/contentstack-import-setup/src/types/import-config.ts +++ b/packages/contentstack-import-setup/src/types/import-config.ts @@ -10,6 +10,18 @@ export interface ExternalConfig { password?: string; } +export interface Context { + command: string; + module: string; + userId: string | undefined; + email?: string | undefined; + sessionId: string | undefined; + clientId?: string | undefined; + apiKey: string; + orgId: string; + authenticationMethod?: string; +} + export default interface ImportConfig extends DefaultConfig, ExternalConfig { cliLogsPath?: string; contentDir: string; @@ -46,6 +58,8 @@ export default interface ImportConfig extends DefaultConfig, ExternalConfig { backupDir: string; createBackupDir?: string; region: any; + authenticationMethod?: string; + context?: Context; } type branch = { diff --git a/packages/contentstack-import-setup/src/utils/constants.ts b/packages/contentstack-import-setup/src/utils/constants.ts new file mode 100644 index 0000000000..014e7d2ae8 --- /dev/null +++ b/packages/contentstack-import-setup/src/utils/constants.ts @@ -0,0 +1,132 @@ +export const PROCESS_NAMES = { + // Entries module + ENTRIES_MAPPER_GENERATION: 'Mapper Generation', + + // Content Types module + CONTENT_TYPES_MAPPER_GENERATION: 'Mapper Generation', + CONTENT_TYPES_DEPENDENCY_SETUP: 'Dependency Setup', + + // Global Fields module + GLOBAL_FIELDS_MAPPER_GENERATION: 'Mapper Generation', + GLOBAL_FIELDS_DEPENDENCY_SETUP: 'Dependency Setup', + + // Extensions module + EXTENSIONS_MAPPER_GENERATION: 'Mapper Generation', + + // Assets module + ASSETS_MAPPER_GENERATION: 'Mapper Generation', + ASSETS_FETCH_AND_MAP: 'Fetch and Map', + + // Custom Roles module + CUSTOM_ROLES_MAPPER_GENERATION: 'Mapper Generation', + + // Marketplace Apps module + MARKETPLACE_APPS_MAPPER_GENERATION: 'Mapper Generation', + MARKETPLACE_APPS_FETCH: 'Fetch Apps', + + // Taxonomies module + TAXONOMIES_MAPPER_GENERATION: 'Mapper Generation', + TAXONOMIES_FETCH: 'Fetch Taxonomies', + TAXONOMIES_TERMS_FETCH: 'Fetch Terms', +} as const; + +export const MODULE_CONTEXTS = { + ENTRIES: 'entries', + CONTENT_TYPES: 'content-types', + GLOBAL_FIELDS: 'global-fields', + EXTENSIONS: 'extensions', + ASSETS: 'assets', + CUSTOM_ROLES: 'custom-roles', + MARKETPLACE_APPS: 'marketplace-apps', + TAXONOMIES: 'taxonomies', +} as const; + +// Display names for modules to avoid scattering user-facing strings +export const MODULE_NAMES = { + [MODULE_CONTEXTS.ENTRIES]: 'Entries', + [MODULE_CONTEXTS.CONTENT_TYPES]: 'Content Types', + [MODULE_CONTEXTS.GLOBAL_FIELDS]: 'Global Fields', + [MODULE_CONTEXTS.EXTENSIONS]: 'Extensions', + [MODULE_CONTEXTS.ASSETS]: 'Assets', + [MODULE_CONTEXTS.CUSTOM_ROLES]: 'Custom Roles', + [MODULE_CONTEXTS.MARKETPLACE_APPS]: 'Marketplace Apps', + [MODULE_CONTEXTS.TAXONOMIES]: 'Taxonomies', +} as const; + +export const PROCESS_STATUS: Record> = { + // Entries + ENTRIES_MAPPER_GENERATION: { + GENERATING: 'Generating mapper files...', + FAILED: 'Failed to generate mapper files.', + }, + + // Content Types + CONTENT_TYPES_MAPPER_GENERATION: { + GENERATING: 'Generating mapper files...', + FAILED: 'Failed to generate mapper files.', + }, + CONTENT_TYPES_DEPENDENCY_SETUP: { + SETTING_UP: 'Setting up dependencies...', + FAILED: 'Failed to setup dependencies.', + }, + + // Global Fields + GLOBAL_FIELDS_MAPPER_GENERATION: { + GENERATING: 'Generating mapper files...', + FAILED: 'Failed to generate mapper files.', + }, + GLOBAL_FIELDS_DEPENDENCY_SETUP: { + SETTING_UP: 'Setting up dependencies...', + FAILED: 'Failed to setup dependencies.', + }, + + // Extensions + EXTENSIONS_MAPPER_GENERATION: { + GENERATING: 'Generating mapper files...', + FAILED: 'Failed to generate mapper files.', + }, + + // Assets + ASSETS_MAPPER_GENERATION: { + GENERATING: 'Generating mapper files...', + FAILED: 'Failed to generate mapper files.', + }, + ASSETS_FETCH_AND_MAP: { + FETCHING: 'Fetching and mapping assets...', + FAILED: 'Failed to fetch and map assets.', + }, + + // Custom Roles + CUSTOM_ROLES_MAPPER_GENERATION: { + GENERATING: 'Generating mapper files...', + FAILED: 'Failed to generate mapper files.', + }, + + // Marketplace Apps + MARKETPLACE_APPS_MAPPER_GENERATION: { + GENERATING: 'Generating mapper files...', + FAILED: 'Failed to generate mapper files.', + }, + MARKETPLACE_APPS_FETCH: { + FETCHING: 'Fetching marketplace apps...', + FAILED: 'Failed to fetch marketplace apps.', + }, + + // Taxonomies + TAXONOMIES_MAPPER_GENERATION: { + GENERATING: 'Generating mapper files...', + FAILED: 'Failed to generate mapper files.', + }, + TAXONOMIES_FETCH: { + FETCHING: 'Fetching taxonomies...', + FAILED: 'Failed to fetch taxonomies.', + }, + TAXONOMIES_TERMS_FETCH: { + FETCHING: 'Fetching terms...', + FAILED: 'Failed to fetch terms.', + }, +} as const; + +export type ImportSetupProcessName = (typeof PROCESS_NAMES)[keyof typeof PROCESS_NAMES]; +export type ImportSetupModuleContext = (typeof MODULE_CONTEXTS)[keyof typeof MODULE_CONTEXTS]; + diff --git a/packages/contentstack-import-setup/src/utils/index.ts b/packages/contentstack-import-setup/src/utils/index.ts index e349a5225d..de35f72021 100644 --- a/packages/contentstack-import-setup/src/utils/index.ts +++ b/packages/contentstack-import-setup/src/utils/index.ts @@ -7,3 +7,4 @@ export { log, unlinkFileLogger } from './logger'; export * from './log'; export * from './common-helper'; export { setupBranchConfig } from './setup-branch'; +export { MODULE_CONTEXTS, MODULE_NAMES, PROCESS_NAMES, PROCESS_STATUS } from './constants'; diff --git a/packages/contentstack-import-setup/test/unit/modules/assets.test.ts b/packages/contentstack-import-setup/test/unit/modules/assets.test.ts index 0077e5fb39..bde556ac7f 100644 --- a/packages/contentstack-import-setup/test/unit/modules/assets.test.ts +++ b/packages/contentstack-import-setup/test/unit/modules/assets.test.ts @@ -81,14 +81,25 @@ describe('AssetImportSetup', () => { // }); it('should log success message after setup', async () => { + // Stub withLoadingSpinner to return a non-zero indexerCount to avoid early return + stub(assetSetup as any, 'withLoadingSpinner').resolves(1); // Stub fetchAndMapAssets to avoid actual implementation stub(assetSetup as any, 'fetchAndMapAssets').resolves(); + // Stub createNestedProgress and completeProgress to avoid progress manager issues + stub(assetSetup as any, 'createNestedProgress').returns({ + addProcess: stub().returnsThis(), + startProcess: stub().returnsThis(), + updateStatus: stub().returnsThis(), + completeProcess: stub().returnsThis(), + }); + stub(assetSetup as any, 'completeProgress'); await assetSetup.start(); - expect(logStub.calledOnce).to.be.true; - expect(logStub.firstCall.args[1]).to.include('successfully'); - expect(logStub.firstCall.args[2]).to.equal('success'); + expect(logStub.called).to.be.true; + const successCall = logStub.getCalls().find((call) => call.args[1]?.includes('successfully')); + expect(successCall).to.exist; + expect(successCall?.args[2]).to.equal('success'); }); it('should handle errors during start process', async () => { diff --git a/packages/contentstack-import-setup/test/unit/modules/base-setup.test.ts b/packages/contentstack-import-setup/test/unit/modules/base-setup.test.ts index 491cdac11b..620ea95da7 100644 --- a/packages/contentstack-import-setup/test/unit/modules/base-setup.test.ts +++ b/packages/contentstack-import-setup/test/unit/modules/base-setup.test.ts @@ -3,6 +3,7 @@ import { stub, restore, SinonStub } from 'sinon'; import BaseImportSetup from '../../../src/import/modules/base-setup'; import * as loggerModule from '../../../src/utils/logger'; import { ImportConfig } from '../../../src/types'; +import { CLIProgressManager, configHandler } from '@contentstack/cli-utilities'; describe('BaseImportSetup', () => { let baseSetup: BaseImportSetup; @@ -39,7 +40,7 @@ describe('BaseImportSetup', () => { baseSetup = new BaseImportSetup({ config: baseConfig as ImportConfig, stackAPIClient: mockStackAPIClient, - dependencies: {} as any, + dependencies: [] as any, }); }); @@ -50,7 +51,7 @@ describe('BaseImportSetup', () => { it('should initialize with the provided config and client', () => { expect(baseSetup.config).to.equal(baseConfig); expect(baseSetup.stackAPIClient).to.equal(mockStackAPIClient); - expect(baseSetup.dependencies).to.deep.equal({} as any); + expect(baseSetup.dependencies).to.deep.equal([] as any); }); it('should delay execution for specified milliseconds', async () => { @@ -119,4 +120,191 @@ describe('BaseImportSetup', () => { await baseSetup.logMsgAndWaitIfRequired('test-process', Date.now() - 500, 5, 3, false); expect(logStub.called).to.be.false; }); + + describe('Progress Manager', () => { + let configHandlerGetStub: SinonStub; + let createSimpleStub: SinonStub; + let createNestedStub: SinonStub; + let withLoadingSpinnerStub: SinonStub; + let mockProgressManager: any; + + beforeEach(() => { + mockProgressManager = { + tick: stub(), + complete: stub(), + addProcess: stub().returnsThis(), + startProcess: stub().returnsThis(), + updateStatus: stub().returnsThis(), + completeProcess: stub().returnsThis(), + }; + + configHandlerGetStub = stub(configHandler, 'get'); + createSimpleStub = stub(CLIProgressManager, 'createSimple'); + createNestedStub = stub(CLIProgressManager, 'createNested'); + withLoadingSpinnerStub = stub(CLIProgressManager, 'withLoadingSpinner'); + }); + + afterEach(() => { + restore(); + }); + + describe('createSimpleProgress', () => { + it('should create a simple progress manager with default showConsoleLogs', () => { + configHandlerGetStub.returns({}); + createSimpleStub.returns(mockProgressManager); + + const result = (baseSetup as any).createSimpleProgress('test-module', 100); + + expect(configHandlerGetStub.calledWith('log')).to.be.true; + expect(createSimpleStub.calledOnce).to.be.true; + expect(createSimpleStub.firstCall.args[0]).to.equal('test-module'); + expect(createSimpleStub.firstCall.args[1]).to.equal(100); + expect(createSimpleStub.firstCall.args[2]).to.equal(false); + expect(result).to.equal(mockProgressManager); + expect((baseSetup as any).currentModuleName).to.equal('test-module'); + expect((baseSetup as any).progressManager).to.equal(mockProgressManager); + }); + + it('should create a simple progress manager with showConsoleLogs enabled', () => { + configHandlerGetStub.returns({ showConsoleLogs: true }); + createSimpleStub.returns(mockProgressManager); + + const result = (baseSetup as any).createSimpleProgress('test-module', 50); + + expect(createSimpleStub.firstCall.args[2]).to.equal(true); + expect(result).to.equal(mockProgressManager); + }); + + it('should create a simple progress manager without total count', () => { + configHandlerGetStub.returns({}); + createSimpleStub.returns(mockProgressManager); + + (baseSetup as any).createSimpleProgress('test-module'); + + expect(createSimpleStub.firstCall.args[1]).to.be.undefined; + }); + }); + + describe('createNestedProgress', () => { + it('should create a nested progress manager with default showConsoleLogs', () => { + configHandlerGetStub.returns({}); + createNestedStub.returns(mockProgressManager); + + const result = (baseSetup as any).createNestedProgress('test-module'); + + expect(configHandlerGetStub.calledWith('log')).to.be.true; + expect(createNestedStub.calledOnce).to.be.true; + expect(createNestedStub.firstCall.args[0]).to.equal('test-module'); + expect(createNestedStub.firstCall.args[1]).to.equal(false); + expect(result).to.equal(mockProgressManager); + expect((baseSetup as any).currentModuleName).to.equal('test-module'); + expect((baseSetup as any).progressManager).to.equal(mockProgressManager); + }); + + it('should create a nested progress manager with showConsoleLogs enabled', () => { + configHandlerGetStub.returns({ showConsoleLogs: true }); + createNestedStub.returns(mockProgressManager); + + const result = (baseSetup as any).createNestedProgress('test-module'); + + expect(createNestedStub.firstCall.args[1]).to.equal(true); + expect(result).to.equal(mockProgressManager); + }); + }); + + describe('completeProgress', () => { + it('should complete progress manager with success', () => { + (baseSetup as any).progressManager = mockProgressManager; + + (baseSetup as any).completeProgress(true); + + expect(mockProgressManager.complete.calledOnce).to.be.true; + expect(mockProgressManager.complete.firstCall.args[0]).to.equal(true); + expect(mockProgressManager.complete.firstCall.args[1]).to.be.undefined; + expect((baseSetup as any).progressManager).to.be.null; + }); + + it('should complete progress manager with error', () => { + (baseSetup as any).progressManager = mockProgressManager; + const errorMessage = 'Test error message'; + + (baseSetup as any).completeProgress(false, errorMessage); + + expect(mockProgressManager.complete.calledOnce).to.be.true; + expect(mockProgressManager.complete.firstCall.args[0]).to.equal(false); + expect(mockProgressManager.complete.firstCall.args[1]).to.equal(errorMessage); + expect((baseSetup as any).progressManager).to.be.null; + }); + + it('should handle null progress manager gracefully', () => { + (baseSetup as any).progressManager = null; + + expect(() => (baseSetup as any).completeProgress(true)).to.not.throw(); + }); + }); + + describe('withLoadingSpinner', () => { + it('should execute action directly when showConsoleLogs is enabled', async () => { + configHandlerGetStub.returns({ showConsoleLogs: true }); + const action = stub().resolves('result'); + + const result = await (baseSetup as any).withLoadingSpinner('Loading...', action); + + expect(action.calledOnce).to.be.true; + expect(withLoadingSpinnerStub.called).to.be.false; + expect(result).to.equal('result'); + }); + + it('should use CLIProgressManager.withLoadingSpinner when showConsoleLogs is disabled', async () => { + configHandlerGetStub.returns({ showConsoleLogs: false }); + const action = stub().resolves('result'); + withLoadingSpinnerStub.resolves('result'); + + const result = await (baseSetup as any).withLoadingSpinner('Loading...', action); + + expect(withLoadingSpinnerStub.calledOnce).to.be.true; + expect(withLoadingSpinnerStub.firstCall.args[0]).to.equal('Loading...'); + expect(withLoadingSpinnerStub.firstCall.args[1]).to.equal(action); + expect(result).to.equal('result'); + }); + + it('should use CLIProgressManager.withLoadingSpinner when log config is empty', async () => { + configHandlerGetStub.returns({}); + const action = stub().resolves('result'); + withLoadingSpinnerStub.resolves('result'); + + const result = await (baseSetup as any).withLoadingSpinner('Loading...', action); + + expect(withLoadingSpinnerStub.calledOnce).to.be.true; + expect(result).to.equal('result'); + }); + + it('should handle errors in action when showConsoleLogs is enabled', async () => { + configHandlerGetStub.returns({ showConsoleLogs: true }); + const error = new Error('Action failed'); + const action = stub().rejects(error); + + try { + await (baseSetup as any).withLoadingSpinner('Loading...', action); + expect.fail('Should have thrown an error'); + } catch (e) { + expect(e).to.equal(error); + } + }); + + it('should handle errors in action when showConsoleLogs is disabled', async () => { + configHandlerGetStub.returns({ showConsoleLogs: false }); + const error = new Error('Action failed'); + const action = stub().rejects(error); + withLoadingSpinnerStub.rejects(error); + + try { + await (baseSetup as any).withLoadingSpinner('Loading...', action); + expect.fail('Should have thrown an error'); + } catch (e) { + expect(e).to.equal(error); + } + }); + }); + }); }); diff --git a/packages/contentstack-import-setup/test/unit/modules/content-types.test.ts b/packages/contentstack-import-setup/test/unit/modules/content-types.test.ts index 4ff5a27cf2..8380440ce3 100644 --- a/packages/contentstack-import-setup/test/unit/modules/content-types.test.ts +++ b/packages/contentstack-import-setup/test/unit/modules/content-types.test.ts @@ -40,11 +40,19 @@ describe('ContentTypesImportSetup', () => { contentTypesSetup = new ContentTypesImportSetup({ config: baseConfig as ImportConfig, stackAPIClient: mockStackAPIClient, - dependencies: {} as any, + dependencies: ['extensions'] as any, }); // Stub the setupDependencies method to avoid actual imports setupDependenciesStub = stub(contentTypesSetup, 'setupDependencies').resolves(); + // Stub createNestedProgress and completeProgress to avoid progress manager issues + stub(contentTypesSetup as any, 'createNestedProgress').returns({ + addProcess: stub().returnsThis(), + startProcess: stub().returnsThis(), + updateStatus: stub().returnsThis(), + completeProcess: stub().returnsThis(), + }); + stub(contentTypesSetup as any, 'completeProgress'); }); afterEach(() => { @@ -54,7 +62,7 @@ describe('ContentTypesImportSetup', () => { it('should initialize with the provided config and client', () => { expect(contentTypesSetup.config).to.equal(baseConfig); expect(contentTypesSetup.stackAPIClient).to.equal(mockStackAPIClient); - expect(contentTypesSetup.dependencies).to.deep.equal({} as any); + expect(contentTypesSetup.dependencies).to.deep.equal(['extensions'] as any); }); it('should call setupDependencies during start', async () => { @@ -75,9 +83,10 @@ describe('ContentTypesImportSetup', () => { await contentTypesSetup.start(); - expect(logStub.calledOnce).to.be.true; - expect(logStub.firstCall.args[1]).to.include('Error occurred'); - expect(logStub.firstCall.args[1]).to.include('Test error'); - expect(logStub.firstCall.args[2]).to.equal('error'); + expect(logStub.called).to.be.true; + const errorCall = logStub.getCalls().find((call) => call.args[1]?.includes('Error occurred')); + expect(errorCall).to.exist; + expect(errorCall?.args[1]).to.include('Test error'); + expect(errorCall?.args[2]).to.equal('error'); }); }); diff --git a/packages/contentstack-import-setup/test/unit/modules/global-fields.test.ts b/packages/contentstack-import-setup/test/unit/modules/global-fields.test.ts index 25e7316414..1416c7fb59 100644 --- a/packages/contentstack-import-setup/test/unit/modules/global-fields.test.ts +++ b/packages/contentstack-import-setup/test/unit/modules/global-fields.test.ts @@ -40,11 +40,19 @@ describe('GlobalFieldsImportSetup', () => { globalFieldsSetup = new GlobalFieldsImportSetup({ config: baseConfig as ImportConfig, stackAPIClient: mockStackAPIClient, - dependencies: {} as any, + dependencies: ['extensions'] as any, }); // Stub the setupDependencies method to avoid actual imports setupDependenciesStub = stub(globalFieldsSetup, 'setupDependencies').resolves(); + // Stub createNestedProgress and completeProgress to avoid progress manager issues + stub(globalFieldsSetup as any, 'createNestedProgress').returns({ + addProcess: stub().returnsThis(), + startProcess: stub().returnsThis(), + updateStatus: stub().returnsThis(), + completeProcess: stub().returnsThis(), + }); + stub(globalFieldsSetup as any, 'completeProgress'); }); afterEach(() => { @@ -54,7 +62,7 @@ describe('GlobalFieldsImportSetup', () => { it('should initialize with the provided config and client', () => { expect((globalFieldsSetup as any).config).to.equal(baseConfig); expect((globalFieldsSetup as any).stackAPIClient).to.equal(mockStackAPIClient); - expect((globalFieldsSetup as any).dependencies).to.deep.equal({} as any); + expect((globalFieldsSetup as any).dependencies).to.deep.equal(['extensions'] as any); }); it('should call setupDependencies during start', async () => { @@ -75,9 +83,10 @@ describe('GlobalFieldsImportSetup', () => { await globalFieldsSetup.start(); - expect(logStub.calledOnce).to.be.true; - expect(logStub.firstCall.args[1]).to.include('Error occurred'); - expect(logStub.firstCall.args[1]).to.include('Test error'); - expect(logStub.firstCall.args[2]).to.equal('error'); + expect(logStub.called).to.be.true; + const errorCall = logStub.getCalls().find((call) => call.args[1]?.includes('Error occurred')); + expect(errorCall).to.exist; + expect(errorCall?.args[1]).to.include('Test error'); + expect(errorCall?.args[2]).to.equal('error'); }); }); diff --git a/packages/contentstack-utilities/src/constants/logging.ts b/packages/contentstack-utilities/src/constants/logging.ts index b553006701..ac2e09eecc 100644 --- a/packages/contentstack-utilities/src/constants/logging.ts +++ b/packages/contentstack-utilities/src/constants/logging.ts @@ -16,4 +16,4 @@ export const levelColors = { debug: 'blue', }; -export const PROGRESS_SUPPORTED_MODULES = ['export', 'import'] as const; +export const PROGRESS_SUPPORTED_MODULES = ['export', 'import','import-setup'] as const;