@contentstack/cli-variants 1.3.3 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/lib/export/attributes.d.ts +2 -2
  2. package/lib/export/attributes.js +51 -23
  3. package/lib/export/audiences.d.ts +2 -2
  4. package/lib/export/audiences.js +50 -24
  5. package/lib/export/events.d.ts +2 -2
  6. package/lib/export/events.js +52 -24
  7. package/lib/export/experiences.js +87 -54
  8. package/lib/export/projects.d.ts +3 -2
  9. package/lib/export/projects.js +55 -63
  10. package/lib/export/variant-entries.d.ts +19 -0
  11. package/lib/export/variant-entries.js +76 -1
  12. package/lib/import/attribute.d.ts +2 -0
  13. package/lib/import/attribute.js +83 -37
  14. package/lib/import/audiences.d.ts +2 -0
  15. package/lib/import/audiences.js +85 -41
  16. package/lib/import/events.d.ts +3 -1
  17. package/lib/import/events.js +86 -30
  18. package/lib/import/experiences.d.ts +2 -0
  19. package/lib/import/experiences.js +93 -39
  20. package/lib/import/project.d.ts +2 -0
  21. package/lib/import/project.js +81 -22
  22. package/lib/import/variant-entries.d.ts +10 -0
  23. package/lib/import/variant-entries.js +132 -47
  24. package/lib/index.d.ts +1 -0
  25. package/lib/index.js +1 -0
  26. package/lib/types/export-config.d.ts +0 -2
  27. package/lib/types/import-config.d.ts +0 -1
  28. package/lib/types/utils.d.ts +1 -1
  29. package/lib/utils/constants.d.ts +91 -0
  30. package/lib/utils/constants.js +93 -0
  31. package/lib/utils/personalization-api-adapter.d.ts +34 -1
  32. package/lib/utils/personalization-api-adapter.js +171 -44
  33. package/lib/utils/variant-api-adapter.d.ts +28 -1
  34. package/lib/utils/variant-api-adapter.js +75 -0
  35. package/package.json +2 -2
  36. package/src/export/attributes.ts +84 -34
  37. package/src/export/audiences.ts +87 -41
  38. package/src/export/events.ts +84 -41
  39. package/src/export/experiences.ts +155 -83
  40. package/src/export/projects.ts +71 -39
  41. package/src/export/variant-entries.ts +136 -12
  42. package/src/import/attribute.ts +105 -49
  43. package/src/import/audiences.ts +110 -54
  44. package/src/import/events.ts +104 -41
  45. package/src/import/experiences.ts +140 -62
  46. package/src/import/project.ts +108 -38
  47. package/src/import/variant-entries.ts +179 -65
  48. package/src/index.ts +2 -1
  49. package/src/types/export-config.ts +0 -2
  50. package/src/types/import-config.ts +0 -1
  51. package/src/types/utils.ts +1 -1
  52. package/src/utils/constants.ts +98 -0
  53. package/src/utils/personalization-api-adapter.ts +202 -66
  54. package/src/utils/variant-api-adapter.ts +82 -1
  55. package/tsconfig.json +1 -1
@@ -1,12 +1,15 @@
1
- import * as path from 'path';
2
- import { sanitizePath, log } from '@contentstack/cli-utilities';
3
- import { ExportConfig, PersonalizeConfig } from '../types';
4
- import { PersonalizationAdapter, fsUtil, } from '../utils';
1
+ import { resolve as pResolve } from 'node:path';
2
+ import { sanitizePath, log, handleAndLogError } from '@contentstack/cli-utilities';
3
+ import { PersonalizeConfig, ExportConfig, ProjectStruct } from '../types';
4
+ import { fsUtil, PersonalizationAdapter } from '../utils';
5
+ import { PROCESS_NAMES, MODULE_CONTEXTS, EXPORT_PROCESS_STATUS } from '../utils/constants';
5
6
 
