@contentstack/cli-variants 0.0.1-alpha

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 (124) hide show
  1. package/lib/export/attributes.d.ts +15 -0
  2. package/lib/export/attributes.js +62 -0
  3. package/lib/export/audiences.d.ts +15 -0
  4. package/lib/export/audiences.js +63 -0
  5. package/lib/export/events.d.ts +15 -0
  6. package/lib/export/events.js +63 -0
  7. package/lib/export/experiences.d.ts +9 -0
  8. package/lib/export/experiences.js +99 -0
  9. package/lib/export/index.d.ts +9 -0
  10. package/lib/export/index.js +21 -0
  11. package/lib/export/projects.d.ts +9 -0
  12. package/lib/export/projects.js +73 -0
  13. package/lib/export/variant-entries.d.ts +18 -0
  14. package/lib/export/variant-entries.js +109 -0
  15. package/lib/import/attribute.d.ts +17 -0
  16. package/lib/import/attribute.js +64 -0
  17. package/lib/import/audiences.d.ts +19 -0
  18. package/lib/import/audiences.js +71 -0
  19. package/lib/import/events.d.ts +17 -0
  20. package/lib/import/events.js +62 -0
  21. package/lib/import/experiences.d.ts +46 -0
  22. package/lib/import/experiences.js +214 -0
  23. package/lib/import/index.d.ts +14 -0
  24. package/lib/import/index.js +21 -0
  25. package/lib/import/project.d.ts +13 -0
  26. package/lib/import/project.js +74 -0
  27. package/lib/import/variant-entries.d.ts +98 -0
  28. package/lib/import/variant-entries.js +407 -0
  29. package/lib/index.d.ts +5 -0
  30. package/lib/index.js +21 -0
  31. package/lib/messages/index.d.ts +35 -0
  32. package/lib/messages/index.js +55 -0
  33. package/lib/types/adapter-helper.d.ts +8 -0
  34. package/lib/types/adapter-helper.js +2 -0
  35. package/lib/types/content-types.d.ts +19 -0
  36. package/lib/types/content-types.js +2 -0
  37. package/lib/types/export-config.d.ts +264 -0
  38. package/lib/types/export-config.js +2 -0
  39. package/lib/types/import-config.d.ts +92 -0
  40. package/lib/types/import-config.js +2 -0
  41. package/lib/types/index.d.ts +8 -0
  42. package/lib/types/index.js +24 -0
  43. package/lib/types/personalization-api-adapter.d.ts +152 -0
  44. package/lib/types/personalization-api-adapter.js +2 -0
  45. package/lib/types/utils.d.ts +7 -0
  46. package/lib/types/utils.js +2 -0
  47. package/lib/types/variant-api-adapter.d.ts +49 -0
  48. package/lib/types/variant-api-adapter.js +2 -0
  49. package/lib/types/variant-entry.d.ts +47 -0
  50. package/lib/types/variant-entry.js +2 -0
  51. package/lib/utils/adapter-helper.d.ts +30 -0
  52. package/lib/utils/adapter-helper.js +95 -0
  53. package/lib/utils/attributes-helper.d.ts +7 -0
  54. package/lib/utils/attributes-helper.js +37 -0
  55. package/lib/utils/audiences-helper.d.ts +8 -0
  56. package/lib/utils/audiences-helper.js +49 -0
  57. package/lib/utils/error-helper.d.ts +6 -0
  58. package/lib/utils/error-helper.js +27 -0
  59. package/lib/utils/events-helper.d.ts +8 -0
  60. package/lib/utils/events-helper.js +27 -0
  61. package/lib/utils/helper.d.ts +4 -0
  62. package/lib/utils/helper.js +51 -0
  63. package/lib/utils/index.d.ts +9 -0
  64. package/lib/utils/index.js +25 -0
  65. package/lib/utils/logger.d.ts +3 -0
  66. package/lib/utils/logger.js +175 -0
  67. package/lib/utils/personalization-api-adapter.d.ts +73 -0
  68. package/lib/utils/personalization-api-adapter.js +184 -0
  69. package/lib/utils/variant-api-adapter.d.ts +79 -0
  70. package/lib/utils/variant-api-adapter.js +263 -0
  71. package/package.json +38 -0
  72. package/src/export/attributes.ts +55 -0
  73. package/src/export/audiences.ts +57 -0
  74. package/src/export/events.ts +57 -0
  75. package/src/export/experiences.ts +80 -0
  76. package/src/export/index.ts +11 -0
  77. package/src/export/projects.ts +45 -0
  78. package/src/export/variant-entries.ts +88 -0
  79. package/src/import/attribute.ts +60 -0
  80. package/src/import/audiences.ts +69 -0
  81. package/src/import/events.ts +58 -0
  82. package/src/import/experiences.ts +224 -0
  83. package/src/import/index.ts +16 -0
  84. package/src/import/project.ts +71 -0
  85. package/src/import/variant-entries.ts +483 -0
  86. package/src/index.ts +5 -0
  87. package/src/messages/index.ts +63 -0
  88. package/src/types/adapter-helper.ts +10 -0
  89. package/src/types/content-types.ts +41 -0
  90. package/src/types/export-config.ts +292 -0
  91. package/src/types/import-config.ts +95 -0
  92. package/src/types/index.ts +8 -0
  93. package/src/types/personalization-api-adapter.ts +197 -0
  94. package/src/types/utils.ts +8 -0
  95. package/src/types/variant-api-adapter.ts +56 -0
  96. package/src/types/variant-entry.ts +61 -0
  97. package/src/utils/adapter-helper.ts +79 -0
  98. package/src/utils/attributes-helper.ts +31 -0
  99. package/src/utils/audiences-helper.ts +50 -0
  100. package/src/utils/error-helper.ts +26 -0
  101. package/src/utils/events-helper.ts +26 -0
  102. package/src/utils/helper.ts +34 -0
  103. package/src/utils/index.ts +9 -0
  104. package/src/utils/logger.ts +160 -0
  105. package/src/utils/personalization-api-adapter.ts +188 -0
  106. package/src/utils/variant-api-adapter.ts +326 -0
  107. package/test/unit/export/variant-entries.test.ts +80 -0
  108. package/test/unit/import/variant-entries.test.ts +200 -0
  109. package/test/unit/mock/contents/content_types/CT-1.json +7 -0
  110. package/test/unit/mock/contents/entries/CT-1/en-us/variants/E-1/9b0da6xd7et72y-6gv7he23.json +12 -0
  111. package/test/unit/mock/contents/entries/CT-1/en-us/variants/E-1/index.json +3 -0
  112. package/test/unit/mock/contents/entries/CT-1/en-us/variants/E-2/9b0da6xd7et72y-6gv7he23.json +12 -0
  113. package/test/unit/mock/contents/entries/CT-1/en-us/variants/E-2/index.json +3 -0
  114. package/test/unit/mock/contents/mapper/assets/uid-mapping.json +6 -0
  115. package/test/unit/mock/contents/mapper/assets/url-mapping.json +6 -0
  116. package/test/unit/mock/contents/mapper/entries/data-for-variant-entry.json +6 -0
  117. package/test/unit/mock/contents/mapper/entries/empty-data/data-for-variant-entry.json +1 -0
  118. package/test/unit/mock/contents/mapper/entries/uid-mapping.json +6 -0
  119. package/test/unit/mock/contents/mapper/marketplace_apps/uid-mapping.json +3 -0
  120. package/test/unit/mock/contents/mapper/personalization/experiences/variants-uid-mapping.json +5 -0
  121. package/test/unit/mock/contents/mapper/taxonomies/terms/success.json +1 -0
  122. package/test/unit/mock/export-config.json +48 -0
  123. package/test/unit/mock/import-config.json +63 -0
  124. package/tsconfig.json +19 -0
