@contentstack/cli-variants 1.3.2 → 2.0.0-beta

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 +139 -53
  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 +180 -53
  33. package/lib/utils/variant-api-adapter.d.ts +28 -1
  34. package/lib/utils/variant-api-adapter.js +89 -32
  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 +188 -75
  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 +212 -76
  54. package/src/utils/variant-api-adapter.ts +137 -50
  55. package/tsconfig.json +1 -1
@@ -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
  }
@@ -3,6 +3,7 @@ import { existsSync } from 'fs';
3
3
  import { sanitizePath, log, handleAndLogError } from '@contentstack/cli-utilities';
4
4
  import { APIConfig, AudienceStruct, ImportConfig } from '../types';
5
5
  import { PersonalizationAdapter, fsUtil, lookUpAttributes } from '../utils';
6
+ import { PROCESS_NAMES, MODULE_CONTEXTS, IMPORT_PROCESS_STATUS } from '../utils/constants';
6
7
 
7
8
  export default class Audiences extends PersonalizationAdapter<ImportConfig> {
8
9
  private mapperDirPath: string;
@@ -13,15 +14,16 @@ export default class Audiences extends PersonalizationAdapter<ImportConfig> {
13
14
  private personalizeConfig: ImportConfig['modules']['personalize'];
14
15
  private audienceConfig: ImportConfig['modules']['personalize']['audiences'];
15
16
  public attributeConfig: ImportConfig['modules']['personalize']['attributes'];
17
+ private audiences: AudienceStruct[];
16
18
 
17
- constructor(public readonly config: ImportConfig ) {
19
+ constructor(public readonly config: ImportConfig) {
18
20
  const conf: APIConfig = {
19
21
  config,
20
22
  baseURL: config.modules.personalize.baseURL[config.region.name],
21
23
  headers: { 'X-Project-Uid': config.modules.personalize.project_id },
22
24
  };
23
25
  super(Object.assign(config, conf));
24
-
26
+
25
27
  this.personalizeConfig = this.config.modules.personalize;
26
28
  this.audienceConfig = this.personalizeConfig.audiences;
27
29
  this.attributeConfig = this.personalizeConfig.attributes;
@@ -38,68 +40,122 @@ export default class Audiences extends PersonalizationAdapter<ImportConfig> {
38
40
  'uid-mapping.json',
39
41
  );
40
42
  this.audiencesUidMapper = {};
41
- this.config.context.module = 'audiences';
43
+ this.config.context.module = MODULE_CONTEXTS.AUDIENCES;
44
+ this.audiences = [];
42
45
  }
43
46
 
44
47
  /**
45
48
  * The function asynchronously imports audiences from a JSON file and creates them in the system.
46
49
  */
47
- async import() {
48
- await this.init();
49
- await fsUtil.makeDirectory(this.audienceMapperDirPath);
50
- log.debug(`Created mapper directory: ${this.audienceMapperDirPath}`, this.config.context);
51
-
52
- const { dirName, fileName } = this.audienceConfig;
53
- const audiencesPath = resolve(
54
- sanitizePath(this.config.data),
55
- sanitizePath(this.personalizeConfig.dirName),
56
- sanitizePath(dirName),
57
- sanitizePath(fileName),
58
- );
50
+ async import() {
51
+ try {
52
+ log.debug('Starting audiences import...', this.config.context);
59
53
 
60
- log.debug(`Checking for audiences file: ${audiencesPath}`, this.config.context);
61
-
62
- if (existsSync(audiencesPath)) {
63
- try {
64
- const audiences = fsUtil.readFile(audiencesPath, true) as AudienceStruct[];
65
- log.info(`Found ${audiences.length} audiences to import`, this.config.context);
66
-
67
- const attributesUid = (fsUtil.readFile(this.attributesMapperPath, true) as Record<string, string>) || {};
68
- log.debug(`Loaded ${Object.keys(attributesUid).length} attribute mappings for audience processing`, this.config.context);
69
-
70
- for (const audience of audiences) {
71
- let { name, definition, description, uid } = audience;
72
- log.debug(`Processing audience: ${name} (${uid})`, this.config.context);
73
-
74
- try {
75
- //check whether reference attributes exists or not
76
- if (definition.rules?.length) {
77
- log.debug(`Processing ${definition.rules.length} definition rules for audience: ${name}`, this.config.context);
78
- definition.rules = lookUpAttributes(definition.rules, attributesUid);
79
- log.debug(`Processed definition rules, remaining rules: ${definition.rules.length}`, this.config.context);
80
- } else {
81
- log.debug(`No definition rules found for audience: ${name}`, this.config.context);
82
- }
83
-
84
- log.debug(`Creating audience: ${name}`, this.config.context);
85
- const audienceRes = await this.createAudience({ definition, name, description });
86
- //map old audience uid to new audience uid
87
- //mapper file is used to check whether audience created or not before creating experience
88
- this.audiencesUidMapper[uid] = audienceRes?.uid ?? '';
89
- log.debug(`Created audience: ${uid} -> ${audienceRes?.uid}`, this.config.context);
90
- } catch (error) {
91
- handleAndLogError(error, this.config.context, `Failed to create audience: ${name} (${uid})`);
54
+ const [canImport, audiencesCount] = await this.analyzeAudiences();
55
+ if (!canImport) {
56
+ log.info('No audiences found to import', this.config.context);
57
+ // Still need to mark as complete for parent progress
58
+ if (this.parentProgressManager) {
59
+ this.parentProgressManager.tick(true, 'audiences module (no data)', null, PROCESS_NAMES.AUDIENCES);
60
+ }
61
+ return;
62
+ }
63
+
64
+ // Don't create own progress manager if we have a parent
65
+ let progress;
66
+ if (this.parentProgressManager) {
67
+ progress = this.parentProgressManager;
68
+ log.debug('Using parent progress manager for audiences import', this.config.context);
69
+ this.parentProgressManager.updateProcessTotal(PROCESS_NAMES.AUDIENCES, audiencesCount);
70
+ } else {
71
+ progress = this.createSimpleProgress(PROCESS_NAMES.AUDIENCES, audiencesCount);
72
+ log.debug('Created standalone progress manager for audiences import', this.config.context);
73
+ }
74
+
75
+ await this.init();
76
+ await fsUtil.makeDirectory(this.audienceMapperDirPath);
77
+ log.debug(`Created mapper directory: ${this.audienceMapperDirPath}`, this.config.context);
78
+
79
+ const attributesUid = (fsUtil.readFile(this.attributesMapperPath, true) as Record<string, string>) || {};
80
+ log.debug(
81
+ `Loaded ${Object.keys(attributesUid).length} attribute mappings for audience processing`,
82
+ this.config.context,
83
+ );
84
+
85
+ for (const audience of this.audiences) {
86
+ let { name, definition, description, uid } = audience;
87
+ if (!this.parentProgressManager) {
88
+ progress.updateStatus(IMPORT_PROCESS_STATUS[PROCESS_NAMES.AUDIENCES].CREATING);
89
+ }
90
+ log.debug(`Processing audience: ${name} (${uid})`, this.config.context);
91
+
92
+ try {
93
+ //check whether reference attributes exists or not
94
+ if (definition.rules?.length) {
95
+ log.debug(
96
+ `Processing ${definition.rules.length} definition rules for audience: ${name}`,
97
+ this.config.context,
98
+ );
99
+ definition.rules = lookUpAttributes(definition.rules, attributesUid);
100
+ log.debug(`Processed definition rules, remaining rules: ${definition.rules.length}`, this.config.context);
101
+ } else {
102
+ log.debug(`No definition rules found for audience: ${name}`, this.config.context);
92
103
  }
104
+
105
+ log.debug(`Creating audience: ${name}`, this.config.context);
106
+ const audienceRes = await this.createAudience({ definition, name, description });
107
+ //map old audience uid to new audience uid
108
+ //mapper file is used to check whether audience created or not before creating experience
109
+ this.audiencesUidMapper[uid] = audienceRes?.uid ?? '';
110
+
111
+ this.updateProgress(true, `audience: ${name}`, undefined, PROCESS_NAMES.AUDIENCES);
112
+ log.debug(`Created audience: ${uid} -> ${audienceRes?.uid}`, this.config.context);
113
+ } catch (error) {
114
+ this.updateProgress(false, `audience: ${name}`, (error as any)?.message, PROCESS_NAMES.AUDIENCES);
115
+ handleAndLogError(error, this.config.context, `Failed to create audience: ${name} (${uid})`);
93
116
  }
117
+ }
118
+
119
+ fsUtil.writeFile(this.audiencesUidMapperPath, this.audiencesUidMapper);
120
+ log.debug(`Saved ${Object.keys(this.audiencesUidMapper).length} audience mappings`, this.config.context);
94
121
 
95
- fsUtil.writeFile(this.audiencesUidMapperPath, this.audiencesUidMapper);
96
- log.debug(`Saved ${Object.keys(this.audiencesUidMapper).length} audience mappings to: ${this.audiencesUidMapperPath}`, this.config.context);
97
- log.success('Audiences imported successfully', this.config.context);
98
- } catch (error) {
99
- handleAndLogError(error, this.config.context);
122
+ // Only complete progress if we own the progress manager (no parent)
123
+ if (!this.parentProgressManager) {
124
+ this.completeProgress(true);
100
125
  }
101
- } else {
102
- log.warn(`Audiences file not found: ${audiencesPath}`, this.config.context);
126
+
127
+ log.success(`Audiences imported successfully! Total audiences: ${audiencesCount}`, this.config.context);
128
+ } catch (error) {
129
+ if (!this.parentProgressManager) {
130
+ this.completeProgress(false, (error as any)?.message || 'Audiences import failed');
131
+ }
132
+ handleAndLogError(error, this.config.context);
133
+ throw error;
103
134
  }
104
135
  }
136
+
137
+ private async analyzeAudiences(): Promise<[boolean, number]> {
138
+ return this.withLoadingSpinner('AUDIENCES: Analyzing import data...', async () => {
139
+ const { dirName, fileName } = this.audienceConfig;
140
+ const audiencesPath = resolve(
141
+ sanitizePath(this.config.data),
142
+ sanitizePath(this.personalizeConfig.dirName),
143
+ sanitizePath(dirName),
144
+ sanitizePath(fileName),
145
+ );
146
+
147
+ log.debug(`Checking for audiences file: ${audiencesPath}`, this.config.context);
148
+
149
+ if (!existsSync(audiencesPath)) {
150
+ log.warn(`Audiences file not found: ${audiencesPath}`, this.config.context);
151
+ return [false, 0];
152
+ }
153
+
154
+ this.audiences = fsUtil.readFile(audiencesPath, true) as AudienceStruct[];
155
+ const audiencesCount = this.audiences?.length || 0;
156
+
157
+ log.debug(`Found ${audiencesCount} audiences to import`, this.config.context);
158
+ return [audiencesCount > 0, audiencesCount];
159
+ });
160
+ }
105
161
  }