6
7
  export default class ExportProjects extends PersonalizationAdapter<ExportConfig> {
7
- private projectFolderPath: string;
8
+ private projectsFolderPath: string;
9
+ private projectsData: ProjectStruct[];
8
10
  public exportConfig: ExportConfig;
9
11
  public personalizeConfig: PersonalizeConfig;
12
+
10
13
  constructor(exportConfig: ExportConfig) {
11
14
  super({
12
15
  config: exportConfig,
@@ -15,57 +18,86 @@ export default class ExportProjects extends PersonalizationAdapter<ExportConfig>
15
18
  });
16
19
  this.exportConfig = exportConfig;
17
20
  this.personalizeConfig = exportConfig.modules.personalize;
18
- this.projectFolderPath = path.resolve(
21
+ this.projectsFolderPath = pResolve(
19
22
  sanitizePath(exportConfig.data),
20
23
  sanitizePath(exportConfig.branchName || ''),
21
24
  sanitizePath(this.personalizeConfig.dirName),
22
25
  'projects',
23
26
  );
24
- this.exportConfig.context.module = 'projects';
27
+ this.projectsData = [];
28
+ this.exportConfig.context.module = MODULE_CONTEXTS.PROJECTS;
25
29
  }
26
30
 
27
31
  async start() {
28
32
  try {
29
33
  log.debug('Starting projects export process...', this.exportConfig.context);
30
- log.info(`Starting projects export`, this.exportConfig.context);
31
-
32
- log.debug('Initializing personalization adapter...', this.exportConfig.context);
33
- await this.init();
34
- log.debug('Personalization adapter initialized successfully', this.exportConfig.context);
35
-
36
- log.debug(`Creating projects directory at: ${this.projectFolderPath}`, this.exportConfig.context);
37
- await fsUtil.makeDirectory(this.projectFolderPath);
38
- log.debug('Projects directory created successfully', this.exportConfig.context);
39
-
40
- log.debug(`Fetching projects for stack API key: ${this.exportConfig.apiKey}`, this.exportConfig.context);
41
- const project = await this.projects({ connectedStackApiKey: this.exportConfig.apiKey });
42
- log.debug(`Fetched ${project?.length || 0} projects`, this.exportConfig.context);
43
-
44
- if (!project || project?.length < 1) {
34
+ log.info('Starting projects export', this.exportConfig.context);
35
+
36
+ // Initial setup with loading spinner
37
+ await this.withLoadingSpinner('PROJECTS: Initializing export and fetching data...', async () => {
38
+ log.debug('Initializing personalization adapter...', this.exportConfig.context);
39
+ await this.init();
40
+ log.debug('Personalization adapter initialized successfully', this.exportConfig.context);
41
+
42
+ log.debug(`Creating projects directory at: ${this.projectsFolderPath}`, this.exportConfig.context);
43
+ await fsUtil.makeDirectory(this.projectsFolderPath);
44
+ log.debug('Projects directory created successfully', this.exportConfig.context);
45
+
46
+ log.debug('Fetching projects from personalization API...', this.exportConfig.context);
47
+ // talisman-ignore-line
48
+ this.projectsData = (await this.projects({ connectedStackApiKey: this.exportConfig.apiKey })) || [];
49
+ log.debug(`Fetched ${this.projectsData?.length || 0} projects`, this.exportConfig.context);
50
+ });
51
+
52
+ if (!this.projectsData?.length) {
45
53
  log.debug('No projects found, disabling personalization', this.exportConfig.context);
46
- log.info(`No Personalize Project connected with the given stack`, this.exportConfig.context);
54
+ log.info('No Personalize Project connected with the given stack', this.exportConfig.context);
47
55
  this.exportConfig.personalizationEnabled = false;
48
56
  return;
49
57
  }
50
-
51
- log.debug(`Found ${project.length} projects, enabling personalization`, this.exportConfig.context);
58
+
59
+ // Enable personalization and set project config
60
+ log.debug(`Found ${this.projectsData.length} projects, enabling personalization`, this.exportConfig.context);
52
61
  this.exportConfig.personalizationEnabled = true;
53
- this.exportConfig.project_id = project[0]?.uid;
54
- log.debug(`Set project ID: ${project[0]?.uid}`, this.exportConfig.context);
55
-
56
- const projectsFilePath = path.resolve(sanitizePath(this.projectFolderPath), 'projects.json');
57
- log.debug(`Writing projects data to: ${projectsFilePath}`, this.exportConfig.context);
58
- fsUtil.writeFile(projectsFilePath, project);
59
-
60
- log.debug('Projects export completed successfully', this.exportConfig.context);
61
- log.success(`Projects exported successfully!`, this.exportConfig.context);
62
- } catch (error) {
63
- if (error !== 'Forbidden') {
64
- log.debug(`Error occurred during projects export: ${error}`, this.exportConfig.context);
65
- log.error('Failed to export projects!', this.exportConfig.context);
62
+ this.exportConfig.project_id = this.projectsData[0]?.uid;
63
+ log.debug(`Set project ID: ${this.projectsData[0]?.uid}`, this.exportConfig.context);
64
+
65
+ let progress: any;
66
+ if (this.parentProgressManager) {
67
+ progress = this.parentProgressManager;
68
+ this.progressManager = this.parentProgressManager;
69
+ // Parent already has correct count, just update status
70
+ progress.updateStatus(EXPORT_PROCESS_STATUS[PROCESS_NAMES.PROJECTS].EXPORTING, PROCESS_NAMES.PROJECTS);
66
71
  } else {
67
- log.debug('Projects export forbidden, likely due to permissions', this.exportConfig.context);
72
+ progress = this.createNestedProgress(PROCESS_NAMES.PROJECTS);
73
+ progress.addProcess(PROCESS_NAMES.PROJECTS, this.projectsData?.length);
74
+ progress
75
+ .startProcess(PROCESS_NAMES.PROJECTS)
76
+ .updateStatus(EXPORT_PROCESS_STATUS[PROCESS_NAMES.PROJECTS].EXPORTING, PROCESS_NAMES.PROJECTS);
68
77
  }
78
+
79
+ const projectsFilePath = pResolve(sanitizePath(this.projectsFolderPath), 'projects.json');
80
+ log.debug(`Writing projects to: ${projectsFilePath}`, this.exportConfig.context);
81
+ fsUtil.writeFile(projectsFilePath, this.projectsData);
82
+ log.debug('Projects export completed successfully', this.exportConfig.context);
83
+
84
+ const processName = PROCESS_NAMES.PROJECTS;
85
+ this.updateProgress(true, 'project export', undefined, processName);
86
+
87
+ // Complete process only if we're managing our own progress
88
+ if (!this.parentProgressManager) {
89
+ progress.completeProcess(PROCESS_NAMES.PROJECTS, true);
90
+ this.completeProgress(true);
91
+ }
92
+
93
+ log.success(
94
+ `Projects exported successfully! Total projects: ${this.projectsData.length} - personalization enabled`,
95
+ this.exportConfig.context,
96
+ );
97
+ } catch (error: any) {
98
+ log.debug(`Error occurred during projects export: ${error}`, this.exportConfig.context);
99
+ this.completeProgress(false, error?.message || 'Projects export failed');
100
+ handleAndLogError(error, { ...this.exportConfig.context });
69
101
  throw error;
70
102
  }
71
103
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync } from 'fs';
2
2
  import { join, resolve } from 'path';
3
- import { FsUtility, sanitizePath, log, handleAndLogError } from '@contentstack/cli-utilities';
3
+ import { FsUtility, sanitizePath, log, handleAndLogError, CLIProgressManager } from '@contentstack/cli-utilities';
4
+ import { PROCESS_NAMES, EXPORT_PROCESS_STATUS } from '../utils/constants';
4
5
 
5
6
  import { APIConfig, AdapterType, ExportConfig } from '../types';
6
7
  import VariantAdapter, { VariantHttpClient } from '../utils/variant-api-adapter';
@@ -8,6 +9,12 @@ import VariantAdapter, { VariantHttpClient } from '../utils/variant-api-adapter'
8
9
  export default class VariantEntries extends VariantAdapter<VariantHttpClient<ExportConfig>> {
9
10
  public entriesDirPath: string;
10
11
  public variantEntryBasePath!: string;
12
+ protected progressManager: CLIProgressManager | null = null;
13
+ protected parentProgressManager: CLIProgressManager | null = null;
14
+ public progress: any;
15
+ private processInitialized: boolean = false;
16
+ private totalVariantCount: number = 0;
17
+ private processedVariantCount: number = 0;
11
18
 
12
19
  constructor(readonly config: ExportConfig) {
13
20
  const conf: APIConfig & AdapterType<VariantHttpClient<ExportConfig>, APIConfig> = {
@@ -33,6 +40,39 @@ export default class VariantEntries extends VariantAdapter<VariantHttpClient<Exp
33
40
  }
34
41
  }
35
42
 
43
+ /**
44
+ * Set parent progress manager for integration with entries module
45
+ */
46
+ public setParentProgressManager(parentProgress: CLIProgressManager): void {
47
+ this.parentProgressManager = parentProgress;
48
+ this.progressManager = parentProgress;
49
+ this.progress = parentProgress;
50
+ }
51
+
52
+ /**
53
+ * Update progress for a specific item
54
+ */
55
+ protected updateProgress(success: boolean, itemName: string, error?: string, processName?: string): void {
56
+ if (this.progress) {
57
+ this.progress.tick(success, itemName, error, processName);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Complete the variant entries export process
63
+ */
64
+ public completeExport(): void {
65
+ if (this.processInitialized && this.progress) {
66
+ this.progress.completeProcess(PROCESS_NAMES.VARIANT_ENTRIES, true);
67
+ log.success(
68
+ `Completed export of ${this.totalVariantCount} variant entries across all content types and locales`,
69
+ this.config.context,
70
+ );
71
+ } else if (this.totalVariantCount === 0) {
72
+ log.info(`No variant entries found for export`, this.config.context);
73
+ }
74
+ }
75
+
36
76
  /**
37
77
  * This function exports variant entries for a specific content type and locale.
38
78
  * @param options - The `exportVariantEntry` function takes in an `options` object with the following
@@ -41,18 +81,27 @@ export default class VariantEntries extends VariantAdapter<VariantHttpClient<Exp
41
81
  async exportVariantEntry(options: { locale: string; contentTypeUid: string; entries: Record<string, any>[] }) {
42
82
  const variantEntry = this.config.modules.variantEntry;
43
83
  const { entries, locale, contentTypeUid: content_type_uid } = options;
44
-
45
- log.debug(`Starting variant entries export for content type: ${content_type_uid}, locale: ${locale}`, this.config.context);
84
+
85
+ log.debug(
86
+ `Starting variant entries export for content type: ${content_type_uid}, locale: ${locale}`,
87
+ this.config.context,
88
+ );
46
89
  log.debug(`Processing ${entries.length} entries for variant export`, this.config.context);
47
-
90
+
48
91
  log.debug('Initializing variant instance...', this.config.context);
49
92
  await this.variantInstance.init();
50
93
  log.debug('Variant instance initialized successfully', this.config.context);
51
-
94
+
95
+ let localVariantCount = 0; // Track variants found in this specific call
96
+ let processedEntries = 0;
97
+
52
98
  for (let index = 0; index < entries.length; index++) {
53
99
  const entry = entries[index];
54
- log.debug(`Processing variant entries for entry: ${entry.title} (${entry.uid}) - ${index + 1}/${entries.length}`, this.config.context);
55
-
100
+ log.debug(
101
+ `Processing variant entries for entry: ${entry.title} (${entry.uid}) - ${index + 1}/${entries.length}`,
102
+ this.config.context,
103
+ );
104
+
56
105
  const variantEntryBasePath = join(
57
106
  sanitizePath(this.entriesDirPath),
58
107
  sanitizePath(content_type_uid),
@@ -61,7 +110,7 @@ export default class VariantEntries extends VariantAdapter<VariantHttpClient<Exp
61
110
  sanitizePath(entry.uid),
62
111
  );
63
112
  log.debug(`Variant entry base path: ${variantEntryBasePath}`, this.config.context);
64
-
113
+
65
114
  const variantEntriesFs = new FsUtility({
66
115
  isArray: true,
67
116
  keepMetadata: false,
@@ -73,9 +122,38 @@ export default class VariantEntries extends VariantAdapter<VariantHttpClient<Exp
73
122
  });
74
123
  log.debug('Initialized FsUtility for variant entries', this.config.context);
75
124
 
125
+ let entryHasVariants = false;
126
+ let variantCount = 0;
127
+
76
128
  const callback = (variantEntries: Record<string, any>[]) => {
77
- log.debug(`Callback received ${variantEntries?.length || 0} variant entries for entry: ${entry.uid}`, this.config.context);
129
+ log.debug(
130
+ `Callback received ${variantEntries?.length || 0} variant entries for entry: ${entry.uid}`,
131
+ this.config.context,
132
+ );
78
133
  if (variantEntries?.length) {
134
+ log.info(`Fetched ${variantEntries.length} variant entries for entry: ${entry.uid}`, this.config.context);
135
+ entryHasVariants = true;
136
+ variantCount = variantEntries.length;
137
+ localVariantCount += variantCount;
138
+ this.totalVariantCount += variantCount;
139
+
140
+ // Initialize progress ONLY when we find the first variants globally (lazy initialization)
141
+ if (!this.processInitialized && this.progress) {
142
+ this.progress.addProcess(PROCESS_NAMES.VARIANT_ENTRIES, variantCount);
143
+ this.progress.startProcess(PROCESS_NAMES.VARIANT_ENTRIES);
144
+ this.processInitialized = true;
145
+ log.debug(
146
+ `Initialized variant entries progress with first batch of ${variantCount} variants`,
147
+ this.config.context,
148
+ );
149
+ }
150
+
151
+ // Update total as we discover more variants globally
152
+ if (this.processInitialized && this.progress) {
153
+ this.progress.updateProcessTotal(PROCESS_NAMES.VARIANT_ENTRIES, this.totalVariantCount);
154
+ log.debug(`Updated progress total to: ${this.totalVariantCount}`, this.config.context);
155
+ }
156
+
79
157
  if (!existsSync(variantEntryBasePath)) {
80
158
  log.debug(`Creating directory: ${variantEntryBasePath}`, this.config.context);
81
159
  mkdirSync(variantEntryBasePath, { recursive: true });
@@ -96,7 +174,7 @@ export default class VariantEntries extends VariantAdapter<VariantHttpClient<Exp
96
174
  entry_uid: entry.uid,
97
175
  locale,
98
176
  });
99
-
177
+
100
178
  if (existsSync(variantEntryBasePath)) {
101
179
  log.debug(`Completing file for entry: ${entry.uid}`, this.config.context);
102
180
  variantEntriesFs.completeFile(true);
@@ -107,8 +185,39 @@ export default class VariantEntries extends VariantAdapter<VariantHttpClient<Exp
107
185
  } else {
108
186
  log.debug(`No variant entries directory created for entry: ${entry.uid}`, this.config.context);
109
187
  }
188
+
189
+ // After processing this entry, update progress for variants found
190
+ if (entryHasVariants && this.processInitialized) {
191
+ // Tick progress for each variant found in this entry
192
+ for (let i = 0; i < variantCount; i++) {
193
+ this.processedVariantCount++;
194
+ this.updateProgress(
195
+ true,
196
+ `Exported variant ${this.processedVariantCount}/${this.totalVariantCount} from ${entry.title || entry.uid}`,
197
+ undefined,
198
+ PROCESS_NAMES.VARIANT_ENTRIES,
199
+ );
200
+ }
201
+ log.debug(
202
+ `Processed ${variantCount} variants for entry: ${entry.uid}, total processed: ${this.processedVariantCount}/${this.totalVariantCount}`,
203
+ this.config.context,
204
+ );
205
+ }
206
+
207
+ processedEntries++;
110
208
  } catch (error) {
111
209
  log.debug(`Error occurred while exporting variant entries for entry: ${entry.uid}`, this.config.context);
210
+
211
+ // Track progress for failed entry
212
+ if (this.processInitialized) {
213
+ this.updateProgress(
214
+ false,
215
+ `Failed to export variants for entry: ${entry.title || entry.uid}`,
216
+ (error as any)?.message || 'Unknown error',
217
+ PROCESS_NAMES.VARIANT_ENTRIES,
218
+ );
219
+ }
220
+
112
221
  handleAndLogError(
113
222
  error,
114
223
  { ...this.config.context },
@@ -116,7 +225,22 @@ export default class VariantEntries extends VariantAdapter<VariantHttpClient<Exp
116
225
  );
117
226
  }
118
227
  }
119
-
120
- log.debug(`Completed variant entries export for content type: ${content_type_uid}, locale: ${locale}`, this.config.context);
228
+
229
+ if (localVariantCount > 0) {
230
+ log.success(
231
+ `Exported ${localVariantCount} variant entries across ${processedEntries} entries for ${content_type_uid}/${locale}`,
232
+ this.config.context,
233
+ );
234
+ } else {
235
+ log.info(
236
+ `No variant entries found for content type: ${content_type_uid}, locale: ${locale}`,
237
+ this.config.context,
238
+ );
239
+ }
240
+
241
+ log.debug(
242
+ `Completed variant entries export for content type: ${content_type_uid}, locale: ${locale}. Local variants: ${localVariantCount}, Total variants so far: ${this.totalVariantCount}, Processed entries: ${processedEntries}`,
243
+ this.config.context,
244
+ );
121
245
  }
122
246
  }
@@ -3,6 +3,7 @@ import { existsSync } from 'fs';
3
3
  import { sanitizePath, log, handleAndLogError } from '@contentstack/cli-utilities';
4
4
  import { PersonalizationAdapter, fsUtil } from '../utils';
5
5
  import { APIConfig, AttributeStruct, ImportConfig, LogType } from '../types';
6
+ import { PROCESS_NAMES, MODULE_CONTEXTS, IMPORT_PROCESS_STATUS } from '../utils/constants';
6
7
 
7
8
  export default class Attribute extends PersonalizationAdapter<ImportConfig> {
8
9
  private mapperDirPath: string;
@@ -11,15 +12,16 @@ export default class Attribute extends PersonalizationAdapter<ImportConfig> {
11
12
  private attributesUidMapper: Record<string, unknown>;
12
13
  private personalizeConfig: ImportConfig['modules']['personalize'];
13
14
  private attributeConfig: ImportConfig['modules']['personalize']['attributes'];
15
+ private attributeData: AttributeStruct[];
14
16
 
15
- constructor(public readonly config: ImportConfig) {
17
+ constructor(public readonly config: ImportConfig) {
16
18
  const conf: APIConfig = {
17
19
  config,
18
20
  baseURL: config.modules.personalize.baseURL[config.region.name],
19
21
  headers: { 'X-Project-Uid': config.modules.personalize.project_id },
20
22
  };
21
23
  super(Object.assign(config, conf));
22
-
24
+
23
25
  this.personalizeConfig = this.config.modules.personalize;
24
26
  this.attributeConfig = this.personalizeConfig.attributes;
25
27
  this.mapperDirPath = resolve(
@@ -30,62 +32,116 @@ export default class Attribute extends PersonalizationAdapter<ImportConfig> {
30
32
  this.attrMapperDirPath = resolve(sanitizePath(this.mapperDirPath), sanitizePath(this.attributeConfig.dirName));
31
33
  this.attributesUidMapperPath = resolve(sanitizePath(this.attrMapperDirPath), 'uid-mapping.json');
32
34
  this.attributesUidMapper = {};
33
- this.config.context.module = 'attributes';
35
+ this.config.context.module = MODULE_CONTEXTS.ATTRIBUTES;
36
+ this.attributeData = [];
34
37
  }
35
38
 
36
39
  /**
37
40
  * The function asynchronously imports attributes from a JSON file and creates them in the system.
38
41
  */
39
- async import() {
40
- await this.init();
41
- await fsUtil.makeDirectory(this.attrMapperDirPath);
42
- log.debug(`Created mapper directory: ${this.attrMapperDirPath}`, this.config.context);
43
-
44
- const { dirName, fileName } = this.attributeConfig;
45
- const attributesPath = resolve(
46
- sanitizePath(this.config.data),
47
- sanitizePath(this.personalizeConfig.dirName),
48
- sanitizePath(dirName),
49
- sanitizePath(fileName),
50
- );
42
+ async import() {
43
+ try {
44
+ log.debug('Starting attributes import...', this.config.context);
45
+
46
+ const [canImport, attributesCount] = await this.analyzeAttributes();
47
+ if (!canImport) {
48
+ log.info('No attributes found to import', this.config.context);
49
+ // Still need to mark as complete for parent progress
50
+ if (this.parentProgressManager) {
51
+ this.parentProgressManager.tick(true, 'attributes module (no data)', null, PROCESS_NAMES.ATTRIBUTES);
52
+ }
53
+ return;
54
+ }
55
+
56
+ // Don't create own progress manager if we have a parent
57
+ let progress;
58
+ if (this.parentProgressManager) {
59
+ progress = this.parentProgressManager;
60
+ log.debug('Using parent progress manager for attributes import', this.config.context);
61
+ this.parentProgressManager.updateProcessTotal(PROCESS_NAMES.ATTRIBUTES, attributesCount);
62
+
63
+ } else {
64
+ progress = this.createSimpleProgress(PROCESS_NAMES.ATTRIBUTES, attributesCount);
65
+ log.debug('Created standalone progress manager for attributes import', this.config.context);
66
+ }
67
+
68
+ await this.init();
69
+ await fsUtil.makeDirectory(this.attrMapperDirPath);
70
+ log.debug(`Created mapper directory: ${this.attrMapperDirPath}`, this.config.context);
71
+
72
+ const { dirName, fileName } = this.attributeConfig;
73
+ log.info(`Processing ${attributesCount} attributes`, this.config.context);
74
+
75
+ for (const attribute of this.attributeData) {
76
+ const { key, name, description, uid } = attribute;
77
+ if (!this.parentProgressManager) {
78
+ progress.updateStatus(IMPORT_PROCESS_STATUS[PROCESS_NAMES.ATTRIBUTES].CREATING);
79
+ }
80
+ log.debug(`Processing attribute: ${name} - ${attribute.__type}`, this.config.context);
51
81
 
52
- log.debug(`Checking for attributes file: ${attributesPath}`, this.config.context);
53
-
54
- if (existsSync(attributesPath)) {
55
- try {
56
- const attributes = fsUtil.readFile(attributesPath, true) as AttributeStruct[];
57
- log.info(`Found ${attributes.length} attributes to import`, this.config.context);
58
-
59
- for (const attribute of attributes) {
60
- const { key, name, description, uid } = attribute;
61
- log.debug(`Processing attribute: ${name} - ${attribute.__type}`, this.config.context);
62
-
63
- // skip creating preset attributes, as they are already present in the system
64
- if (attribute.__type === 'PRESET') {
65
- log.debug(`Skipping preset attribute: ${name}`, this.config.context);
66
- continue;
67
- }
68
-
69
- try {
70
- log.debug(`Creating custom attribute: ${name}`, this.config.context);
71
- const attributeRes = await this.createAttribute({ key, name, description });
72
- //map old attribute uid to new attribute uid
73
- //mapper file is used to check whether attribute created or not before creating audience
74
- this.attributesUidMapper[uid] = attributeRes?.uid ?? '';
75
- log.debug(`Created attribute: ${uid} -> ${attributeRes?.uid}`, this.config.context);
76
- } catch (error) {
77
- handleAndLogError(error, this.config.context, `Failed to create attribute: ${name}`);
78
- }
82
+ // skip creating preset attributes, as they are already present in the system
83
+ if (attribute.__type === 'PRESET') {
84
+ log.debug(`Skipping preset attribute: ${name}`, this.config.context);
85
+ this.updateProgress(true, `attribute: ${name} (preset - skipped)`, undefined, PROCESS_NAMES.ATTRIBUTES);
86
+ continue;
79
87
  }
80
88
 
81
- fsUtil.writeFile(this.attributesUidMapperPath, this.attributesUidMapper);
82
- log.debug(`Saved ${Object.keys(this.attributesUidMapper).length} attribute mappings to: ${this.attributesUidMapperPath}`, this.config.context);
83
- log.success('Attributes imported successfully', this.config.context);
84
- } catch (error) {
85
- handleAndLogError(error, this.config.context);
89
+ try {
90
+ log.debug(`Creating custom attribute: ${name}`, this.config.context);
91
+ const attributeRes = await this.createAttribute({ key, name, description });
92
+ //map old attribute uid to new attribute uid
93
+ //mapper file is used to check whether attribute created or not before creating audience
94
+ this.attributesUidMapper[uid] = attributeRes?.uid ?? '';
95
+
96
+ this.updateProgress(true, `attribute: ${name}`, undefined, PROCESS_NAMES.ATTRIBUTES);
97
+ log.debug(`Created attribute: ${uid} -> ${attributeRes?.uid}`, this.config.context);
98
+ } catch (error) {
99
+ this.updateProgress(false, `attribute: ${name}`, (error as any)?.message, PROCESS_NAMES.ATTRIBUTES);
100
+ handleAndLogError(error, this.config.context, `Failed to create attribute: ${name}`);
101
+ }
86
102
  }
87
- } else {
88
- log.warn(`Attributes file not found: ${attributesPath}`, this.config.context);
103
+
104
+ fsUtil.writeFile(this.attributesUidMapperPath, this.attributesUidMapper);
105
+ log.debug(`Saved ${Object.keys(this.attributesUidMapper).length} attribute mappings`, this.config.context);
106
+
107
+ if (!this.parentProgressManager) {
108
+ this.completeProgress(true);
109
+ }
110
+ log.success(
111
+ `Attributes imported successfully! Total attributes: ${attributesCount} - personalization enabled`,
112
+ this.config.context,
113
+ );
114
+ } catch (error) {
115
+ if (!this.parentProgressManager) {
116
+ this.completeProgress(false, (error as any)?.message || 'Attributes import failed');
117
+ }
118
+ handleAndLogError(error, this.config.context);
119
+ throw error;
89
120
  }
90
121
  }
122
+
123
+ private async analyzeAttributes(): Promise<[boolean, number]> {
124
+ return this.withLoadingSpinner('ATTRIBUTES: Analyzing import data...', async () => {
125
+ const { dirName, fileName } = this.attributeConfig;
126
+ const attributesPath = resolve(
127
+ sanitizePath(this.config.data),
128
+ sanitizePath(this.personalizeConfig.dirName),
129
+ sanitizePath(dirName),
130
+ sanitizePath(fileName),
131
+ );
132
+
133
+ log.debug(`Checking for attributes file: ${attributesPath}`, this.config.context);
134
+
135
+ if (!existsSync(attributesPath)) {
136
+ log.warn(`Attributes file not found: ${attributesPath}`, this.config.context);
137
+ return [false, 0];
138
+ }
139
+
140
+ this.attributeData = fsUtil.readFile(attributesPath, true) as AttributeStruct[];
141
+ const attributesCount = this.attributeData?.length || 0;
142
+
143
+ log.debug(`Found ${attributesCount} attributes to import`, this.config.context);
144
+ return [attributesCount > 0, attributesCount];
145
+ });
146
+ }
91
147
  }