@@ -0,0 +1,224 @@
1
+ import { join, resolve } from 'path';
2
+ import { existsSync } from 'fs';
3
+ import values from 'lodash/values';
4
+ import cloneDeep from 'lodash/cloneDeep';
5
+ import { sanitizePath } from '@contentstack/cli-utilities';
6
+ import { PersonalizationAdapter, fsUtil, lookUpAudiences, lookUpEvents } from '../utils';
7
+ import { APIConfig, ImportConfig, ExperienceStruct, CreateExperienceInput, LogType } from '../types';
8
+
9
+ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
10
+ private createdCTs: string[];
11
+ private mapperDirPath: string;
12
+ private cmsVariantPath: string;
13
+ private cTsSuccessPath: string;
14
+ private failedCmsExpPath: string;
15
+ private expMapperDirPath: string;
16
+ private eventsMapperPath: string;
17
+ private experiencesPath: string;
18
+ private experiencesDirPath: string;
19
+ private audiencesMapperPath: string;
20
+ private cmsVariantGroupPath: string;
21
+ private experienceVariantsIdsPath: string;
22
+ private variantUidMapperFilePath: string;
23
+ private expThresholdTimer: number;
24
+ private maxValidateRetry: number;
25
+ private experiencesUidMapperPath: string;
26
+ private experienceCTsPath: string;
27
+ private expCheckIntervalDuration: number;
28
+ private cmsVariants: Record<string, Record<string, string>>;
29
+ private cmsVariantGroups: Record<string, unknown>;
30
+ private experiencesUidMapper: Record<string, string>;
31
+ private pendingVariantAndVariantGrpForExperience: string[];
32
+ private personalizationConfig: ImportConfig['modules']['personalization'];
33
+ private audienceConfig: ImportConfig['modules']['personalization']['audiences'];
34
+ private experienceConfig: ImportConfig['modules']['personalization']['experiences'];
35
+
36
+ constructor(public readonly config: ImportConfig, private readonly log: LogType = console.log) {
37
+ const conf: APIConfig = {
38
+ config,
39
+ baseURL: config.modules.personalization.baseURL[config.region.name],
40
+ headers: { 'X-Project-Uid': config.modules.personalization.project_id, authtoken: config.auth_token },
41
+ cmaConfig: {
42
+ baseURL: config.region.cma + `/v3`,
43
+ headers: { authtoken: config.auth_token, api_key: config.apiKey },
44
+ },
45
+ };
46
+ super(Object.assign(config, conf));
47
+ this.personalizationConfig = this.config.modules.personalization;
48
+ this.experiencesDirPath = resolve(
49
+ sanitizePath(this.config.data),
50
+ sanitizePath(this.personalizationConfig.dirName),
51
+ sanitizePath(this.personalizationConfig.experiences.dirName),
52
+ );
53
+ this.experiencesPath = join(sanitizePath(this.experiencesDirPath), sanitizePath(this.personalizationConfig.experiences.fileName));
54
+ this.experienceConfig = this.personalizationConfig.experiences;
55
+ this.audienceConfig = this.personalizationConfig.audiences;
56
+ this.mapperDirPath = resolve(sanitizePath(this.config.backupDir), 'mapper', sanitizePath(this.personalizationConfig.dirName));
57
+ this.expMapperDirPath = resolve(sanitizePath(this.mapperDirPath), sanitizePath(this.experienceConfig.dirName));
58
+ this.experiencesUidMapperPath = resolve(sanitizePath(this.expMapperDirPath), 'uid-mapping.json');
59
+ this.cmsVariantGroupPath = resolve(sanitizePath(this.expMapperDirPath), 'cms-variant-groups.json');
60
+ this.cmsVariantPath = resolve(sanitizePath(this.expMapperDirPath), 'cms-variants.json');
61
+ this.audiencesMapperPath = resolve(sanitizePath(this.mapperDirPath), sanitizePath(this.audienceConfig.dirName), 'uid-mapping.json');
62
+ this.eventsMapperPath = resolve(sanitizePath(this.mapperDirPath), 'events', 'uid-mapping.json');
63
+ this.failedCmsExpPath = resolve(sanitizePath(this.expMapperDirPath), 'failed-cms-experience.json');
64
+ this.failedCmsExpPath = resolve(sanitizePath(this.expMapperDirPath), 'failed-cms-experience.json');
65
+ this.experienceCTsPath = resolve(sanitizePath(this.experiencesDirPath), 'experiences-content-types.json');
66
+ this.experienceVariantsIdsPath = resolve(
67
+ sanitizePath(this.config.data),
68
+ sanitizePath(this.personalizationConfig.dirName),
69
+ sanitizePath(this.experienceConfig.dirName),
70
+ 'experiences-variants-ids.json',
71
+ );
72
+ this.variantUidMapperFilePath = resolve(sanitizePath(this.expMapperDirPath), 'variants-uid-mapping.json');
73
+ this.experiencesUidMapper = {};
74
+ this.cmsVariantGroups = {};
75
+ this.cmsVariants = {};
76
+ this.expThresholdTimer = this.experienceConfig?.thresholdTimer ?? 30000;
77
+ this.expCheckIntervalDuration = this.experienceConfig?.checkIntervalDuration ?? 5000;
78
+ this.maxValidateRetry = Math.round(this.expThresholdTimer / this.expCheckIntervalDuration);
79
+ this.pendingVariantAndVariantGrpForExperience = [];
80
+ this.cTsSuccessPath = resolve(sanitizePath(this.config.backupDir), 'mapper', 'content_types', 'success.json');
81
+ this.createdCTs = [];
82
+ }
83
+
84
+ /**
85
+ * The function asynchronously imports experiences from a JSON file and creates them in the system.
86
+ */
87
+ async import() {
88
+ this.log(this.config, this.$t(this.messages.IMPORT_MSG, { module: 'Experiences' }), 'info');
89
+
90
+ await fsUtil.makeDirectory(this.expMapperDirPath);
91
+
92
+ if (existsSync(this.experiencesPath)) {
93
+ try {
94
+ const experiences = fsUtil.readFile(this.experiencesPath, true) as ExperienceStruct[];
95
+ const audiencesUid = (fsUtil.readFile(this.audiencesMapperPath, true) as Record<string, string>) || {};
96
+ const eventsUid = (fsUtil.readFile(this.eventsMapperPath, true) as Record<string, string>) || {};
97
+
98
+ for (const experience of experiences) {
99
+ const { uid, ...restExperienceData } = experience;
100
+ //check whether reference audience exists or not that referenced in variations having __type equal to AudienceBasedVariation & targeting
101
+ let experienceReqObj: CreateExperienceInput = lookUpAudiences(restExperienceData, audiencesUid);
102
+ //check whether events exists or not that referenced in metrics
103
+ experienceReqObj = lookUpEvents(experienceReqObj, eventsUid);
104
+
105
+ const expRes = await this.createExperience(experienceReqObj);
106
+ //map old experience uid to new experience uid
107
+ this.experiencesUidMapper[uid] = expRes?.uid ?? '';
108
+ }
109
+ fsUtil.writeFile(this.experiencesUidMapperPath, this.experiencesUidMapper);
110
+ this.log(this.config, this.$t(this.messages.CREATE_SUCCESS, { module: 'Experiences' }), 'info');
111
+
112
+ this.log(this.config, this.messages.VALIDATE_VARIANT_AND_VARIANT_GRP, 'info');
113
+ this.pendingVariantAndVariantGrpForExperience = values(cloneDeep(this.experiencesUidMapper));
114
+ const jobRes = await this.validateVariantGroupAndVariantsCreated();
115
+ fsUtil.writeFile(this.cmsVariantPath, this.cmsVariants);
116
+ fsUtil.writeFile(this.cmsVariantGroupPath, this.cmsVariantGroups);
117
+ if (jobRes)
118
+ this.log(this.config, this.$t(this.messages.CREATE_SUCCESS, { module: 'Variant & Variant groups' }), 'info');
119
+
120
+ if (this.personalizationConfig.importData) {
121
+ this.log(this.config, this.messages.UPDATING_CT_IN_EXP, 'info');
122
+ await this.attachCTsInExperience();
123
+ }
124
+
125
+ await this.createVariantIdMapper();
126
+ } catch (error) {
127
+ this.log(this.config, this.$t(this.messages.CREATE_FAILURE, { module: 'Experiences' }), 'error');
128
+ this.log(this.config, error, 'error');
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * function to validate if all variant groups and variants have been created using personalization background job
135
+ * store the variant groups data in mapper/personalization/experiences/cms-variant-groups.json and the variants data
136
+ * in mapper/personalization/experiences/cms-variants.json. If not, invoke validateVariantGroupAndVariantsCreated after some delay.
137
+ * @param retryCount Counter to track the number of times the function has been called
138
+ * @returns
139
+ */
140
+ async validateVariantGroupAndVariantsCreated(retryCount = 0): Promise<any> {
141
+ try {
142
+ const promises = this.pendingVariantAndVariantGrpForExperience.map(async (expUid) => {
143
+ const expRes = await this.getExperience(expUid);
144
+ if (expRes?._cms) {
145
+ this.cmsVariants[expUid] = expRes._cms?.variants ?? {};
146
+ this.cmsVariantGroups[expUid] = expRes._cms?.variantGroup ?? {};
147
+ return expUid; // Return the expUid for filtering later
148
+ }
149
+ });
150
+
151
+ await Promise.all(promises);
152
+ retryCount++;
153
+
154
+ if (this.pendingVariantAndVariantGrpForExperience?.length) {
155
+ if (retryCount !== this.maxValidateRetry) {
156
+ await this.delay(this.expCheckIntervalDuration);
157
+ // Filter out the processed elements
158
+ this.pendingVariantAndVariantGrpForExperience = this.pendingVariantAndVariantGrpForExperience.filter(
159
+ (uid) => !this.cmsVariants[uid],
160
+ );
161
+ return this.validateVariantGroupAndVariantsCreated(retryCount);
162
+ } else {
163
+ this.log(this.config, this.messages.PERSONALIZATION_JOB_FAILURE, 'error');
164
+ fsUtil.writeFile(this.failedCmsExpPath, this.pendingVariantAndVariantGrpForExperience);
165
+ return false;
166
+ }
167
+ } else {
168
+ return true;
169
+ }
170
+ } catch (error) {
171
+ throw error;
172
+ }
173
+ }
174
+
175
+ async attachCTsInExperience() {
176
+ try {
177
+ // Read the created content types from the file
178
+ this.createdCTs = fsUtil.readFile(this.cTsSuccessPath, true) as any;
179
+ if (!this.createdCTs) {
180
+ this.log(this.config, 'No Content types created, skipping following process', 'error');
181
+ return;
182
+ }
183
+ const experienceCTsMap = fsUtil.readFile(this.experienceCTsPath, true) as Record<string, string[]>;
184
+ return await Promise.allSettled(
185
+ Object.entries(this.experiencesUidMapper).map(async ([oldExpUid, newExpUid]) => {
186
+ if (experienceCTsMap[oldExpUid]?.length) {
187
+ // Filter content types that were created
188
+ const updatedContentTypes = experienceCTsMap[oldExpUid].filter((ct: any) =>
189
+ this.createdCTs.includes(ct?.uid),
190
+ );
191
+ if (updatedContentTypes?.length) {
192
+ const { variant_groups: [variantGroup] = [] } =
193
+ (await this.getVariantGroup({ experienceUid: newExpUid })) || {};
194
+ variantGroup.content_types = updatedContentTypes;
195
+ // Update content types detail in the new experience asynchronously
196
+ return await this.updateVariantGroup(variantGroup);
197
+ }
198
+ }
199
+ }),
200
+ );
201
+ } catch (error) {
202
+ this.log(this.config, `Error while attaching content type with experience`, 'error');
203
+ this.log(this.config, error, 'error');
204
+ }
205
+ }
206
+
207
+ async createVariantIdMapper() {
208
+ try {
209
+ const experienceVariantIds: any = fsUtil.readFile(this.experienceVariantsIdsPath, true) || [];
210
+ const variantUIDMapper: Record<string, string> = {};
211
+ for (let experienceVariantId of experienceVariantIds) {
212
+ const [experienceId, variantShortId, oldVariantId] = experienceVariantId.split('-');
213
+ const latestVariantId = this.cmsVariants[this.experiencesUidMapper[experienceId]]?.[variantShortId];
214
+ if (latestVariantId) {
215
+ variantUIDMapper[oldVariantId] = latestVariantId;
216
+ }
217
+ }
218
+
219
+ fsUtil.writeFile(this.variantUidMapperFilePath, variantUIDMapper);
220
+ } catch (error) {
221
+ throw error;
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,16 @@
1
+ import Events from './events';
2
+ import Project from './project';
3
+ import Attribute from './attribute';
4
+ import Audiences from './audiences';
5
+ import Experiences from './experiences';
6
+ import VariantEntries from './variant-entries';
7
+
8
+ // NOTE Acting as namespace to avoid the same class name conflicts in other modules
9
+ export const Import = {
10
+ Events,
11
+ Project,
12
+ Attribute,
13
+ Audiences,
14
+ Experiences,
15
+ VariantEntries,
16
+ };
@@ -0,0 +1,71 @@
1
+ import { join, resolve as pResolve } from 'path';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { sanitizePath } from '@contentstack/cli-utilities';
4
+ import { PersonalizationAdapter, askProjectName, fsUtil } from '../utils';
5
+ import { APIConfig, CreateProjectInput, ImportConfig, LogType, ProjectStruct } from '../types';
6
+
7
+ export default class Project extends PersonalizationAdapter<ImportConfig> {
8
+ private projectMapperFolderPath: string;
9
+ constructor(public readonly config: ImportConfig, private readonly log: LogType = console.log) {
10
+ const conf: APIConfig = {
11
+ config,
12
+ baseURL: config.modules.personalization.baseURL[config.region.name],
13
+ headers: { organization_uid: config.org_uid, authtoken: config.auth_token },
14
+ };
15
+ super(Object.assign(config, conf));
16
+ this.projectMapperFolderPath = pResolve(
17
+ sanitizePath(this.config.backupDir),
18
+ 'mapper',
19
+ sanitizePath(this.config.modules.personalization.dirName),
20
+ 'projects',
21
+ );
22
+ }
23
+
24
+ /**
25
+ * The function asynchronously imports projects data from a file and creates projects based on the
26
+ * data.
27
+ */
28
+ async import() {
29
+ const personalization = this.config.modules.personalization;
30
+ const { dirName, fileName } = personalization.projects;
31
+ const projectPath = join(sanitizePath(this.config.data), sanitizePath(personalization.dirName), sanitizePath(dirName), sanitizePath(fileName));
32
+
33
+ if (existsSync(projectPath)) {
34
+ const projects = JSON.parse(readFileSync(projectPath, 'utf8')) as CreateProjectInput[];
35
+
36
+ if (!projects || projects.length < 1) {
37
+ this.config.modules.personalization.importData = false; // Stop personalization import if stack not connected to any project
38
+ this.log(this.config, 'No project found!', 'info');
39
+ return;
40
+ }
41
+
42
+ for (const project of projects) {
43
+ const createProject = async (newName: void | string): Promise<ProjectStruct> => {
44
+ return await this.createProject({
45
+ name: newName || project.name,
46
+ description: project.description,
47
+ connectedStackApiKey: this.config.apiKey,
48
+ }).catch(async (error) => {
49
+ if (error === 'personalization.PROJECTS.DUPLICATE_NAME') {
50
+ const projectName = await askProjectName('Copy Of ' + (newName || project.name));
51
+ return await createProject(projectName);
52
+ }
53
+ this.log(this.config, this.$t(this.messages.CREATE_FAILURE, { module: 'Projects' }), 'error');
54
+ throw error;
55
+ });
56
+ };
57
+
58
+ const projectRes = await createProject(this.config.personalizeProjectName);
59
+ this.config.modules.personalization.project_id = projectRes.uid;
60
+ this.config.modules.personalization.importData = true;
61
+
62
+ await fsUtil.makeDirectory(this.projectMapperFolderPath);
63
+ fsUtil.writeFile(pResolve(sanitizePath(this.projectMapperFolderPath), 'projects.json'), projectRes);
64
+ this.log(this.config, `Project Created Successfully: ${projectRes.uid}`, 'info');
65
+ }
66
+ } else {
67
+ this.config.modules.personalization.importData = false; // Stop personalization import if stack not connected to any project
68
+ this.log(this.config, 'No project found!', 'info');
69
+ }
70
+ }
71
+ }