@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
@@ -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
  }
@@ -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, EventStruct, ImportConfig } from '../types';
6
+ import { PROCESS_NAMES, MODULE_CONTEXTS, IMPORT_PROCESS_STATUS } from '../utils/constants';
6
7
 
7
8
  export default class Events extends PersonalizationAdapter<ImportConfig> {
8
9
  private mapperDirPath: string;
@@ -10,7 +11,8 @@ export default class Events extends PersonalizationAdapter<ImportConfig> {
10
11
  private eventsUidMapperPath: string;
11
12
  private eventsUidMapper: Record<string, unknown>;
12
13
  private personalizeConfig: ImportConfig['modules']['personalize'];
13
- private eventsConfig: ImportConfig['modules']['personalize']['events'];
14
+ private eventConfig: ImportConfig['modules']['personalize']['events'];
15
+ private events: EventStruct[];
14
16
 
15
17
  constructor(public readonly config: ImportConfig) {
16
18
  const conf: APIConfig = {
@@ -19,65 +21,126 @@ export default class Events extends PersonalizationAdapter<ImportConfig> {
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
- this.eventsConfig = this.personalizeConfig.events;
26
+ this.eventConfig = this.personalizeConfig.events;
25
27
  this.mapperDirPath = resolve(
26
28
  sanitizePath(this.config.backupDir),
27
29
  'mapper',
28
30
  sanitizePath(this.personalizeConfig.dirName),
29
31
  );
30
- this.eventMapperDirPath = resolve(sanitizePath(this.mapperDirPath), sanitizePath(this.eventsConfig.dirName));
32
+ this.eventMapperDirPath = resolve(sanitizePath(this.mapperDirPath), sanitizePath(this.eventConfig.dirName));
31
33
  this.eventsUidMapperPath = resolve(sanitizePath(this.eventMapperDirPath), 'uid-mapping.json');
32
34
  this.eventsUidMapper = {};
33
- this.config.context.module = 'events';
35
+ this.events = [];
36
+ this.config.context.module = MODULE_CONTEXTS.EVENTS;
34
37
  }
35
38
 
36
39
  /**
37
40
  * The function asynchronously imports events from a JSON file and creates them in the system.
38
41
  */
39
42
  async import() {
40
- await this.init();
41
- await fsUtil.makeDirectory(this.eventMapperDirPath);
42
- log.debug(`Created mapper directory: ${this.eventMapperDirPath}`, this.config.context);
43
-
44
- const { dirName, fileName } = this.eventsConfig;
45
- const eventsPath = resolve(
46
- sanitizePath(this.config.data),
47
- sanitizePath(this.personalizeConfig.dirName),
48
- sanitizePath(dirName),
49
- sanitizePath(fileName),
50
- );
43
+ try {
44
+ log.debug('Starting events import...', this.config.context);
45
+
46
+ const [canImport, eventsCount] = await this.analyzeEvents();
47
+ if (!canImport) {
48
+ log.info('No events 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, 'events module (no data)', null, PROCESS_NAMES.EVENTS);
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 events import', this.config.context);
61
+ this.parentProgressManager.updateProcessTotal(PROCESS_NAMES.EVENTS, eventsCount);
62
+ } else {
63
+ progress = this.createSimpleProgress(PROCESS_NAMES.EVENTS, eventsCount);
64
+ log.debug('Created standalone progress manager for events import', this.config.context);
65
+ }
66
+
67
+ await this.init();
68
+ await fsUtil.makeDirectory(this.eventMapperDirPath);
69
+ log.debug(`Created mapper directory: ${this.eventMapperDirPath}`, this.config.context);
70
+
71
+ log.info(`Processing ${eventsCount} events`, this.config.context);
72
+
73
+ for (const event of this.events) {
74
+ const { key, description, uid } = event;
75
+ if (!this.parentProgressManager) {
76
+ progress.updateStatus(IMPORT_PROCESS_STATUS[PROCESS_NAMES.EVENTS].CREATING);
77
+ }
78
+ log.debug(`Processing event: ${key} (${uid})`, this.config.context);
51
79
 
52
- log.debug(`Checking for events file: ${eventsPath}`, this.config.context);
53
-
54
- if (existsSync(eventsPath)) {
55
- try {
56
- const events = fsUtil.readFile(eventsPath, true) as EventStruct[];
57
- log.info(`Found ${events.length} events to import`, this.config.context);
58
-
59
- for (const event of events) {
60
- const { key, description, uid } = event;
61
- log.debug(`Processing event: ${key} (${uid})`, this.config.context);
62
-
63
- try {
64
- log.debug(`Creating event: ${key}`, this.config.context);
65
- const eventsResponse = await this.createEvents({ key, description });
66
- this.eventsUidMapper[uid] = eventsResponse?.uid ?? '';
67
- log.debug(`Created event: ${uid} -> ${eventsResponse?.uid}`, this.config.context);
68
- } catch (error) {
69
- handleAndLogError(error, this.config.context, `Failed to create event: ${key} (${uid})`);
80
+ try {
81
+ log.debug(`Creating event: ${key}`, this.config.context);
82
+ const eventRes = await this.createEvents({ key, description });
83
+ this.eventsUidMapper[uid] = eventRes?.uid ?? '';
84
+
85
+ // For parent progress manager, we don't need to specify process name as it will be handled automatically
86
+ if (this.parentProgressManager) {
87
+ this.updateProgress(true, `event: ${key}`);
88
+ } else {
89
+ this.updateProgress(true, `event: ${key}`, undefined, PROCESS_NAMES.EVENTS);
90
+ }
91
+ log.debug(`Created event: ${uid} -> ${eventRes?.uid}`, this.config.context);
92
+ } catch (error) {
93
+ if (this.parentProgressManager) {
94
+ this.updateProgress(false, `event: ${key}`, (error as any)?.message);
95
+ } else {
96
+ this.updateProgress(false, `event: ${key}`, (error as any)?.message, PROCESS_NAMES.EVENTS);
70
97
  }
98
+ handleAndLogError(error, this.config.context, `Failed to create event: ${key} (${uid})`);
71
99
  }
100
+ }
72
101
 
73
- fsUtil.writeFile(this.eventsUidMapperPath, this.eventsUidMapper);
74
- log.debug(`Saved ${Object.keys(this.eventsUidMapper).length} event mappings to: ${this.eventsUidMapperPath}`, this.config.context);
75
- log.success('Events imported successfully', this.config.context);
76
- } catch (error) {
77
- handleAndLogError(error, this.config.context);
102
+ fsUtil.writeFile(this.eventsUidMapperPath, this.eventsUidMapper);
103
+ log.debug(`Saved ${Object.keys(this.eventsUidMapper).length} event mappings`, this.config.context);
104
+
105
+ // Only complete progress if we own the progress manager (no parent)
106
+ if (!this.parentProgressManager) {
107
+ this.completeProgress(true);
108
+ }
109
+ log.success(
110
+ `Events imported successfully! Total events: ${eventsCount} - personalization enabled`,
111
+ this.config.context,
112
+ );
113
+ } catch (error) {
114
+ if (!this.parentProgressManager) {
115
+ this.completeProgress(false, (error as any)?.message || 'Events import failed');
78
116
  }
79
- } else {
80
- log.warn(`Events file not found: ${eventsPath}`, this.config.context);
117
+ handleAndLogError(error, this.config.context);
118
+ throw error;
81
119
  }
82
120
  }
121
+
122
+ private async analyzeEvents(): Promise<[boolean, number]> {
123
+ return this.withLoadingSpinner('EVENTS: Analyzing import data...', async () => {
124
+ const { dirName, fileName } = this.eventConfig;
125
+ const eventsPath = resolve(
126
+ sanitizePath(this.config.data),
127
+ sanitizePath(this.personalizeConfig.dirName),
128
+ sanitizePath(dirName),
129
+ sanitizePath(fileName),
130
+ );
131
+
132
+ log.debug(`Checking for events file: ${eventsPath}`, this.config.context);
133
+
134
+ if (!existsSync(eventsPath)) {
135
+ log.warn(`Events file not found: ${eventsPath}`, this.config.context);
136
+ return [false, 0];
137
+ }
138
+
139
+ this.events = fsUtil.readFile(eventsPath, true) as EventStruct[];
140
+ const eventsCount = this.events?.length || 0;
141
+
142
+ log.debug(`Found ${eventsCount} events to import`, this.config.context);
143
+ return [eventsCount > 0, eventsCount];
144
+ });
145
+ }
83
146
  }