@contentstack/cli-variants 1.3.3 → 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.
- package/lib/export/attributes.d.ts +2 -2
- package/lib/export/attributes.js +51 -23
- package/lib/export/audiences.d.ts +2 -2
- package/lib/export/audiences.js +50 -24
- package/lib/export/events.d.ts +2 -2
- package/lib/export/events.js +52 -24
- package/lib/export/experiences.js +87 -54
- package/lib/export/projects.d.ts +3 -2
- package/lib/export/projects.js +55 -63
- package/lib/export/variant-entries.d.ts +19 -0
- package/lib/export/variant-entries.js +76 -1
- package/lib/import/attribute.d.ts +2 -0
- package/lib/import/attribute.js +83 -37
- package/lib/import/audiences.d.ts +2 -0
- package/lib/import/audiences.js +85 -41
- package/lib/import/events.d.ts +3 -1
- package/lib/import/events.js +86 -30
- package/lib/import/experiences.d.ts +2 -0
- package/lib/import/experiences.js +93 -39
- package/lib/import/project.d.ts +2 -0
- package/lib/import/project.js +81 -22
- package/lib/import/variant-entries.d.ts +10 -0
- package/lib/import/variant-entries.js +132 -47
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/types/export-config.d.ts +0 -2
- package/lib/types/import-config.d.ts +0 -1
- package/lib/types/utils.d.ts +1 -1
- package/lib/utils/constants.d.ts +91 -0
- package/lib/utils/constants.js +93 -0
- package/lib/utils/personalization-api-adapter.d.ts +34 -1
- package/lib/utils/personalization-api-adapter.js +171 -44
- package/lib/utils/variant-api-adapter.d.ts +28 -1
- package/lib/utils/variant-api-adapter.js +75 -0
- package/package.json +1 -1
- package/src/export/attributes.ts +84 -34
- package/src/export/audiences.ts +87 -41
- package/src/export/events.ts +84 -41
- package/src/export/experiences.ts +155 -83
- package/src/export/projects.ts +71 -39
- package/src/export/variant-entries.ts +136 -12
- package/src/import/attribute.ts +105 -49
- package/src/import/audiences.ts +110 -54
- package/src/import/events.ts +104 -41
- package/src/import/experiences.ts +140 -62
- package/src/import/project.ts +108 -38
- package/src/import/variant-entries.ts +179 -65
- package/src/index.ts +2 -1
- package/src/types/export-config.ts +0 -2
- package/src/types/import-config.ts +0 -1
- package/src/types/utils.ts +1 -1
- package/src/utils/constants.ts +98 -0
- package/src/utils/personalization-api-adapter.ts +202 -66
- package/src/utils/variant-api-adapter.ts +82 -1
- package/tsconfig.json +1 -1
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
CreateExperienceInput,
|
|
12
12
|
CreateExperienceVersionInput,
|
|
13
13
|
} from '../types';
|
|
14
|
+
import { PROCESS_NAMES, MODULE_CONTEXTS } from '../utils/constants';
|
|
14
15
|
|
|
15
16
|
export default class Experiences extends PersonalizationAdapter<ImportConfig> {
|
|
16
17
|
private createdCTs: string[];
|
|
@@ -40,9 +41,10 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
|
|
|
40
41
|
private personalizeConfig: ImportConfig['modules']['personalize'];
|
|
41
42
|
private audienceConfig: ImportConfig['modules']['personalize']['audiences'];
|
|
42
43
|
private experienceConfig: ImportConfig['modules']['personalize']['experiences'];
|
|
44
|
+
private experiences: ExperienceStruct[];
|
|
43
45
|
|
|
44
46
|
constructor(public readonly config: ImportConfig) {
|
|
45
|
-
|
|
47
|
+
const conf: APIConfig = {
|
|
46
48
|
config,
|
|
47
49
|
baseURL: config.modules.personalize.baseURL[config.region.name],
|
|
48
50
|
headers: { 'X-Project-Uid': config.modules.personalize.project_id },
|
|
@@ -52,7 +54,7 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
|
|
|
52
54
|
},
|
|
53
55
|
};
|
|
54
56
|
super(Object.assign(config, conf));
|
|
55
|
-
|
|
57
|
+
|
|
56
58
|
this.personalizeConfig = this.config.modules.personalize;
|
|
57
59
|
this.experiencesDirPath = resolve(
|
|
58
60
|
sanitizePath(this.config.data),
|
|
@@ -101,33 +103,55 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
|
|
|
101
103
|
this.createdCTs = [];
|
|
102
104
|
this.audiencesUid = (fsUtil.readFile(this.audiencesMapperPath, true) as Record<string, string>) || {};
|
|
103
105
|
this.eventsUid = (fsUtil.readFile(this.eventsMapperPath, true) as Record<string, string>) || {};
|
|
104
|
-
this.config.context.module =
|
|
106
|
+
this.config.context.module = MODULE_CONTEXTS.EXPERIENCES;
|
|
107
|
+
this.experiences = [];
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
/**
|
|
108
111
|
* The function asynchronously imports experiences from a JSON file and creates them in the system.
|
|
109
112
|
*/
|
|
110
|
-
async import() {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
113
|
+
async import() {
|
|
114
|
+
try {
|
|
115
|
+
log.debug('Starting experiences import...', this.config.context);
|
|
116
|
+
|
|
117
|
+
const [canImport, experiencesCount] = await this.analyzeExperiences();
|
|
118
|
+
if (!canImport) {
|
|
119
|
+
log.info('No experiences found to import', this.config.context);
|
|
120
|
+
// Still need to mark as complete for parent progress
|
|
121
|
+
if (this.parentProgressManager) {
|
|
122
|
+
this.parentProgressManager.tick(true, 'experiences module (no data)', null, PROCESS_NAMES.EXPERIENCES);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// If we have a parent progress manager, use it as a sub-module
|
|
128
|
+
// Otherwise create our own simple progress manager
|
|
129
|
+
let progress;
|
|
130
|
+
if (this.parentProgressManager) {
|
|
131
|
+
progress = this.parentProgressManager;
|
|
132
|
+
log.debug('Using parent progress manager for experiences import', this.config.context);
|
|
133
|
+
this.parentProgressManager.updateProcessTotal(PROCESS_NAMES.EXPERIENCES, experiencesCount);
|
|
134
|
+
} else {
|
|
135
|
+
progress = this.createSimpleProgress(PROCESS_NAMES.EXPERIENCES, experiencesCount);
|
|
136
|
+
log.debug('Created standalone progress manager for experiences import', this.config.context);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await this.init();
|
|
140
|
+
await fsUtil.makeDirectory(this.expMapperDirPath);
|
|
141
|
+
log.debug(`Created mapper directory: ${this.expMapperDirPath}`, this.config.context);
|
|
142
|
+
|
|
143
|
+
log.info(`Processing ${experiencesCount} experiences for import`, this.config.context);
|
|
130
144
|
|
|
145
|
+
for (const experience of this.experiences) {
|
|
146
|
+
const { uid, ...restExperienceData } = experience;
|
|
147
|
+
log.debug(`Processing experience: ${uid}`, this.config.context);
|
|
148
|
+
|
|
149
|
+
//check whether reference audience exists or not that referenced in variations having __type equal to AudienceBasedVariation & targeting
|
|
150
|
+
let experienceReqObj: CreateExperienceInput = lookUpAudiences(restExperienceData, this.audiencesUid);
|
|
151
|
+
//check whether events exists or not that referenced in metrics
|
|
152
|
+
experienceReqObj = lookUpEvents(experienceReqObj, this.eventsUid);
|
|
153
|
+
|
|
154
|
+
try {
|
|
131
155
|
const expRes = (await this.createExperience(experienceReqObj)) as ExperienceStruct;
|
|
132
156
|
//map old experience uid to new experience uid
|
|
133
157
|
this.experiencesUidMapper[uid] = expRes?.uid ?? '';
|
|
@@ -139,45 +163,90 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
|
|
|
139
163
|
} catch (error) {
|
|
140
164
|
handleAndLogError(error, this.config.context, `Failed to import experience versions for ${expRes.uid}`);
|
|
141
165
|
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
fsUtil.writeFile(this.experiencesUidMapperPath, this.experiencesUidMapper);
|
|
145
|
-
log.success('Experiences created successfully', this.config.context);
|
|
146
|
-
|
|
147
|
-
log.info('Validating variant and variant group creation',this.config.context);
|
|
148
|
-
this.pendingVariantAndVariantGrpForExperience = values(cloneDeep(this.experiencesUidMapper));
|
|
149
|
-
const jobRes = await this.validateVariantGroupAndVariantsCreated();
|
|
150
|
-
fsUtil.writeFile(this.cmsVariantPath, this.cmsVariants);
|
|
151
|
-
fsUtil.writeFile(this.cmsVariantGroupPath, this.cmsVariantGroups);
|
|
152
|
-
|
|
153
|
-
if (jobRes) {
|
|
154
|
-
log.success('Variant and variant groups created successfully', this.config.context);
|
|
155
|
-
} else {
|
|
156
|
-
log.error('Failed to create variants and variant groups', this.config.context);
|
|
157
|
-
this.personalizeConfig.importData = false;
|
|
158
|
-
}
|
|
159
166
|
|
|
160
|
-
|
|
161
|
-
log.
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
this.updateProgress(true, `experience: ${experience.name || uid}`, undefined, PROCESS_NAMES.EXPERIENCES);
|
|
168
|
+
log.debug(`Successfully processed experience: ${uid}`, this.config.context);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.updateProgress(
|
|
171
|
+
false,
|
|
172
|
+
`experience: ${experience.name || uid}`,
|
|
173
|
+
(error as any)?.message,
|
|
174
|
+
PROCESS_NAMES.EXPERIENCES,
|
|
175
|
+
);
|
|
176
|
+
handleAndLogError(error, this.config.context, `Failed to create experience: ${uid}`);
|
|
164
177
|
}
|
|
178
|
+
}
|
|
165
179
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
180
|
+
fsUtil.writeFile(this.experiencesUidMapperPath, this.experiencesUidMapper);
|
|
181
|
+
log.success('Experiences created successfully', this.config.context);
|
|
182
|
+
|
|
183
|
+
log.info('Validating variant and variant group creation', this.config.context);
|
|
184
|
+
this.pendingVariantAndVariantGrpForExperience = values(cloneDeep(this.experiencesUidMapper));
|
|
185
|
+
const jobRes = await this.validateVariantGroupAndVariantsCreated();
|
|
186
|
+
fsUtil.writeFile(this.cmsVariantPath, this.cmsVariants);
|
|
187
|
+
fsUtil.writeFile(this.cmsVariantGroupPath, this.cmsVariantGroups);
|
|
188
|
+
|
|
189
|
+
if (jobRes) {
|
|
190
|
+
log.success('Variant and variant groups created successfully', this.config.context);
|
|
191
|
+
} else {
|
|
192
|
+
log.error('Failed to create variants and variant groups', this.config.context);
|
|
193
|
+
this.personalizeConfig.importData = false;
|
|
169
194
|
}
|
|
170
|
-
|
|
171
|
-
|
|
195
|
+
|
|
196
|
+
if (this.personalizeConfig.importData) {
|
|
197
|
+
log.info('Attaching content types to experiences', this.config.context);
|
|
198
|
+
await this.attachCTsInExperience();
|
|
199
|
+
log.success('Content types attached to experiences successfully', this.config.context);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await this.createVariantIdMapper();
|
|
203
|
+
|
|
204
|
+
// Only complete progress if we own the progress manager (no parent)
|
|
205
|
+
if (!this.parentProgressManager) {
|
|
206
|
+
this.completeProgress(true);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
log.success(
|
|
210
|
+
`Experiences imported successfully! Total experiences: ${experiencesCount} - personalization enabled`,
|
|
211
|
+
this.config.context,
|
|
212
|
+
);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (!this.parentProgressManager) {
|
|
215
|
+
this.completeProgress(false, (error as any)?.message || 'Experiences import failed');
|
|
216
|
+
}
|
|
217
|
+
handleAndLogError(error, this.config.context);
|
|
218
|
+
throw error;
|
|
172
219
|
}
|
|
173
220
|
}
|
|
174
221
|
|
|
222
|
+
private async analyzeExperiences(): Promise<[boolean, number]> {
|
|
223
|
+
return this.withLoadingSpinner('EXPERIENCES: Analyzing import data...', async () => {
|
|
224
|
+
log.debug(`Checking for experiences file: ${this.experiencesPath}`, this.config.context);
|
|
225
|
+
|
|
226
|
+
if (!existsSync(this.experiencesPath)) {
|
|
227
|
+
log.warn(`Experiences file not found: ${this.experiencesPath}`, this.config.context);
|
|
228
|
+
return [false, 0];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.experiences = fsUtil.readFile(this.experiencesPath, true) as ExperienceStruct[];
|
|
232
|
+
const experiencesCount = this.experiences?.length || 0;
|
|
233
|
+
|
|
234
|
+
if (experiencesCount < 1) {
|
|
235
|
+
log.warn('No experiences found in file', this.config.context);
|
|
236
|
+
return [false, 0];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
log.debug(`Found ${experiencesCount} experiences to import`, this.config.context);
|
|
240
|
+
return [true, experiencesCount];
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
175
244
|
/**
|
|
176
245
|
* function import experience versions from a JSON file and creates them in the project.
|
|
177
246
|
*/
|
|
178
247
|
async importExperienceVersions(experience: ExperienceStruct, oldExperienceUid: string) {
|
|
179
248
|
log.debug(`Importing versions for experience: ${oldExperienceUid}`, this.config.context);
|
|
180
|
-
|
|
249
|
+
|
|
181
250
|
const versionsPath = resolve(
|
|
182
251
|
sanitizePath(this.experiencesDirPath),
|
|
183
252
|
'versions',
|
|
@@ -191,7 +260,7 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
|
|
|
191
260
|
|
|
192
261
|
const versions = fsUtil.readFile(versionsPath, true) as ExperienceStruct[];
|
|
193
262
|
log.debug(`Found ${versions.length} versions for experience: ${oldExperienceUid}`, this.config.context);
|
|
194
|
-
|
|
263
|
+
|
|
195
264
|
const versionMap: Record<string, CreateExperienceVersionInput | undefined> = {
|
|
196
265
|
ACTIVE: undefined,
|
|
197
266
|
DRAFT: undefined,
|
|
@@ -258,8 +327,11 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
|
|
|
258
327
|
* @returns
|
|
259
328
|
*/
|
|
260
329
|
async validateVariantGroupAndVariantsCreated(retryCount = 0): Promise<any> {
|
|
261
|
-
log.debug(
|
|
262
|
-
|
|
330
|
+
log.debug(
|
|
331
|
+
`Validating variant groups and variants creation - attempt ${retryCount + 1}/${this.maxValidateRetry}`,
|
|
332
|
+
this.config.context,
|
|
333
|
+
);
|
|
334
|
+
|
|
263
335
|
try {
|
|
264
336
|
const promises = this.pendingVariantAndVariantGrpForExperience.map(async (expUid) => {
|
|
265
337
|
log.debug(`Checking experience: ${expUid}`, this.config.context);
|
|
@@ -289,7 +361,10 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
|
|
|
289
361
|
return this.validateVariantGroupAndVariantsCreated(retryCount);
|
|
290
362
|
} else {
|
|
291
363
|
log.error('Personalize job failed to create variants and variant groups', this.config.context);
|
|
292
|
-
log.error(
|
|
364
|
+
log.error(
|
|
365
|
+
`Failed experiences: ${this.pendingVariantAndVariantGrpForExperience.join(', ')}`,
|
|
366
|
+
this.config.context,
|
|
367
|
+
);
|
|
293
368
|
fsUtil.writeFile(this.failedCmsExpPath, this.pendingVariantAndVariantGrpForExperience);
|
|
294
369
|
return false;
|
|
295
370
|
}
|
|
@@ -305,7 +380,7 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
|
|
|
305
380
|
|
|
306
381
|
async attachCTsInExperience() {
|
|
307
382
|
log.debug('Attaching content types to experiences', this.config.context);
|
|
308
|
-
|
|
383
|
+
|
|
309
384
|
try {
|
|
310
385
|
// Read the created content types from the file
|
|
311
386
|
this.createdCTs = fsUtil.readFile(this.cTsSuccessPath, true) as any;
|
|
@@ -313,22 +388,25 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
|
|
|
313
388
|
log.debug('No Content types created, skipping following process', this.config.context);
|
|
314
389
|
return;
|
|
315
390
|
}
|
|
316
|
-
|
|
391
|
+
|
|
317
392
|
log.debug(`Found ${this.createdCTs.length} created content types`, this.config.context);
|
|
318
393
|
const experienceCTsMap = fsUtil.readFile(this.experienceCTsPath, true) as Record<string, string[]>;
|
|
319
|
-
|
|
394
|
+
|
|
320
395
|
return await Promise.allSettled(
|
|
321
396
|
Object.entries(this.experiencesUidMapper).map(async ([oldExpUid, newExpUid]) => {
|
|
322
397
|
if (experienceCTsMap[oldExpUid]?.length) {
|
|
323
398
|
log.debug(`Processing content types for experience: ${oldExpUid} -> ${newExpUid}`, this.config.context);
|
|
324
|
-
|
|
399
|
+
|
|
325
400
|
// Filter content types that were created
|
|
326
401
|
const updatedContentTypes = experienceCTsMap[oldExpUid].filter(
|
|
327
402
|
(ct: any) => this.createdCTs.includes(ct?.uid) && ct.status === 'linked',
|
|
328
403
|
);
|
|
329
|
-
|
|
404
|
+
|
|
330
405
|
if (updatedContentTypes?.length) {
|
|
331
|
-
log.debug(
|
|
406
|
+
log.debug(
|
|
407
|
+
`Attaching ${updatedContentTypes.length} content types to experience: ${newExpUid}`,
|
|
408
|
+
this.config.context,
|
|
409
|
+
);
|
|
332
410
|
const { variant_groups: [variantGroup] = [] } =
|
|
333
411
|
(await this.getVariantGroup({ experienceUid: newExpUid })) || {};
|
|
334
412
|
variantGroup.content_types = updatedContentTypes;
|
|
@@ -349,11 +427,11 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
|
|
|
349
427
|
|
|
350
428
|
async createVariantIdMapper() {
|
|
351
429
|
log.debug('Creating variant ID mapper', this.config.context);
|
|
352
|
-
|
|
430
|
+
|
|
353
431
|
try {
|
|
354
432
|
const experienceVariantIds: any = fsUtil.readFile(this.experienceVariantsIdsPath, true) || [];
|
|
355
433
|
log.debug(`Found ${experienceVariantIds.length} experience variant IDs to process`, this.config.context);
|
|
356
|
-
|
|
434
|
+
|
|
357
435
|
const variantUIDMapper: Record<string, string> = {};
|
|
358
436
|
for (let experienceVariantId of experienceVariantIds) {
|
|
359
437
|
const [experienceId, variantShortId, oldVariantId] = experienceVariantId.split('-');
|
package/src/import/project.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { join, resolve as pResolve } from 'path';
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
|
-
import { sanitizePath, log } from '@contentstack/cli-utilities';
|
|
3
|
+
import { sanitizePath, log, cliux } from '@contentstack/cli-utilities';
|
|
4
4
|
import { PersonalizationAdapter, askProjectName, fsUtil } from '../utils';
|
|
5
5
|
import { APIConfig, CreateProjectInput, ImportConfig, ProjectStruct } from '../types';
|
|
6
|
+
import { PROCESS_NAMES, MODULE_CONTEXTS, IMPORT_PROCESS_STATUS } from '../utils/constants';
|
|
6
7
|
|
|
7
8
|
export default class Project extends PersonalizationAdapter<ImportConfig> {
|
|
8
9
|
private projectMapperFolderPath: string;
|
|
9
|
-
|
|
10
|
+
private projectsData: CreateProjectInput[];
|
|
11
|
+
|
|
10
12
|
constructor(public readonly config: ImportConfig) {
|
|
11
13
|
const conf: APIConfig = {
|
|
12
14
|
config,
|
|
@@ -14,50 +16,56 @@ export default class Project extends PersonalizationAdapter<ImportConfig> {
|
|
|
14
16
|
headers: { organization_uid: config.org_uid },
|
|
15
17
|
};
|
|
16
18
|
super(Object.assign(config, conf));
|
|
17
|
-
|
|
19
|
+
|
|
18
20
|
this.projectMapperFolderPath = pResolve(
|
|
19
21
|
sanitizePath(this.config.backupDir),
|
|
20
22
|
'mapper',
|
|
21
23
|
sanitizePath(this.config.modules.personalize.dirName),
|
|
22
24
|
'projects',
|
|
23
25
|
);
|
|
24
|
-
this.config.context.module =
|
|
26
|
+
this.config.context.module = MODULE_CONTEXTS.PROJECTS;
|
|
27
|
+
this.projectsData = [];
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
31
|
* The function asynchronously imports projects data from a file and creates projects based on the
|
|
29
32
|
* data.
|
|
30
33
|
*/
|
|
31
|
-
async import() {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const projectPath = join(
|
|
35
|
-
sanitizePath(this.config.data),
|
|
36
|
-
sanitizePath(personalize.dirName),
|
|
37
|
-
sanitizePath(dirName),
|
|
38
|
-
sanitizePath(fileName),
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
log.debug(`Checking for project file: ${projectPath}`, this.config.context);
|
|
42
|
-
|
|
43
|
-
if (existsSync(projectPath)) {
|
|
44
|
-
const projects = JSON.parse(readFileSync(projectPath, 'utf8')) as CreateProjectInput[];
|
|
45
|
-
log.debug(`Loaded ${projects?.length || 0} projects from file`, this.config.context);
|
|
34
|
+
async import() {
|
|
35
|
+
try {
|
|
36
|
+
log.debug('Starting personalize project import...', this.config.context);
|
|
46
37
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
log.
|
|
38
|
+
const [canImport, projectsCount] = await this.analyzeProjects();
|
|
39
|
+
if (!canImport) {
|
|
40
|
+
log.info('No projects found to import', this.config.context);
|
|
41
|
+
if (this.parentProgressManager) {
|
|
42
|
+
this.parentProgressManager.tick(true, 'projects module (no data)', null, PROCESS_NAMES.PROJECTS);
|
|
43
|
+
}
|
|
50
44
|
return;
|
|
51
45
|
}
|
|
52
|
-
|
|
46
|
+
|
|
47
|
+
// Fix 1: Always use parent progress manager when available
|
|
48
|
+
let progress;
|
|
49
|
+
if (this.parentProgressManager) {
|
|
50
|
+
progress = this.parentProgressManager;
|
|
51
|
+
log.debug('Using parent progress manager for projects import', this.config.context);
|
|
52
|
+
// Don't create our own progress - use parent's
|
|
53
|
+
} else {
|
|
54
|
+
progress = this.createSimpleProgress(PROCESS_NAMES.PROJECTS, projectsCount);
|
|
55
|
+
log.debug('Created standalone progress manager for projects import', this.config.context);
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
await this.init();
|
|
54
|
-
|
|
55
|
-
for (const project of
|
|
59
|
+
|
|
60
|
+
for (const project of this.projectsData) {
|
|
61
|
+
if (!this.parentProgressManager) {
|
|
62
|
+
progress.updateStatus(IMPORT_PROCESS_STATUS[PROCESS_NAMES.PROJECTS].CREATING);
|
|
63
|
+
}
|
|
56
64
|
log.debug(`Processing project: ${project.name}`, this.config.context);
|
|
57
|
-
|
|
65
|
+
|
|
58
66
|
const createProject = async (newName: void | string): Promise<ProjectStruct> => {
|
|
59
67
|
log.debug(`Creating project with name: ${newName || project.name}`, this.config.context);
|
|
60
|
-
|
|
68
|
+
|
|
61
69
|
return await this.createProject({
|
|
62
70
|
name: newName || project.name,
|
|
63
71
|
description: project.description,
|
|
@@ -68,26 +76,88 @@ export default class Project extends PersonalizationAdapter<ImportConfig> {
|
|
|
68
76
|
error.includes('personalize.PROJECTS.DUPLICATE_NAME')
|
|
69
77
|
) {
|
|
70
78
|
log.warn(`Project name already exists, generating new name`, this.config.context);
|
|
79
|
+
|
|
80
|
+
// Prevent progress bar corruption with clean newlines
|
|
81
|
+
cliux.print('\n');
|
|
71
82
|
const projectName = await askProjectName('Copy Of ' + (newName || project.name));
|
|
83
|
+
cliux.print('\n');
|
|
84
|
+
if (this.parentProgressManager) {
|
|
85
|
+
this.parentProgressManager.updateStatus(
|
|
86
|
+
IMPORT_PROCESS_STATUS[PROCESS_NAMES.PROJECTS].CREATING,
|
|
87
|
+
PROCESS_NAMES.PROJECTS,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
return await createProject(projectName);
|
|
73
92
|
}
|
|
74
93
|
throw error;
|
|
75
94
|
});
|
|
76
95
|
};
|
|
77
96
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
try {
|
|
98
|
+
const projectRes = await createProject(this.config.personalizeProjectName);
|
|
99
|
+
this.config.modules.personalize.project_id = projectRes.uid;
|
|
100
|
+
this.config.modules.personalize.importData = true;
|
|
101
|
+
|
|
102
|
+
await fsUtil.makeDirectory(this.projectMapperFolderPath);
|
|
103
|
+
fsUtil.writeFile(pResolve(sanitizePath(this.projectMapperFolderPath), 'projects.json'), projectRes);
|
|
81
104
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
this.updateProgress(true, `project: ${project.name}`, undefined, PROCESS_NAMES.PROJECTS);
|
|
106
|
+
log.success(`Project created successfully: ${projectRes.uid}`, this.config.context);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
this.updateProgress(false, `project: ${project.name}`, (error as any)?.message, PROCESS_NAMES.PROJECTS);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
87
111
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
112
|
+
|
|
113
|
+
// Only complete progress if we own the progress manager (no parent)
|
|
114
|
+
if (!this.parentProgressManager) {
|
|
115
|
+
this.completeProgress(true);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
log.success(
|
|
119
|
+
`Projects imported successfully! Total projects: ${projectsCount} - personalization enabled`,
|
|
120
|
+
this.config.context,
|
|
121
|
+
);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
this.config.modules.personalize.importData = false;
|
|
124
|
+
if (!this.parentProgressManager) {
|
|
125
|
+
this.completeProgress(false, (error as any)?.message || 'Project import failed');
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
91
128
|
}
|
|
92
129
|
}
|
|
130
|
+
|
|
131
|
+
private async analyzeProjects(): Promise<[boolean, number]> {
|
|
132
|
+
return this.withLoadingSpinner('PROJECT: Analyzing import data...', async () => {
|
|
133
|
+
const personalize = this.config.modules.personalize;
|
|
134
|
+
const { dirName, fileName } = personalize.projects;
|
|
135
|
+
const projectPath = join(
|
|
136
|
+
sanitizePath(this.config.data),
|
|
137
|
+
sanitizePath(personalize.dirName),
|
|
138
|
+
sanitizePath(dirName),
|
|
139
|
+
sanitizePath(fileName),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
log.debug(`Checking for project file: ${projectPath}`, this.config.context);
|
|
143
|
+
|
|
144
|
+
if (!existsSync(projectPath)) {
|
|
145
|
+
this.config.modules.personalize.importData = false;
|
|
146
|
+
log.warn(`Project file not found: ${projectPath}`, this.config.context);
|
|
147
|
+
return [false, 0];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.projectsData = JSON.parse(readFileSync(projectPath, 'utf8')) as CreateProjectInput[];
|
|
151
|
+
const projectsCount = this.projectsData?.length || 0;
|
|
152
|
+
|
|
153
|
+
if (projectsCount < 1) {
|
|
154
|
+
this.config.modules.personalize.importData = false;
|
|
155
|
+
log.warn('No projects found in file', this.config.context);
|
|
156
|
+
return [false, 0];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
log.debug(`Found ${projectsCount} projects to import`, this.config.context);
|
|
160
|
+
return [true, projectsCount];
|
|
161
|
+
});
|
|
162
|
+
}
|
|
93
163
|
}
|