From 0e2eeb2695196051ea0c80bb86c41540a1222868 Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Wed, 20 Aug 2025 16:23:35 +0530 Subject: [PATCH 1/2] feat: Integrated CLIProgressManager and SummaryManager in personalize & variant-entries --- .../src/import/modules/entries.ts | 7 +- .../src/import/modules/extensions.ts | 10 +- .../src/import/modules/locales.ts | 14 +- .../src/import/modules/personalize.ts | 133 +++++++------- .../src/import/modules/stack.ts | 2 +- .../src/import/modules/taxonomies.ts | 2 +- .../src/import/modules/variant-entries.ts | 2 +- .../src/import/modules/webhooks.ts | 10 +- .../src/import/modules/workflows.ts | 16 +- .../src/utils/strategy-registrations.ts | 16 +- .../src/export/projects.ts | 3 +- .../src/export/variant-entries.ts | 33 +++- .../src/import/attribute.ts | 150 +++++++++++----- .../src/import/audiences.ts | 164 ++++++++++++------ .../src/import/events.ts | 142 ++++++++++----- .../src/import/experiences.ts | 150 +++++++++++----- .../src/import/project.ts | 127 ++++++++++---- .../src/import/variant-entries.ts | 83 ++++++--- 18 files changed, 701 insertions(+), 363 deletions(-) diff --git a/packages/contentstack-import/src/import/modules/entries.ts b/packages/contentstack-import/src/import/modules/entries.ts index 59b1867898..d897c0235f 100644 --- a/packages/contentstack-import/src/import/modules/entries.ts +++ b/packages/contentstack-import/src/import/modules/entries.ts @@ -149,9 +149,7 @@ export default class EntriesImport extends BaseClass { progress.completeProcess('Reference Updates', true); // Step 5: Restore content types - progress - .startProcess('CT Restoration') - .updateStatus('Restoring content type references...', 'CT Restoration'); + progress.startProcess('CT Restoration').updateStatus('Restoring content type references...', 'CT Restoration'); await this.enableMandatoryCTReferences(); progress.completeProcess('CT Restoration', true); @@ -523,6 +521,7 @@ export default class EntriesImport extends BaseClass { log.debug(`Found content type schema for ${cTUid}`, this.importConfig.context); const onSuccess = ({ response, apiData: entry, additionalInfo }: any) => { + this.progressManager?.tick(true, `${entry?.title} - ${entry?.uid}`, null, 'Create'); if (additionalInfo[entry.uid]?.isLocalized) { let oldUid = additionalInfo[entry.uid].entryOldUid; this.entriesForVariant.push({ content_type: cTUid, entry_uid: oldUid, locale }); @@ -556,6 +555,7 @@ export default class EntriesImport extends BaseClass { const onReject = ({ error, apiData: entry, additionalInfo }: any) => { const { title, uid } = entry; + this.progressManager?.tick(false, `${title} - ${uid}`, 'Error while creating entries', 'Create'); this.entriesForVariant = this.entriesForVariant.filter( (item) => !(item.locale === locale && item.entry_uid === uid), ); @@ -613,7 +613,6 @@ export default class EntriesImport extends BaseClass { entriesCreateFileHelper?.completeFile(true); existingEntriesFileHelper?.completeFile(true); - this.progressManager?.tick(true, `${cTUid} - ${locale}`, null, 'Create'); log.success(`Created entries for content type ${cTUid} in locale ${locale}`, this.importConfig.context); } diff --git a/packages/contentstack-import/src/import/modules/extensions.ts b/packages/contentstack-import/src/import/modules/extensions.ts index 0c1c09da52..0cec3980e2 100644 --- a/packages/contentstack-import/src/import/modules/extensions.ts +++ b/packages/contentstack-import/src/import/modules/extensions.ts @@ -71,10 +71,10 @@ export default class ImportExtensions extends BaseClass { this.updateUidExtension(); if (this.importConfig.replaceExisting && this.existingExtensions.length > 0) { - progress.addProcess('Update', this.existingExtensions.length); - progress.startProcess('Update').updateStatus('Updating existing extensions...', 'Update'); + progress.addProcess('Replace existing', this.existingExtensions.length); + progress.startProcess('Replace existing').updateStatus('Updating existing extensions...', 'Replace existing'); await this.replaceExtensions(); - progress.completeProcess('Update', true); + progress.completeProcess('Replace existing', true); } await this.processExtensionResults(); @@ -167,7 +167,7 @@ export default class ImportExtensions extends BaseClass { const onSuccess = ({ response, apiData: { uid, title } = { uid: null, title: '' } }: any) => { this.extSuccess.push(response); this.extUidMapper[uid] = response.uid; - this.progressManager?.tick(true, `extension: ${title || uid} (updated)`, null, 'Update'); + this.progressManager?.tick(true, `extension: ${title || uid} (updated)`, null, 'Replace existing'); log.success(`Extension '${title}' updated successfully`, this.importConfig.context); log.debug(`Extension update completed: ${title} (${uid})`, this.importConfig.context); fsUtil.writeFile(this.extUidMapperPath, this.extUidMapper); @@ -180,7 +180,7 @@ export default class ImportExtensions extends BaseClass { false, `extension: ${title || uid}`, error?.message || 'Failed to update extension', - 'Update', + 'Replace existing', ); log.debug(`Extension '${title}' update failed`, this.importConfig.context); handleAndLogError(error, { ...this.importConfig.context, title }, `Extension '${title}' failed to be updated`); diff --git a/packages/contentstack-import/src/import/modules/locales.ts b/packages/contentstack-import/src/import/modules/locales.ts index 2e07073e35..c262d007c6 100644 --- a/packages/contentstack-import/src/import/modules/locales.ts +++ b/packages/contentstack-import/src/import/modules/locales.ts @@ -130,12 +130,7 @@ export default class ImportLocales extends BaseClass { }; const onReject = ({ error, apiData: { uid, code } = undefined }: any) => { - this.progressManager?.tick( - false, - `locale: ${code}`, - error?.message || 'Failed to create locale', - 'Create', - ); + this.progressManager?.tick(false, `locale: ${code}`, error?.message || 'Failed to create locale', 'Create'); if (error?.errorCode === 247) { log.info(formatError(error), this.config.context); } else { @@ -164,10 +159,12 @@ export default class ImportLocales extends BaseClass { const onSuccess = ({ response = {}, apiData: { uid, code } = undefined }: any) => { log.info(`Updated locale: '${code}'`, this.config.context); log.debug(`Locale update completed for: ${code}`, this.config.context); + this.progressManager?.tick(true, `locale: ${code}`, null, 'Update'); fsUtil.writeFile(this.langSuccessPath, this.createdLocales); }; const onReject = ({ error, apiData: { uid, code } = undefined }: any) => { + this.progressManager?.tick(false, `locale: ${code}`, 'Failed to update locale', 'Update'); log.error(`Language '${code}' failed to update`, this.config.context); handleAndLogError(error, { ...this.config.context, code }); fsUtil.writeFile(this.langFailsPath, this.failedLocales); @@ -304,7 +301,10 @@ export default class ImportLocales extends BaseClass { const message = `master locale: codes differ (${sourceCode} vs ${targetCode})`; this.tickProgress(true, message); - log.debug(`Master Locale language codes do not match. Source: ${sourceCode}, Target: ${targetCode}`, this.config.context); + log.debug( + `Master Locale language codes do not match. Source: ${sourceCode}, Target: ${targetCode}`, + this.config.context, + ); } private async handleNameMismatch(source: Record, target: Record): Promise { diff --git a/packages/contentstack-import/src/import/modules/personalize.ts b/packages/contentstack-import/src/import/modules/personalize.ts index c9d5a31e30..3e4444b9fe 100644 --- a/packages/contentstack-import/src/import/modules/personalize.ts +++ b/packages/contentstack-import/src/import/modules/personalize.ts @@ -7,6 +7,13 @@ export default class ImportPersonalize extends BaseClass { private config: ImportConfig; public personalizeConfig: ImportConfig['modules']['personalize']; + private readonly moduleDisplayMapper = { + events: 'Events', + attributes: 'Attributes', + audiences: 'Audiences', + experiences: 'Experiences', + }; + constructor({ importConfig, stackAPIClient }: ModuleClassParams) { super({ importConfig, stackAPIClient }); this.config = importConfig; @@ -29,30 +36,23 @@ export default class ImportPersonalize extends BaseClass { } const progress = this.createNestedProgress(this.currentModuleName); - progress.addProcess('Project Import', 1); - if (this.personalizeConfig.importData && modulesCount > 0) { - progress.addProcess('Personalize data import', modulesCount); - } + this.addProjectProcess(progress); + this.addModuleProcesses(progress, modulesCount); // Step 1: Import personalize project - progress.startProcess('Project Import').updateStatus('Importing personalize project...', 'Project Import'); - log.info('Starting personalize project import', this.config.context); - await this.importPersonalizeProject(progress); - progress.completeProcess('Project Import', true); + await this.importProjects(progress); // Step 2: Import personalize data modules (if enabled) if (this.personalizeConfig.importData && modulesCount > 0) { - progress - .startProcess('Personalize data import') - .updateStatus('Importing personalize data modules...', 'Personalize data import'); - log.info('Starting personalize data import', this.config.context); - await this.importPersonalizeData(progress); - progress.completeProcess('Personalize data import', true); + log.debug('Processing personalize modules...', this.config.context); + await this.importModules(progress); + } else { + log.debug('No personalize modules configured for processing', this.config.context); } this.completeProgress(true); - log.success('Personalize import completed successfully', this.config.context) + log.success('Personalize import completed successfully', this.config.context); } catch (error) { this.personalizeConfig.importData = false; // Stop personalize import if project creation fails this.completeProgress(false, (error as any)?.message || 'Personalize import failed'); @@ -64,25 +64,40 @@ export default class ImportPersonalize extends BaseClass { } } - private async importPersonalizeProject(parentProgress: any): Promise { - log.debug('Starting personalize project import', this.config.context); - log.debug(`Base URL: ${this.personalizeConfig.baseURL[this.config.region.name]}`, this.config.context); + private addProjectProcess(progress: any) { + progress.addProcess('Projects', 1); + log.debug('Added Projects process to personalize progress', this.config.context); + } + + private addModuleProcesses(progress: any, moduleCount: number) { + if (moduleCount > 0) { + const order: (keyof typeof this.moduleDisplayMapper)[] = this.personalizeConfig + .importOrder as (keyof typeof this.moduleDisplayMapper)[]; - // Create project instance and set parent progress manager - const projectInstance = new Import.Project(this.config); - if (projectInstance.setParentProgressManager) { - projectInstance.setParentProgressManager(parentProgress); + log.debug(`Adding ${order.length} personalize module processes: ${order.join(', ')}`, this.config.context); + + for (const module of order) { + const processName = this.moduleDisplayMapper[module]; + progress.addProcess(processName, 1); + log.debug(`Added ${processName} process to personalize progress`, this.config.context); + } + } else { + log.debug('No personalize modules to add to progress', this.config.context); } + } + + private async importProjects(progress: any): Promise { + progress.startProcess('Projects').updateStatus('Importing personalization projects...', 'Projects'); + log.debug('Starting projects import for personalization...', this.config.context); + const projectInstance = new Import.Project(this.config); + projectInstance.setParentProgressManager(progress); await projectInstance.import(); - parentProgress?.tick(true, 'personalize project', null, 'Project Import'); - log.debug('Personalize project import completed', this.config.context); + progress.completeProcess('Projects', true); } - private async importPersonalizeData(parentProgress: any): Promise { - log.debug('Personalize data import is enabled', this.config.context); - + private async importModules(progress: any): Promise { const moduleMapper = { events: Import.Events, audiences: Import.Audiences, @@ -92,46 +107,36 @@ export default class ImportPersonalize extends BaseClass { const order: (keyof typeof moduleMapper)[] = this.personalizeConfig.importOrder as (keyof typeof moduleMapper)[]; - log.debug(`Processing ${order.length} personalize modules in order: ${order.join(', ')}`, this.config.context); + log.debug(`Personalize import order: ${order.join(', ')}`, this.config.context); for (const module of order) { - log.debug(`Starting import for personalize module: ${module}`, this.config.context); - const Module = moduleMapper[module]; - - if (!Module) { - parentProgress?.tick( - false, - `module: ${module}`, - 'Module not found in moduleMapper', - 'Personalize data import', - ); - log.debug(`Module ${module} not found in moduleMapper`, this.config.context); - continue; - } - - try { - log.debug(`Creating instance of ${module} module`, this.config.context); - const moduleInstance = new Module(this.config); - - // Set parent progress manager for sub-module - if (moduleInstance.setParentProgressManager) { - moduleInstance.setParentProgressManager(parentProgress); + log.debug(`Processing personalize module: ${module}`, this.config.context); + const processName = this.moduleDisplayMapper[module]; + const ModuleClass = moduleMapper[module]; + + if (ModuleClass) { + progress.startProcess(processName).updateStatus(`Importing ${module}...`, processName); + log.debug(`Starting import for module: ${module}`, this.config.context); + + if (this.personalizeConfig.importData) { + const importer = new ModuleClass(this.config); + importer.setParentProgressManager(progress); + await importer.import(); + + progress.completeProcess(processName, true); + log.debug(`Completed import for module: ${module}`, this.config.context); + } else { + log.debug(`Skipping ${module} - personalization not enabled`, this.config.context); + this.progressManager?.tick(true, `${module} skipped (no project)`, null, processName); + progress.completeProcess(processName, true); + log.info(`Skipped ${module} import - no personalize project found`, this.config.context); } - - log.debug(`Importing ${module} module`, this.config.context); - await moduleInstance.import(); - - parentProgress?.tick(true, `module: ${module}`, null, 'Personalize data import'); - log.success(`Successfully imported personalize module: ${module}`, this.config.context); - } catch (error) { - parentProgress?.tick( - false, - `module: ${module}`, - (error as any)?.message || 'Import failed', - 'Personalize data import', - ); - log.debug(`Failed to import personalize module: ${module} - ${(error as any)?.message}`, this.config.context); - handleAndLogError(error, { ...this.config.context, module }); + } else { + log.debug(`Module not implemented: ${module}`, this.config.context); + progress.startProcess(processName).updateStatus(`Module not implemented: ${module}`, processName); + this.progressManager?.tick(false, `module: ${module}`, 'Module not implemented', processName); + progress.completeProcess(processName, false); + log.info(`Module not implemented: ${module}`, this.config.context); } } diff --git a/packages/contentstack-import/src/import/modules/stack.ts b/packages/contentstack-import/src/import/modules/stack.ts index 3e04b683c2..26010dbba9 100644 --- a/packages/contentstack-import/src/import/modules/stack.ts +++ b/packages/contentstack-import/src/import/modules/stack.ts @@ -13,7 +13,7 @@ export default class ImportStack extends BaseClass { constructor({ importConfig, stackAPIClient }: ModuleClassParams) { super({ importConfig, stackAPIClient }); this.importConfig.context.module = 'stack'; - this.currentModuleName = 'Stack Settings'; + this.currentModuleName = 'Stack'; this.stackSettingsPath = join(this.importConfig.backupDir, 'stack', 'settings.json'); this.envUidMapperPath = join(this.importConfig.backupDir, 'mapper', 'environments', 'uid-mapping.json'); } diff --git a/packages/contentstack-import/src/import/modules/taxonomies.ts b/packages/contentstack-import/src/import/modules/taxonomies.ts index b8e53cb898..ab9410015a 100644 --- a/packages/contentstack-import/src/import/modules/taxonomies.ts +++ b/packages/contentstack-import/src/import/modules/taxonomies.ts @@ -58,7 +58,7 @@ export default class ImportTaxonomies extends BaseClass { this.createSuccessAndFailedFile(); this.completeProgress(true); - log.success('Taxonomies imported successfully!', this.importConfig.context); + log.success('Taxonomies imported successfully!', this.importConfig.context); } catch (error) { this.completeProgress(false, error?.message || 'Taxonomies import failed'); handleAndLogError(error, { ...this.importConfig.context }); diff --git a/packages/contentstack-import/src/import/modules/variant-entries.ts b/packages/contentstack-import/src/import/modules/variant-entries.ts index daf2f567be..03047f6537 100644 --- a/packages/contentstack-import/src/import/modules/variant-entries.ts +++ b/packages/contentstack-import/src/import/modules/variant-entries.ts @@ -48,7 +48,7 @@ export default class ImportVariantEntries extends BaseClass { return; } - const progress = this.createSimpleProgress(this.currentModuleName, 1); + const progress = this.createSimpleProgress(this.currentModuleName); progress.updateStatus('Importing variant entries...'); log.info('Starting variant entries import process', this.config.context); diff --git a/packages/contentstack-import/src/import/modules/webhooks.ts b/packages/contentstack-import/src/import/modules/webhooks.ts index 16186830c5..6426fff4de 100644 --- a/packages/contentstack-import/src/import/modules/webhooks.ts +++ b/packages/contentstack-import/src/import/modules/webhooks.ts @@ -46,7 +46,7 @@ export default class ImportWebhooks extends BaseClass { const [webhooksCount] = await this.analyzeWebhooks(); if (webhooksCount === 0) { - log.info(`No Webhooks Found - '${this.webhooksFolderPath}'`, this.importConfig.context); + log.info(`No Webhooks Found - '${this.webhooksFolderPath}'`, this.importConfig.context); return; } @@ -59,7 +59,7 @@ export default class ImportWebhooks extends BaseClass { this.processWebhookResults(); this.completeProgress(true); - log.success('Webhooks have been imported successfully!', this.importConfig.context); + log.success('Webhooks have been imported successfully!', this.importConfig.context); } catch (error) { this.completeProgress(false, error?.message || 'Webhooks import failed'); handleAndLogError(error, { ...this.importConfig.context }); @@ -95,11 +95,7 @@ export default class ImportWebhooks extends BaseClass { log.info(`Webhook '${name}' already exists`, this.importConfig.context); } else { this.failedWebhooks.push(apiData); - this.progressManager?.tick( - false, - `webhook: ${name || uid}`, - error?.message || 'Failed to import webhook', - ); + this.progressManager?.tick(false, `webhook: ${name || uid}`, error?.message || 'Failed to import webhook'); handleAndLogError( error, { ...this.importConfig.context, webhookName: name }, diff --git a/packages/contentstack-import/src/import/modules/workflows.ts b/packages/contentstack-import/src/import/modules/workflows.ts index 7da85e7282..b0059026bf 100644 --- a/packages/contentstack-import/src/import/modules/workflows.ts +++ b/packages/contentstack-import/src/import/modules/workflows.ts @@ -60,7 +60,7 @@ export default class ImportWorkflows extends BaseClass { const progress = this.createNestedProgress(this.currentModuleName); progress.addProcess('Get Roles', 1); - progress.addProcess('Workflows Import', workflowsCount); + progress.addProcess('Create', workflowsCount); await this.prepareWorkflowMapper(); @@ -71,10 +71,10 @@ export default class ImportWorkflows extends BaseClass { progress.completeProcess('Get Roles', true); // Step 2: Import workflows - progress.startProcess('Workflows Import').updateStatus('Importing workflows...', 'Workflows Import'); + progress.startProcess('Create').updateStatus('Importing workflows...', 'Create'); log.info('Starting workflows import process', this.importConfig.context); await this.importWorkflows(); - progress.completeProcess('Workflows Import', true); + progress.completeProcess('Create', true); this.processWorkflowResults(); @@ -144,7 +144,7 @@ export default class ImportWorkflows extends BaseClass { false, `workflow: ${name || uid}`, error?.message || 'Failed to update next available stages', - 'Workflows Import', + 'Create', ); handleAndLogError(error, { ...this.importConfig.context, name }, `Workflow '${name}' update failed`); }); @@ -157,7 +157,7 @@ export default class ImportWorkflows extends BaseClass { this.createdWorkflows.push(response); this.workflowUidMapper[uid] = response.uid; - this.progressManager?.tick(true, `workflow: ${name || uid}`, null, 'Workflows Import'); + this.progressManager?.tick(true, `workflow: ${name || uid}`, null, 'Create'); log.success(`Workflow '${name}' imported successfully`, this.importConfig.context); log.debug(`Workflow UID mapping: ${uid} → ${response.uid}`, this.importConfig.context); fsUtil.writeFile(this.workflowUidMapperPath, this.workflowUidMapper); @@ -170,7 +170,7 @@ export default class ImportWorkflows extends BaseClass { const workflowExists = err?.errors?.name || err?.errors?.['workflow.name']; if (workflowExists) { - this.progressManager?.tick(true, `workflow: ${name || uid} (already exists)`, null, 'Workflows Import'); + this.progressManager?.tick(true, `workflow: ${name || uid} (already exists)`, null, 'Create'); log.info(`Workflow '${name}' already exists`, this.importConfig.context); } else { this.failedWebhooks.push(apiData); @@ -178,7 +178,7 @@ export default class ImportWorkflows extends BaseClass { false, `workflow: ${name || uid}`, error?.message || 'Failed to import workflow', - 'Workflows Import', + 'Create', ); if (error.errors['workflow_stages.0.users']) { log.error( @@ -260,7 +260,7 @@ export default class ImportWorkflows extends BaseClass { true, `workflow: ${workflow.name} (skipped - already exists)`, null, - 'Workflows Import', + 'Create', ); apiOptions.entity = undefined; } else { diff --git a/packages/contentstack-import/src/utils/strategy-registrations.ts b/packages/contentstack-import/src/utils/strategy-registrations.ts index a7657d0637..1811430673 100644 --- a/packages/contentstack-import/src/utils/strategy-registrations.ts +++ b/packages/contentstack-import/src/utils/strategy-registrations.ts @@ -20,13 +20,13 @@ ProgressStrategyRegistry.register( // Register strategy for Assets - use Asset Upload as primary process ProgressStrategyRegistry.register( 'ASSETS', - new PrimaryProcessStrategy('Asset Upload') + new PrimaryProcessStrategy('Upload') ); // Register strategy for Entries - use Entry Creation as primary process ProgressStrategyRegistry.register( 'ENTRIES', - new PrimaryProcessStrategy('Entry Creation') + new PrimaryProcessStrategy('Create') ); // Register strategy for Global Fields - use Create as primary process @@ -50,13 +50,13 @@ ProgressStrategyRegistry.register( // Register strategy for Locales - uses default (no nested progress yet) ProgressStrategyRegistry.register( 'LOCALES', - new DefaultProgressStrategy() + new PrimaryProcessStrategy('Create') ); // Register strategy for Labels - uses default (no nested progress yet) ProgressStrategyRegistry.register( 'LABELS', - new DefaultProgressStrategy() + new PrimaryProcessStrategy('Create') ); // Register strategy for Webhooks - uses default (no nested progress yet) @@ -68,7 +68,7 @@ ProgressStrategyRegistry.register( // Register strategy for Workflows - uses default (no nested progress yet) ProgressStrategyRegistry.register( 'WORKFLOWS', - new DefaultProgressStrategy() + new PrimaryProcessStrategy('Create') ); // Register strategy for Custom Roles - uses default (no nested progress yet) @@ -91,7 +91,7 @@ ProgressStrategyRegistry.register( // Register strategy for Stack Settings - simple module ProgressStrategyRegistry.register( - 'STACK SETTINGS', + 'STACK', new DefaultProgressStrategy() ); @@ -100,7 +100,7 @@ ProgressStrategyRegistry.register( 'PERSONALIZE', new CustomProgressStrategy((processes) => { // For personalize import, count project imports as primary metric - const projectImport = processes.get('Project Import'); + const projectImport = processes.get('Project'); if (projectImport) { return { total: projectImport.total, @@ -126,7 +126,7 @@ ProgressStrategyRegistry.register( // Register strategy for Variant Entries - sub-process of entries ProgressStrategyRegistry.register( 'VARIANT ENTRIES', - new DefaultProgressStrategy() // Uses default since it's a sub-process + new DefaultProgressStrategy() ); export default ProgressStrategyRegistry; \ No newline at end of file diff --git a/packages/contentstack-variants/src/export/projects.ts b/packages/contentstack-variants/src/export/projects.ts index b3efa561cb..7566e8f963 100644 --- a/packages/contentstack-variants/src/export/projects.ts +++ b/packages/contentstack-variants/src/export/projects.ts @@ -67,9 +67,10 @@ export default class ExportProjects extends PersonalizationAdapter if (this.parentProgressManager) { progress = this.parentProgressManager; this.progressManager = this.parentProgressManager; + progress.updateProcessTotal('Projects', this.projectsData?.length); } else { progress = this.createNestedProgress('Projects'); - progress.addProcess('Projects', 1); + progress.addProcess('Projects', this.projectsData?.length); progress.startProcess('Projects').updateStatus('Processing and exporting project data...', 'Projects'); } diff --git a/packages/contentstack-variants/src/export/variant-entries.ts b/packages/contentstack-variants/src/export/variant-entries.ts index a83440f439..5cad4994e4 100644 --- a/packages/contentstack-variants/src/export/variant-entries.ts +++ b/packages/contentstack-variants/src/export/variant-entries.ts @@ -71,6 +71,9 @@ export default class VariantEntries extends VariantAdapter { private attributesUidMapper: Record; private personalizeConfig: ImportConfig['modules']['personalize']; private attributeConfig: ImportConfig['modules']['personalize']['attributes']; + private attributeData: AttributeStruct[]; - constructor(public readonly config: ImportConfig) { + constructor(public readonly config: ImportConfig) { const conf: APIConfig = { config, baseURL: config.modules.personalize.baseURL[config.region.name], headers: { 'X-Project-Uid': config.modules.personalize.project_id }, }; super(Object.assign(config, conf)); - + this.personalizeConfig = this.config.modules.personalize; this.attributeConfig = this.personalizeConfig.attributes; this.mapperDirPath = resolve( @@ -31,61 +32,114 @@ export default class Attribute extends PersonalizationAdapter { this.attributesUidMapperPath = resolve(sanitizePath(this.attrMapperDirPath), 'uid-mapping.json'); this.attributesUidMapper = {}; this.config.context.module = 'attributes'; + this.attributeData = []; } /** * The function asynchronously imports attributes from a JSON file and creates them in the system. */ - async import() { - await this.init(); - await fsUtil.makeDirectory(this.attrMapperDirPath); - log.debug(`Created mapper directory: ${this.attrMapperDirPath}`, this.config.context); - - const { dirName, fileName } = this.attributeConfig; - const attributesPath = resolve( - sanitizePath(this.config.data), - sanitizePath(this.personalizeConfig.dirName), - sanitizePath(dirName), - sanitizePath(fileName), - ); + async import() { + try { + log.debug('Starting attributes import...', this.config.context); - log.debug(`Checking for attributes file: ${attributesPath}`, this.config.context); - - if (existsSync(attributesPath)) { - try { - const attributes = fsUtil.readFile(attributesPath, true) as AttributeStruct[]; - log.info(`Found ${attributes.length} attributes to import`, this.config.context); - - for (const attribute of attributes) { - const { key, name, description, uid } = attribute; - log.debug(`Processing attribute: ${name} - ${attribute.__type}`, this.config.context); - - // skip creating preset attributes, as they are already present in the system - if (attribute.__type === 'PRESET') { - log.debug(`Skipping preset attribute: ${name}`, this.config.context); - continue; - } - - try { - log.debug(`Creating custom attribute: ${name}`, this.config.context); - const attributeRes = await this.createAttribute({ key, name, description }); - //map old attribute uid to new attribute uid - //mapper file is used to check whether attribute created or not before creating audience - this.attributesUidMapper[uid] = attributeRes?.uid ?? ''; - log.debug(`Created attribute: ${uid} -> ${attributeRes?.uid}`, this.config.context); - } catch (error) { - handleAndLogError(error, this.config.context, `Failed to create attribute: ${name}`); - } + const [canImport, attributesCount] = await this.analyzeAttributes(); + if (!canImport) { + log.info('No attributes found to import', this.config.context); + // Still need to mark as complete for parent progress + if (this.parentProgressManager) { + this.parentProgressManager.tick(true, 'attributes module (no data)', null, 'Attributes'); } + return; + } - fsUtil.writeFile(this.attributesUidMapperPath, this.attributesUidMapper); - log.debug(`Saved ${Object.keys(this.attributesUidMapper).length} attribute mappings to: ${this.attributesUidMapperPath}`, this.config.context); - log.success('Attributes imported successfully', this.config.context); - } catch (error) { - handleAndLogError(error, this.config.context); + // If we have a parent progress manager, use it as a sub-module + // Otherwise create our own simple progress manager + let progress; + if (this.parentProgressManager) { + progress = this.parentProgressManager; + log.debug('Using parent progress manager for attributes import', this.config.context); + } else { + progress = this.createSimpleProgress('Attributes', attributesCount); + log.debug('Created standalone progress manager for attributes import', this.config.context); } - } else { - log.warn(`Attributes file not found: ${attributesPath}`, this.config.context); + + await this.init(); + await fsUtil.makeDirectory(this.attrMapperDirPath); + log.debug(`Created mapper directory: ${this.attrMapperDirPath}`, this.config.context); + + const { dirName, fileName } = this.attributeConfig; + log.info(`Processing ${attributesCount} attributes`, this.config.context); + + for (const attribute of this.attributeData) { + const { key, name, description, uid } = attribute; + if (!this.parentProgressManager) { + progress.updateStatus(`Processing attribute: ${name}...`); + } + log.debug(`Processing attribute: ${name} - ${attribute.__type}`, this.config.context); + + // skip creating preset attributes, as they are already present in the system + if (attribute.__type === 'PRESET') { + log.debug(`Skipping preset attribute: ${name}`, this.config.context); + this.updateProgress(true, `attribute: ${name} (preset - skipped)`, undefined, 'Attributes'); + continue; + } + + try { + log.debug(`Creating custom attribute: ${name}`, this.config.context); + const attributeRes = await this.createAttribute({ key, name, description }); + //map old attribute uid to new attribute uid + //mapper file is used to check whether attribute created or not before creating audience + this.attributesUidMapper[uid] = attributeRes?.uid ?? ''; + + this.updateProgress(true, `attribute: ${name}`, undefined, 'Attributes'); + log.debug(`Created attribute: ${uid} -> ${attributeRes?.uid}`, this.config.context); + } catch (error) { + this.updateProgress(false, `attribute: ${name}`, (error as any)?.message, 'Attributes'); + handleAndLogError(error, this.config.context, `Failed to create attribute: ${name}`); + } + } + + fsUtil.writeFile(this.attributesUidMapperPath, this.attributesUidMapper); + log.debug(`Saved ${Object.keys(this.attributesUidMapper).length} attribute mappings`, this.config.context); + + if (!this.parentProgressManager) { + this.completeProgress(true); + } + log.success( + `Attributes imported successfully! Total attributes: ${attributesCount} - personalization enabled`, + this.config.context, + ); + } catch (error) { + if (!this.parentProgressManager) { + this.completeProgress(false, (error as any)?.message || 'Attributes import failed'); + } + handleAndLogError(error, this.config.context); + throw error; } } + + private async analyzeAttributes(): Promise<[boolean, number]> { + return this.withLoadingSpinner('ATTRIBUTES: Analyzing import data...', async () => { + const { dirName, fileName } = this.attributeConfig; + const attributesPath = resolve( + sanitizePath(this.config.data), + sanitizePath(this.personalizeConfig.dirName), + sanitizePath(dirName), + sanitizePath(fileName), + ); + + log.debug(`Checking for attributes file: ${attributesPath}`, this.config.context); + + if (!existsSync(attributesPath)) { + log.warn(`Attributes file not found: ${attributesPath}`, this.config.context); + return [false, 0]; + } + + this.attributeData = fsUtil.readFile(attributesPath, true) as AttributeStruct[]; + const attributesCount = this.attributeData?.length || 0; + + log.debug(`Found ${attributesCount} attributes to import`, this.config.context); + return [attributesCount > 0, attributesCount]; + }); + } } diff --git a/packages/contentstack-variants/src/import/audiences.ts b/packages/contentstack-variants/src/import/audiences.ts index aee1ebe69b..6496bc5811 100644 --- a/packages/contentstack-variants/src/import/audiences.ts +++ b/packages/contentstack-variants/src/import/audiences.ts @@ -13,15 +13,16 @@ export default class Audiences extends PersonalizationAdapter { private personalizeConfig: ImportConfig['modules']['personalize']; private audienceConfig: ImportConfig['modules']['personalize']['audiences']; public attributeConfig: ImportConfig['modules']['personalize']['attributes']; + private audiences: AudienceStruct[]; - constructor(public readonly config: ImportConfig ) { + constructor(public readonly config: ImportConfig) { const conf: APIConfig = { config, baseURL: config.modules.personalize.baseURL[config.region.name], headers: { 'X-Project-Uid': config.modules.personalize.project_id }, }; super(Object.assign(config, conf)); - + this.personalizeConfig = this.config.modules.personalize; this.audienceConfig = this.personalizeConfig.audiences; this.attributeConfig = this.personalizeConfig.attributes; @@ -39,67 +40,124 @@ export default class Audiences extends PersonalizationAdapter { ); this.audiencesUidMapper = {}; this.config.context.module = 'audiences'; + this.audiences = []; } /** * The function asynchronously imports audiences from a JSON file and creates them in the system. */ - async import() { - await this.init(); - await fsUtil.makeDirectory(this.audienceMapperDirPath); - log.debug(`Created mapper directory: ${this.audienceMapperDirPath}`, this.config.context); - - const { dirName, fileName } = this.audienceConfig; - const audiencesPath = resolve( - sanitizePath(this.config.data), - sanitizePath(this.personalizeConfig.dirName), - sanitizePath(dirName), - sanitizePath(fileName), - ); + async import() { + try { + log.debug('Starting audiences import...', this.config.context); - log.debug(`Checking for audiences file: ${audiencesPath}`, this.config.context); - - if (existsSync(audiencesPath)) { - try { - const audiences = fsUtil.readFile(audiencesPath, true) as AudienceStruct[]; - log.info(`Found ${audiences.length} audiences to import`, this.config.context); - - const attributesUid = (fsUtil.readFile(this.attributesMapperPath, true) as Record) || {}; - log.debug(`Loaded ${Object.keys(attributesUid).length} attribute mappings for audience processing`, this.config.context); - - for (const audience of audiences) { - let { name, definition, description, uid } = audience; - log.debug(`Processing audience: ${name} (${uid})`, this.config.context); - - try { - //check whether reference attributes exists or not - if (definition.rules?.length) { - log.debug(`Processing ${definition.rules.length} definition rules for audience: ${name}`, this.config.context); - definition.rules = lookUpAttributes(definition.rules, attributesUid); - log.debug(`Processed definition rules, remaining rules: ${definition.rules.length}`, this.config.context); - } else { - log.debug(`No definition rules found for audience: ${name}`, this.config.context); - } - - log.debug(`Creating audience: ${name}`, this.config.context); - const audienceRes = await this.createAudience({ definition, name, description }); - //map old audience uid to new audience uid - //mapper file is used to check whether audience created or not before creating experience - this.audiencesUidMapper[uid] = audienceRes?.uid ?? ''; - log.debug(`Created audience: ${uid} -> ${audienceRes?.uid}`, this.config.context); - } catch (error) { - handleAndLogError(error, this.config.context, `Failed to create audience: ${name} (${uid})`); + const [canImport, audiencesCount] = await this.analyzeAudiences(); + if (!canImport) { + log.info('No audiences found to import', this.config.context); + // Still need to mark as complete for parent progress + if (this.parentProgressManager) { + this.parentProgressManager.tick(true, 'audiences module (no data)', null, 'Audiences'); + } + return; + } + + // If we have a parent progress manager, use it as a sub-module + // Otherwise create our own simple progress manager + let progress; + if (this.parentProgressManager) { + progress = this.parentProgressManager; + log.debug('Using parent progress manager for audiences import', this.config.context); + } else { + progress = this.createSimpleProgress('Audiences', audiencesCount); + log.debug('Created standalone progress manager for audiences import', this.config.context); + } + + await this.init(); + await fsUtil.makeDirectory(this.audienceMapperDirPath); + log.debug(`Created mapper directory: ${this.audienceMapperDirPath}`, this.config.context); + + const attributesUid = (fsUtil.readFile(this.attributesMapperPath, true) as Record) || {}; + log.debug( + `Loaded ${Object.keys(attributesUid).length} attribute mappings for audience processing`, + this.config.context, + ); + + for (const audience of this.audiences) { + let { name, definition, description, uid } = audience; + if (!this.parentProgressManager) { + progress.updateStatus(`Processing audience: ${name}...`); + } + log.debug(`Processing audience: ${name} (${uid})`, this.config.context); + + try { + //check whether reference attributes exists or not + if (definition.rules?.length) { + log.debug( + `Processing ${definition.rules.length} definition rules for audience: ${name}`, + this.config.context, + ); + definition.rules = lookUpAttributes(definition.rules, attributesUid); + log.debug(`Processed definition rules, remaining rules: ${definition.rules.length}`, this.config.context); + } else { + log.debug(`No definition rules found for audience: ${name}`, this.config.context); } + + log.debug(`Creating audience: ${name}`, this.config.context); + const audienceRes = await this.createAudience({ definition, name, description }); + //map old audience uid to new audience uid + //mapper file is used to check whether audience created or not before creating experience + this.audiencesUidMapper[uid] = audienceRes?.uid ?? ''; + + this.updateProgress(true, `audience: ${name}`, undefined, 'Audiences'); + log.debug(`Created audience: ${uid} -> ${audienceRes?.uid}`, this.config.context); + } catch (error) { + this.updateProgress(false, `audience: ${name}`, (error as any)?.message, 'Audiences'); + handleAndLogError(error, this.config.context, `Failed to create audience: ${name} (${uid})`); } + } + + fsUtil.writeFile(this.audiencesUidMapperPath, this.audiencesUidMapper); + log.debug(`Saved ${Object.keys(this.audiencesUidMapper).length} audience mappings`, this.config.context); - fsUtil.writeFile(this.audiencesUidMapperPath, this.audiencesUidMapper); - log.debug(`Saved ${Object.keys(this.audiencesUidMapper).length} audience mappings to: ${this.audiencesUidMapperPath}`, this.config.context); - log.success('Audiences imported successfully', this.config.context); - } catch (error) { - handleAndLogError(error, this.config.context); + // Only complete progress if we own the progress manager (no parent) + if (!this.parentProgressManager) { + this.completeProgress(true); } - } else { - log.warn(`Audiences file not found: ${audiencesPath}`, this.config.context); + + log.success( + `Audiences imported successfully! Total audiences: ${audiencesCount}`, + this.config.context, + ); + } catch (error) { + if (!this.parentProgressManager) { + this.completeProgress(false, (error as any)?.message || 'Audiences import failed'); + } + handleAndLogError(error, this.config.context); + throw error; } } + + private async analyzeAudiences(): Promise<[boolean, number]> { + return this.withLoadingSpinner('AUDIENCES: Analyzing import data...', async () => { + const { dirName, fileName } = this.audienceConfig; + const audiencesPath = resolve( + sanitizePath(this.config.data), + sanitizePath(this.personalizeConfig.dirName), + sanitizePath(dirName), + sanitizePath(fileName), + ); + + log.debug(`Checking for audiences file: ${audiencesPath}`, this.config.context); + + if (!existsSync(audiencesPath)) { + log.warn(`Audiences file not found: ${audiencesPath}`, this.config.context); + return [false, 0]; + } + + this.audiences = fsUtil.readFile(audiencesPath, true) as AudienceStruct[]; + const audiencesCount = this.audiences?.length || 0; + + log.debug(`Found ${audiencesCount} audiences to import`, this.config.context); + return [audiencesCount > 0, audiencesCount]; + }); + } } diff --git a/packages/contentstack-variants/src/import/events.ts b/packages/contentstack-variants/src/import/events.ts index 795838f791..46647e8722 100644 --- a/packages/contentstack-variants/src/import/events.ts +++ b/packages/contentstack-variants/src/import/events.ts @@ -10,7 +10,8 @@ export default class Events extends PersonalizationAdapter { private eventsUidMapperPath: string; private eventsUidMapper: Record; private personalizeConfig: ImportConfig['modules']['personalize']; - private eventsConfig: ImportConfig['modules']['personalize']['events']; + private eventConfig: ImportConfig['modules']['personalize']['events']; + private events: EventStruct[]; constructor(public readonly config: ImportConfig) { const conf: APIConfig = { @@ -19,65 +20,124 @@ export default class Events extends PersonalizationAdapter { headers: { 'X-Project-Uid': config.modules.personalize.project_id }, }; super(Object.assign(config, conf)); - + this.personalizeConfig = this.config.modules.personalize; - this.eventsConfig = this.personalizeConfig.events; + this.eventConfig = this.personalizeConfig.events; this.mapperDirPath = resolve( sanitizePath(this.config.backupDir), 'mapper', sanitizePath(this.personalizeConfig.dirName), ); - this.eventMapperDirPath = resolve(sanitizePath(this.mapperDirPath), sanitizePath(this.eventsConfig.dirName)); + this.eventMapperDirPath = resolve(sanitizePath(this.mapperDirPath), sanitizePath(this.eventConfig.dirName)); this.eventsUidMapperPath = resolve(sanitizePath(this.eventMapperDirPath), 'uid-mapping.json'); this.eventsUidMapper = {}; - this.config.context.module = 'events'; + this.events = []; } /** * The function asynchronously imports events from a JSON file and creates them in the system. */ async import() { - await this.init(); - await fsUtil.makeDirectory(this.eventMapperDirPath); - log.debug(`Created mapper directory: ${this.eventMapperDirPath}`, this.config.context); - - const { dirName, fileName } = this.eventsConfig; - const eventsPath = resolve( - sanitizePath(this.config.data), - sanitizePath(this.personalizeConfig.dirName), - sanitizePath(dirName), - sanitizePath(fileName), - ); + try { + log.debug('Starting events import...', this.config.context); + + const [canImport, eventsCount] = await this.analyzeEvents(); + if (!canImport) { + log.info('No events found to import', this.config.context); + // Still need to mark as complete for parent progress + if (this.parentProgressManager) { + this.parentProgressManager.tick(true, 'events module (no data)', null, 'Events'); + } + return; + } + + // Don't create own progress manager if we have a parent + let progress; + if (this.parentProgressManager) { + progress = this.parentProgressManager; + log.debug('Using parent progress manager for events import', this.config.context); + } else { + progress = this.createSimpleProgress('Events', eventsCount); + log.debug('Created standalone progress manager for events import', this.config.context); + } + + await this.init(); + await fsUtil.makeDirectory(this.eventMapperDirPath); + log.debug(`Created mapper directory: ${this.eventMapperDirPath}`, this.config.context); + + log.info(`Processing ${eventsCount} events`, this.config.context); + + for (const event of this.events) { + const { key, description, uid } = event; + if (!this.parentProgressManager) { + progress.updateStatus(`Processing event: ${key}...`); + } + log.debug(`Processing event: ${key} (${uid})`, this.config.context); - log.debug(`Checking for events file: ${eventsPath}`, this.config.context); - - if (existsSync(eventsPath)) { - try { - const events = fsUtil.readFile(eventsPath, true) as EventStruct[]; - log.info(`Found ${events.length} events to import`, this.config.context); - - for (const event of events) { - const { key, description, uid } = event; - log.debug(`Processing event: ${key} (${uid})`, this.config.context); - - try { - log.debug(`Creating event: ${key}`, this.config.context); - const eventsResponse = await this.createEvents({ key, description }); - this.eventsUidMapper[uid] = eventsResponse?.uid ?? ''; - log.debug(`Created event: ${uid} -> ${eventsResponse?.uid}`, this.config.context); - } catch (error) { - handleAndLogError(error, this.config.context, `Failed to create event: ${key} (${uid})`); + try { + log.debug(`Creating event: ${key}`, this.config.context); + const eventRes = await this.createEvents({ key, description }); + this.eventsUidMapper[uid] = eventRes?.uid ?? ''; + + // For parent progress manager, we don't need to specify process name as it will be handled automatically + if (this.parentProgressManager) { + this.updateProgress(true, `event: ${key}`); + } else { + this.updateProgress(true, `event: ${key}`, undefined, 'Events'); + } + log.debug(`Created event: ${uid} -> ${eventRes?.uid}`, this.config.context); + } catch (error) { + if (this.parentProgressManager) { + this.updateProgress(false, `event: ${key}`, (error as any)?.message); + } else { + this.updateProgress(false, `event: ${key}`, (error as any)?.message, 'Events'); } + handleAndLogError(error, this.config.context, `Failed to create event: ${key} (${uid})`); } + } - fsUtil.writeFile(this.eventsUidMapperPath, this.eventsUidMapper); - log.debug(`Saved ${Object.keys(this.eventsUidMapper).length} event mappings to: ${this.eventsUidMapperPath}`, this.config.context); - log.success('Events imported successfully', this.config.context); - } catch (error) { - handleAndLogError(error, this.config.context); + fsUtil.writeFile(this.eventsUidMapperPath, this.eventsUidMapper); + log.debug(`Saved ${Object.keys(this.eventsUidMapper).length} event mappings`, this.config.context); + + // Only complete progress if we own the progress manager (no parent) + if (!this.parentProgressManager) { + this.completeProgress(true); + } + log.success( + `Events imported successfully! Total events: ${eventsCount} - personalization enabled`, + this.config.context, + ); + } catch (error) { + if (!this.parentProgressManager) { + this.completeProgress(false, (error as any)?.message || 'Events import failed'); } - } else { - log.warn(`Events file not found: ${eventsPath}`, this.config.context); + handleAndLogError(error, this.config.context); + throw error; } } + + private async analyzeEvents(): Promise<[boolean, number]> { + return this.withLoadingSpinner('EVENTS: Analyzing import data...', async () => { + const { dirName, fileName } = this.eventConfig; + const eventsPath = resolve( + sanitizePath(this.config.data), + sanitizePath(this.personalizeConfig.dirName), + sanitizePath(dirName), + sanitizePath(fileName), + ); + + log.debug(`Checking for events file: ${eventsPath}`, this.config.context); + + if (!existsSync(eventsPath)) { + log.warn(`Events file not found: ${eventsPath}`, this.config.context); + return [false, 0]; + } + + this.events = fsUtil.readFile(eventsPath, true) as EventStruct[]; + const eventsCount = this.events?.length || 0; + + log.debug(`Found ${eventsCount} events to import`, this.config.context); + return [eventsCount > 0, eventsCount]; + }); + } } diff --git a/packages/contentstack-variants/src/import/experiences.ts b/packages/contentstack-variants/src/import/experiences.ts index 29c4db03da..daf3475b20 100644 --- a/packages/contentstack-variants/src/import/experiences.ts +++ b/packages/contentstack-variants/src/import/experiences.ts @@ -40,6 +40,7 @@ export default class Experiences extends PersonalizationAdapter { private personalizeConfig: ImportConfig['modules']['personalize']; private audienceConfig: ImportConfig['modules']['personalize']['audiences']; private experienceConfig: ImportConfig['modules']['personalize']['experiences']; + private experiences: ExperienceStruct[]; constructor(public readonly config: ImportConfig) { const conf: APIConfig = { @@ -102,32 +103,53 @@ export default class Experiences extends PersonalizationAdapter { this.audiencesUid = (fsUtil.readFile(this.audiencesMapperPath, true) as Record) || {}; this.eventsUid = (fsUtil.readFile(this.eventsMapperPath, true) as Record) || {}; this.config.context.module = 'experiences'; + this.experiences = []; } /** * The function asynchronously imports experiences from a JSON file and creates them in the system. */ async import() { - await this.init(); - await fsUtil.makeDirectory(this.expMapperDirPath); - log.debug(`Created mapper directory: ${this.expMapperDirPath}`, this.config.context); + try { + log.debug('Starting experiences import...', this.config.context); - if (existsSync(this.experiencesPath)) { - log.debug(`Loading experiences from: ${this.experiencesPath}`, this.config.context); - - try { - const experiences = fsUtil.readFile(this.experiencesPath, true) as ExperienceStruct[]; - log.info(`Found ${experiences.length} experiences to import`, this.config.context); - - for (const experience of experiences) { - const { uid, ...restExperienceData } = experience; - log.debug(`Processing experience: ${uid}`, this.config.context); - - //check whether reference audience exists or not that referenced in variations having __type equal to AudienceBasedVariation & targeting - let experienceReqObj: CreateExperienceInput = lookUpAudiences(restExperienceData, this.audiencesUid); - //check whether events exists or not that referenced in metrics - experienceReqObj = lookUpEvents(experienceReqObj, this.eventsUid); + const [canImport, experiencesCount] = await this.analyzeExperiences(); + if (!canImport) { + log.info('No experiences found to import', this.config.context); + // Still need to mark as complete for parent progress + if (this.parentProgressManager) { + this.parentProgressManager.tick(true, 'experiences module (no data)', null, 'Experiences'); + } + return; + } + // If we have a parent progress manager, use it as a sub-module + // Otherwise create our own simple progress manager + let progress; + if (this.parentProgressManager) { + progress = this.parentProgressManager; + log.debug('Using parent progress manager for experiences import', this.config.context); + } else { + progress = this.createSimpleProgress('Experiences', experiencesCount); + log.debug('Created standalone progress manager for experiences import', this.config.context); + } + + await this.init(); + await fsUtil.makeDirectory(this.expMapperDirPath); + log.debug(`Created mapper directory: ${this.expMapperDirPath}`, this.config.context); + + log.info(`Processing ${experiencesCount} experiences for import`, this.config.context); + + for (const experience of this.experiences) { + const { uid, ...restExperienceData } = experience; + log.debug(`Processing experience: ${uid}`, this.config.context); + + //check whether reference audience exists or not that referenced in variations having __type equal to AudienceBasedVariation & targeting + let experienceReqObj: CreateExperienceInput = lookUpAudiences(restExperienceData, this.audiencesUid); + //check whether events exists or not that referenced in metrics + experienceReqObj = lookUpEvents(experienceReqObj, this.eventsUid); + + try { const expRes = (await this.createExperience(experienceReqObj)) as ExperienceStruct; //map old experience uid to new experience uid this.experiencesUidMapper[uid] = expRes?.uid ?? ''; @@ -139,39 +161,79 @@ export default class Experiences extends PersonalizationAdapter { } catch (error) { handleAndLogError(error, this.config.context, `Failed to import experience versions for ${expRes.uid}`); } - } - - fsUtil.writeFile(this.experiencesUidMapperPath, this.experiencesUidMapper); - log.success('Experiences created successfully', this.config.context); - - log.info('Validating variant and variant group creation',this.config.context); - this.pendingVariantAndVariantGrpForExperience = values(cloneDeep(this.experiencesUidMapper)); - const jobRes = await this.validateVariantGroupAndVariantsCreated(); - fsUtil.writeFile(this.cmsVariantPath, this.cmsVariants); - fsUtil.writeFile(this.cmsVariantGroupPath, this.cmsVariantGroups); - - if (jobRes) { - log.success('Variant and variant groups created successfully', this.config.context); - } else { - log.error('Failed to create variants and variant groups', this.config.context); - this.personalizeConfig.importData = false; - } - if (this.personalizeConfig.importData) { - log.info('Attaching content types to experiences', this.config.context); - await this.attachCTsInExperience(); - log.success('Content types attached to experiences successfully', this.config.context); + this.updateProgress(true, `experience: ${experience.name || uid}`, undefined, 'Experiences'); + log.debug(`Successfully processed experience: ${uid}`, this.config.context); + } catch (error) { + this.updateProgress(false, `experience: ${experience.name || uid}`, (error as any)?.message, 'Experiences'); + handleAndLogError(error, this.config.context, `Failed to create experience: ${uid}`); } + } + + fsUtil.writeFile(this.experiencesUidMapperPath, this.experiencesUidMapper); + log.success('Experiences created successfully', this.config.context); - await this.createVariantIdMapper(); - } catch (error) { - handleAndLogError(error, this.config.context); + log.info('Validating variant and variant group creation',this.config.context); + this.pendingVariantAndVariantGrpForExperience = values(cloneDeep(this.experiencesUidMapper)); + const jobRes = await this.validateVariantGroupAndVariantsCreated(); + fsUtil.writeFile(this.cmsVariantPath, this.cmsVariants); + fsUtil.writeFile(this.cmsVariantGroupPath, this.cmsVariantGroups); + + if (jobRes) { + log.success('Variant and variant groups created successfully', this.config.context); + } else { + log.error('Failed to create variants and variant groups', this.config.context); + this.personalizeConfig.importData = false; } - } else { - log.warn(`Experiences file not found: ${this.experiencesPath}`, this.config.context); + + if (this.personalizeConfig.importData) { + log.info('Attaching content types to experiences', this.config.context); + await this.attachCTsInExperience(); + log.success('Content types attached to experiences successfully', this.config.context); + } + + await this.createVariantIdMapper(); + + // Only complete progress if we own the progress manager (no parent) + if (!this.parentProgressManager) { + this.completeProgress(true); + } + + log.success( + `Experiences imported successfully! Total experiences: ${experiencesCount} - personalization enabled`, + this.config.context, + ); + } catch (error) { + if (!this.parentProgressManager) { + this.completeProgress(false, (error as any)?.message || 'Experiences import failed'); + } + handleAndLogError(error, this.config.context); + throw error; } } + private async analyzeExperiences(): Promise<[boolean, number]> { + return this.withLoadingSpinner('EXPERIENCES: Analyzing import data...', async () => { + log.debug(`Checking for experiences file: ${this.experiencesPath}`, this.config.context); + + if (!existsSync(this.experiencesPath)) { + log.warn(`Experiences file not found: ${this.experiencesPath}`, this.config.context); + return [false, 0]; + } + + this.experiences = fsUtil.readFile(this.experiencesPath, true) as ExperienceStruct[]; + const experiencesCount = this.experiences?.length || 0; + + if (experiencesCount < 1) { + log.warn('No experiences found in file', this.config.context); + return [false, 0]; + } + + log.debug(`Found ${experiencesCount} experiences to import`, this.config.context); + return [true, experiencesCount]; + }); + } + /** * function import experience versions from a JSON file and creates them in the project. */ diff --git a/packages/contentstack-variants/src/import/project.ts b/packages/contentstack-variants/src/import/project.ts index 5eb8811504..dcc0c6c9f9 100644 --- a/packages/contentstack-variants/src/import/project.ts +++ b/packages/contentstack-variants/src/import/project.ts @@ -6,7 +6,8 @@ import { APIConfig, CreateProjectInput, ImportConfig, ProjectStruct } from '../t export default class Project extends PersonalizationAdapter { private projectMapperFolderPath: string; - + private projectsData: CreateProjectInput[]; + constructor(public readonly config: ImportConfig) { const conf: APIConfig = { config, @@ -14,7 +15,7 @@ export default class Project extends PersonalizationAdapter { headers: { organization_uid: config.org_uid }, }; super(Object.assign(config, conf)); - + this.projectMapperFolderPath = pResolve( sanitizePath(this.config.backupDir), 'mapper', @@ -22,42 +23,45 @@ export default class Project extends PersonalizationAdapter { 'projects', ); this.config.context.module = 'project'; + this.projectsData = []; } /** * The function asynchronously imports projects data from a file and creates projects based on the * data. */ - async import() { - const personalize = this.config.modules.personalize; - const { dirName, fileName } = personalize.projects; - const projectPath = join( - sanitizePath(this.config.data), - sanitizePath(personalize.dirName), - sanitizePath(dirName), - sanitizePath(fileName), - ); - - log.debug(`Checking for project file: ${projectPath}`, this.config.context); - - if (existsSync(projectPath)) { - const projects = JSON.parse(readFileSync(projectPath, 'utf8')) as CreateProjectInput[]; - log.debug(`Loaded ${projects?.length || 0} projects from file`, this.config.context); + async import() { + try { + log.debug('Starting personalize project import...', this.config.context); - if (!projects || projects.length < 1) { - this.config.modules.personalize.importData = false; // Stop personalize import if stack not connected to any project - log.warn('No projects found in file', this.config.context); + const [canImport, projectsCount] = await this.analyzeProjects(); + if (!canImport) { + log.info('No projects found to import', this.config.context); return; } - + + // If we have a parent progress manager, use it as a sub-module + // Otherwise create our own simple progress manager + let progress; + if (this.parentProgressManager) { + progress = this.parentProgressManager; + log.debug('Using parent progress manager for projects import', this.config.context); + } else { + progress = this.createSimpleProgress('Projects', projectsCount); + log.debug('Created standalone progress manager for projects import', this.config.context); + } + await this.init(); - - for (const project of projects) { + + for (const project of this.projectsData) { + if (!this.parentProgressManager) { + progress.updateStatus(`Creating project: ${project.name}...`); + } log.debug(`Processing project: ${project.name}`, this.config.context); - + const createProject = async (newName: void | string): Promise => { log.debug(`Creating project with name: ${newName || project.name}`, this.config.context); - + return await this.createProject({ name: newName || project.name, description: project.description, @@ -75,19 +79,70 @@ export default class Project extends PersonalizationAdapter { }); }; - const projectRes = await createProject(this.config.personalizeProjectName); - this.config.modules.personalize.project_id = projectRes.uid; - this.config.modules.personalize.importData = true; + try { + const projectRes = await createProject(this.config.personalizeProjectName); + this.config.modules.personalize.project_id = projectRes.uid; + this.config.modules.personalize.importData = true; + + await fsUtil.makeDirectory(this.projectMapperFolderPath); + fsUtil.writeFile(pResolve(sanitizePath(this.projectMapperFolderPath), 'projects.json'), projectRes); + + this.updateProgress(true, `project: ${project.name}`, undefined, 'Projects'); + log.success(`Project created successfully: ${projectRes.uid}`, this.config.context); + } catch (error) { + this.updateProgress(false, `project: ${project.name}`, (error as any)?.message, 'Projects'); + throw error; + } + } + + // Only complete progress if we own the progress manager (no parent) + if (!this.parentProgressManager) { + this.completeProgress(true); + } - await fsUtil.makeDirectory(this.projectMapperFolderPath); - fsUtil.writeFile(pResolve(sanitizePath(this.projectMapperFolderPath), 'projects.json'), projectRes); - - log.success(`Project created successfully: ${projectRes.uid}`, this.config.context); - log.debug(`Project data saved to: ${this.projectMapperFolderPath}/projects.json`, this.config.context); + log.success( + `Projects imported successfully! Total projects: ${projectsCount} - personalization enabled`, + this.config.context, + ); + } catch (error) { + this.config.modules.personalize.importData = false; + if (!this.parentProgressManager) { + this.completeProgress(false, (error as any)?.message || 'Project import failed'); } - } else { - this.config.modules.personalize.importData = false; // Stop personalize import if stack not connected to any project - log.warn(`Project file not found: ${projectPath}`, this.config.context); + throw error; } } + + private async analyzeProjects(): Promise<[boolean, number]> { + return this.withLoadingSpinner('PROJECT: Analyzing import data...', async () => { + const personalize = this.config.modules.personalize; + const { dirName, fileName } = personalize.projects; + const projectPath = join( + sanitizePath(this.config.data), + sanitizePath(personalize.dirName), + sanitizePath(dirName), + sanitizePath(fileName), + ); + + log.debug(`Checking for project file: ${projectPath}`, this.config.context); + + if (!existsSync(projectPath)) { + this.config.modules.personalize.importData = false; + log.warn(`Project file not found: ${projectPath}`, this.config.context); + return [false, 0]; + } + + this.projectsData = JSON.parse(readFileSync(projectPath, 'utf8')) as CreateProjectInput[]; + const projectsCount = this.projectsData?.length || 0; + + if (projectsCount < 1) { + this.config.modules.personalize.importData = false; + log.warn('No projects found in file', this.config.context); + return [false, 0]; + } + + log.debug(`Found ${projectsCount} projects to import`, this.config.context); + return [true, projectsCount]; + }); + } } diff --git a/packages/contentstack-variants/src/import/variant-entries.ts b/packages/contentstack-variants/src/import/variant-entries.ts index 006435db6b..98dcf51ba4 100644 --- a/packages/contentstack-variants/src/import/variant-entries.ts +++ b/packages/contentstack-variants/src/import/variant-entries.ts @@ -74,34 +74,35 @@ export default class VariantEntries extends VariantAdapter Date: Wed, 20 Aug 2025 16:27:39 +0530 Subject: [PATCH 2/2] updated talismanrc file --- .talismanrc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.talismanrc b/.talismanrc index 7383ec503f..0fd8d87921 100644 --- a/.talismanrc +++ b/.talismanrc @@ -39,4 +39,12 @@ fileignoreconfig: checksum: 0dbf0a6bc447206260b8acd41b85781d60ca50c948bb3ca62f444f97d64d1fb2 - filename: packages/contentstack-utilities/src/interfaces/index.ts checksum: d0b0042e643ce0c0489b86f15f3b64f60a837c2ae928b6275028e5e0184b0a7a +- filename: packages/contentstack-variants/src/import/attribute.ts + checksum: 03e764ee2032c44d9493f2be194f91a2337026b7fd8037df90240327e6bcaabb +- filename: packages/contentstack-variants/src/import/audiences.ts + checksum: f24697ef86e928bb4d16f93c021b647639cc344a7f02463d79d69f9434ebed56 +- filename: packages/contentstack-variants/src/import/events.ts + checksum: 6cb014b5518ffe204a9f894ad801c05e2ef91a1692049168f74dd12a224363c4 +- filename: packages/contentstack-import/src/import/modules/personalize.ts + checksum: 1311a613177160637e21b3983b281b384c2cb15837d001a398b67afef30a393a version: "1.0"