@contentstack/cli-variants 1.3.6 → 1.3.8

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.
@@ -37,7 +37,7 @@ class Audiences extends utils_1.PersonalizationAdapter {
37
37
  */
38
38
  import() {
39
39
  return __awaiter(this, void 0, void 0, function* () {
40
- var _a, _b;
40
+ var _a, _b, _c;
41
41
  yield this.init();
42
42
  yield utils_1.fsUtil.makeDirectory(this.audienceMapperDirPath);
43
43
  cli_utilities_1.log.debug(`Created mapper directory: ${this.audienceMapperDirPath}`, this.config.context);
@@ -53,9 +53,14 @@ class Audiences extends utils_1.PersonalizationAdapter {
53
53
  for (const audience of audiences) {
54
54
  let { name, definition, description, uid } = audience;
55
55
  cli_utilities_1.log.debug(`Processing audience: ${name} (${uid})`, this.config.context);
56
+ // Skip Lytics audiences - they cannot be created via API (synced from Lytics)
57
+ if (((_a = audience.source) === null || _a === void 0 ? void 0 : _a.toUpperCase()) === 'LYTICS') {
58
+ cli_utilities_1.log.debug(`Skipping Lytics audience: ${name} (${uid})`, this.config.context);
59
+ continue;
60
+ }
56
61
  try {
57
62
  //check whether reference attributes exists or not
58
- if ((_a = definition.rules) === null || _a === void 0 ? void 0 : _a.length) {
63
+ if ((_b = definition === null || definition === void 0 ? void 0 : definition.rules) === null || _b === void 0 ? void 0 : _b.length) {
59
64
  cli_utilities_1.log.debug(`Processing ${definition.rules.length} definition rules for audience: ${name}`, this.config.context);
60
65
  definition.rules = (0, utils_1.lookUpAttributes)(definition.rules, attributesUid);
61
66
  cli_utilities_1.log.debug(`Processed definition rules, remaining rules: ${definition.rules.length}`, this.config.context);
@@ -67,7 +72,7 @@ class Audiences extends utils_1.PersonalizationAdapter {
67
72
  const audienceRes = yield this.createAudience({ definition, name, description });
68
73
  //map old audience uid to new audience uid
69
74
  //mapper file is used to check whether audience created or not before creating experience
70
- this.audiencesUidMapper[uid] = (_b = audienceRes === null || audienceRes === void 0 ? void 0 : audienceRes.uid) !== null && _b !== void 0 ? _b : '';
75
+ this.audiencesUidMapper[uid] = (_c = audienceRes === null || audienceRes === void 0 ? void 0 : audienceRes.uid) !== null && _c !== void 0 ? _c : '';
71
76
  cli_utilities_1.log.debug(`Created audience: ${uid} -> ${audienceRes === null || audienceRes === void 0 ? void 0 : audienceRes.uid}`, this.config.context);
72
77
  }
73
78
  catch (error) {
@@ -156,12 +156,16 @@ class Experiences extends utils_1.PersonalizationAdapter {
156
156
  };
157
157
  // Process each version and map them by status
158
158
  versions.forEach((version) => {
159
+ var _a, _b, _c, _d;
159
160
  let versionReqObj = (0, utils_1.lookUpAudiences)(version, this.audiencesUid);
160
161
  versionReqObj = (0, utils_1.lookUpEvents)(version, this.eventsUid);
161
- if (versionReqObj && versionReqObj.status) {
162
+ if (versionReqObj && versionReqObj.status && ((_b = (_a = versionReqObj.variants) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0) {
162
163
  versionMap[versionReqObj.status] = versionReqObj;
163
164
  cli_utilities_1.log.debug(`Mapped version with status: ${versionReqObj.status}`, this.config.context);
164
165
  }
166
+ else if ((versionReqObj === null || versionReqObj === void 0 ? void 0 : versionReqObj.status) && !((_d = (_c = versionReqObj.variants) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0)) {
167
+ cli_utilities_1.log.warn(`Skipping version ${versionReqObj.status}: no valid variants (all had unmapped Lytics audiences)`, this.config.context);
168
+ }
165
169
  });
166
170
  // Prioritize updating or creating versions based on the order: ACTIVE -> DRAFT -> PAUSE
167
171
  return yield this.handleVersionUpdateOrCreate(experience, versionMap);
@@ -30,7 +30,7 @@ function updateAudiences(audiences, audiencesUid) {
30
30
  * @returns
31
31
  */
32
32
  const lookUpAudiences = (experience, audiencesUid) => {
33
- var _a, _b, _c, _d, _e, _f, _g, _h;
33
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
34
34
  cli_utilities_1.log.debug('Starting audience lookup for experience');
35
35
  cli_utilities_1.log.debug(`Available audience mappings: ${(_a = Object.keys(audiencesUid)) === null || _a === void 0 ? void 0 : _a.length}`);
36
36
  // Update experience variations
@@ -57,10 +57,13 @@ const lookUpAudiences = (experience, audiencesUid) => {
57
57
  for (let index = experience.variants.length - 1; index >= 0; index--) {
58
58
  const expVariations = experience.variants[index];
59
59
  cli_utilities_1.log.debug(`Processing variant ${index + 1}/${experience.variants.length} of type: ${expVariations['__type']}`);
60
- if (expVariations['__type'] === 'SegmentedVariant' && ((_d = expVariations === null || expVariations === void 0 ? void 0 : expVariations.audiences) === null || _d === void 0 ? void 0 : _d.length)) {
61
- cli_utilities_1.log.debug(`Found ${expVariations.audiences.length} audiences in SegmentedVariant`);
62
- updateAudiences(expVariations.audiences, audiencesUid);
63
- if (!expVariations.audiences.length) {
60
+ if (expVariations['__type'] === 'SegmentedVariant' && (((_d = expVariations === null || expVariations === void 0 ? void 0 : expVariations.audiences) === null || _d === void 0 ? void 0 : _d.length) || ((_e = expVariations === null || expVariations === void 0 ? void 0 : expVariations.lyticsAudiences) === null || _e === void 0 ? void 0 : _e.length))) {
61
+ cli_utilities_1.log.debug(`Found ${(_g = (_f = expVariations.audiences) === null || _f === void 0 ? void 0 : _f.length) !== null && _g !== void 0 ? _g : 0} audiences in SegmentedVariant`);
62
+ if ((_h = expVariations === null || expVariations === void 0 ? void 0 : expVariations.audiences) === null || _h === void 0 ? void 0 : _h.length)
63
+ updateAudiences(expVariations.audiences, audiencesUid);
64
+ if ((_j = expVariations === null || expVariations === void 0 ? void 0 : expVariations.lyticsAudiences) === null || _j === void 0 ? void 0 : _j.length)
65
+ updateAudiences(expVariations.lyticsAudiences, audiencesUid);
66
+ if (!(((_k = expVariations.audiences) === null || _k === void 0 ? void 0 : _k.length) || ((_l = expVariations === null || expVariations === void 0 ? void 0 : expVariations.lyticsAudiences) === null || _l === void 0 ? void 0 : _l.length))) {
64
67
  cli_utilities_1.log.warn('No audiences remaining after mapping. Removing variant.');
65
68
  experience.variants.splice(index, 1);
66
69
  }
@@ -73,7 +76,7 @@ const lookUpAudiences = (experience, audiencesUid) => {
73
76
  else {
74
77
  cli_utilities_1.log.debug('No variations or variants found in experience');
75
78
  }
76
- if (((_e = experience === null || experience === void 0 ? void 0 : experience.targeting) === null || _e === void 0 ? void 0 : _e.hasOwnProperty('audience')) && ((_h = (_g = (_f = experience === null || experience === void 0 ? void 0 : experience.targeting) === null || _f === void 0 ? void 0 : _f.audience) === null || _g === void 0 ? void 0 : _g.audiences) === null || _h === void 0 ? void 0 : _h.length)) {
79
+ if (((_m = experience === null || experience === void 0 ? void 0 : experience.targeting) === null || _m === void 0 ? void 0 : _m.hasOwnProperty('audience')) && ((_q = (_p = (_o = experience === null || experience === void 0 ? void 0 : experience.targeting) === null || _o === void 0 ? void 0 : _o.audience) === null || _p === void 0 ? void 0 : _p.audiences) === null || _q === void 0 ? void 0 : _q.length)) {
77
80
  cli_utilities_1.log.debug(`Processing ${experience.targeting.audience.audiences.length} targeting audiences`);
78
81
  // Update targeting audiences
79
82
  updateAudiences(experience.targeting.audience.audiences, audiencesUid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentstack/cli-variants",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "description": "Variants plugin",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -27,11 +27,11 @@
27
27
  "typescript": "^5.8.3"
28
28
  },
29
29
  "dependencies": {
30
- "@contentstack/cli-utilities": "~1.16.0",
30
+ "@contentstack/cli-utilities": "~1.17.4",
31
31
  "@oclif/core": "^4.3.0",
32
32
  "@oclif/plugin-help": "^6.2.28",
33
- "lodash": "^4.17.21",
33
+ "lodash": "^4.17.23",
34
34
  "mkdirp": "^1.0.4",
35
35
  "winston": "^3.17.0"
36
36
  }
37
- }
37
+ }
@@ -70,10 +70,16 @@ export default class Audiences extends PersonalizationAdapter<ImportConfig> {
70
70
  for (const audience of audiences) {
71
71
  let { name, definition, description, uid } = audience;
72
72
  log.debug(`Processing audience: ${name} (${uid})`, this.config.context);
73
-
73
+
74
+ // Skip Lytics audiences - they cannot be created via API (synced from Lytics)
75
+ if (audience.source?.toUpperCase() === 'LYTICS') {
76
+ log.debug(`Skipping Lytics audience: ${name} (${uid})`, this.config.context);
77
+ continue;
78
+ }
79
+
74
80
  try {
75
81
  //check whether reference attributes exists or not
76
- if (definition.rules?.length) {
82
+ if (definition?.rules?.length) {
77
83
  log.debug(`Processing ${definition.rules.length} definition rules for audience: ${name}`, this.config.context);
78
84
  definition.rules = lookUpAttributes(definition.rules, attributesUid);
79
85
  log.debug(`Processed definition rules, remaining rules: ${definition.rules.length}`, this.config.context);
@@ -203,9 +203,11 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
203
203
  let versionReqObj = lookUpAudiences(version, this.audiencesUid) as CreateExperienceVersionInput;
204
204
  versionReqObj = lookUpEvents(version, this.eventsUid) as CreateExperienceVersionInput;
205
205
 
206
- if (versionReqObj && versionReqObj.status) {
206
+ if (versionReqObj && versionReqObj.status && (versionReqObj.variants?.length ?? 0) > 0) {
207
207
  versionMap[versionReqObj.status] = versionReqObj;
208
208
  log.debug(`Mapped version with status: ${versionReqObj.status}`, this.config.context);
209
+ } else if (versionReqObj?.status && !(versionReqObj.variants?.length ?? 0)) {
210
+ log.warn(`Skipping version ${versionReqObj.status}: no valid variants (all had unmapped Lytics audiences)`, this.config.context);
209
211
  }
210
212
  });
211
213
 
@@ -65,11 +65,11 @@ export const lookUpAudiences = (
65
65
  const expVariations = experience.variants[index];
66
66
  log.debug(`Processing variant ${index + 1}/${experience.variants.length} of type: ${expVariations['__type']}`);
67
67
 
68
- if (expVariations['__type'] === 'SegmentedVariant' && expVariations?.audiences?.length) {
69
- log.debug(`Found ${expVariations.audiences.length} audiences in SegmentedVariant`);
70
- updateAudiences(expVariations.audiences, audiencesUid);
71
-
72
- if (!expVariations.audiences.length) {
68
+ if (expVariations['__type'] === 'SegmentedVariant' && (expVariations?.audiences?.length || expVariations?.lyticsAudiences?.length)) {
69
+ log.debug(`Found ${expVariations.audiences?.length ?? 0} audiences in SegmentedVariant`);
70
+ if (expVariations?.audiences?.length) updateAudiences(expVariations.audiences, audiencesUid);
71
+ if (expVariations?.lyticsAudiences?.length) updateAudiences(expVariations.lyticsAudiences, audiencesUid);
72
+ if (!(expVariations.audiences?.length || expVariations?.lyticsAudiences?.length)) {
73
73
  log.warn('No audiences remaining after mapping. Removing variant.');
74
74
  experience.variants.splice(index, 1);
75
75
  }
@@ -0,0 +1,118 @@
1
+ import { expect } from '@oclif/test';
2
+ import cloneDeep from 'lodash/cloneDeep';
3
+ import { fancy } from '@contentstack/cli-dev-dependencies';
4
+
5
+ import importConf from '../mock/import-config.json';
6
+ import { Import, ImportConfig } from '../../../src';
7
+
8
+ describe('Audiences Import', () => {
9
+ let config: ImportConfig;
10
+ let createAudienceCalls: Array<{ name: string }> = [];
11
+
12
+ const test = fancy.stdout({ print: process.env.PRINT === 'true' || false });
13
+
14
+ beforeEach(() => {
15
+ config = cloneDeep(importConf) as unknown as ImportConfig;
16
+ createAudienceCalls = [];
17
+ // Audiences uses modules.personalize and region - add them for tests
18
+ config.modules.personalize = {
19
+ ...(config.modules as any).personalization,
20
+ dirName: 'personalize',
21
+ baseURL: {
22
+ na: 'https://personalization.na-api.contentstack.com',
23
+ eu: 'https://personalization.eu-api.contentstack.com',
24
+ },
25
+ } as any;
26
+ config.region = { name: 'eu' } as any;
27
+ config.context = config.context || {};
28
+ });
29
+
30
+ describe('import method - Lytics audience skip', () => {
31
+ test
32
+ .stub(Import.Audiences.prototype, 'init', async () => {})
33
+ .stub(Import.Audiences.prototype, 'createAudience', (async (payload: any) => {
34
+ createAudienceCalls.push({ name: payload.name });
35
+ return { uid: `new-${payload.name.replace(/\s/g, '-')}`, name: payload.name };
36
+ }) as any)
37
+ .it('should skip Lytics audiences and not call createAudience for them', async () => {
38
+ const audiencesInstance = new Import.Audiences(config);
39
+ await audiencesInstance.import();
40
+
41
+ const lyticsNames = createAudienceCalls.filter(
42
+ (c) => c.name === 'Lytics Audience' || c.name === 'Lytics Lowercase',
43
+ );
44
+ expect(lyticsNames.length).to.equal(0);
45
+ });
46
+
47
+ test
48
+ .stub(Import.Audiences.prototype, 'init', async () => {})
49
+ .stub(Import.Audiences.prototype, 'createAudience', (async (payload: any) => {
50
+ createAudienceCalls.push({ name: payload.name });
51
+ return { uid: `new-${payload.name.replace(/\s/g, '-')}`, name: payload.name };
52
+ }) as any)
53
+ .it('should process audiences with undefined source', async () => {
54
+ const audiencesInstance = new Import.Audiences(config);
55
+ await audiencesInstance.import();
56
+
57
+ const noSourceCall = createAudienceCalls.find((c) => c.name === 'No Source Audience');
58
+ expect(noSourceCall).to.not.be.undefined;
59
+ });
60
+
61
+ test
62
+ .stub(Import.Audiences.prototype, 'init', async () => {})
63
+ .stub(Import.Audiences.prototype, 'createAudience', (async (payload: any) => {
64
+ createAudienceCalls.push({ name: payload.name });
65
+ return { uid: `new-${payload.name.replace(/\s/g, '-')}`, name: payload.name };
66
+ }) as any)
67
+ .it('should skip audience with source "lytics" (lowercase)', async () => {
68
+ const audiencesInstance = new Import.Audiences(config);
69
+ await audiencesInstance.import();
70
+
71
+ const lyticsLowercaseCall = createAudienceCalls.find((c) => c.name === 'Lytics Lowercase');
72
+ expect(lyticsLowercaseCall).to.be.undefined;
73
+ });
74
+
75
+ test
76
+ .stub(Import.Audiences.prototype, 'init', async () => {})
77
+ .stub(Import.Audiences.prototype, 'createAudience', (async (payload: any) => {
78
+ createAudienceCalls.push({ name: payload.name });
79
+ return { uid: `new-uid-${payload.name}`, name: payload.name };
80
+ }) as any)
81
+ .it('should call createAudience only for non-Lytics audiences', async () => {
82
+ const audiencesInstance = new Import.Audiences(config);
83
+ await audiencesInstance.import();
84
+
85
+ // 4 audiences in mock: 2 Lytics (skip), 2 non-Lytics (Contentstack Test, No Source)
86
+ expect(createAudienceCalls.length).to.equal(2);
87
+ });
88
+
89
+ test
90
+ .stub(Import.Audiences.prototype, 'init', async () => {})
91
+ .stub(Import.Audiences.prototype, 'createAudience', (async (payload: any) => {
92
+ createAudienceCalls.push({ name: payload.name });
93
+ return { uid: 'new-contentstack-uid', name: payload.name };
94
+ }) as any)
95
+ .it('should not add Lytics audiences to audiencesUidMapper', async () => {
96
+ const audiencesInstance = new Import.Audiences(config);
97
+ await audiencesInstance.import();
98
+
99
+ const mapper = (audiencesInstance as any).audiencesUidMapper;
100
+ expect(mapper['lytics-audience-001']).to.be.undefined;
101
+ expect(mapper['lytics-lowercase-001']).to.be.undefined;
102
+ });
103
+
104
+ test
105
+ .stub(Import.Audiences.prototype, 'init', async () => {})
106
+ .stub(Import.Audiences.prototype, 'createAudience', (async (payload: any) => {
107
+ createAudienceCalls.push({ name: payload.name });
108
+ return { uid: 'new-contentstack-uid', name: payload.name };
109
+ }) as any)
110
+ .it('should add Contentstack audiences to audiencesUidMapper', async () => {
111
+ const audiencesInstance = new Import.Audiences(config);
112
+ await audiencesInstance.import();
113
+
114
+ const mapper = (audiencesInstance as any).audiencesUidMapper;
115
+ expect(mapper['contentstack-audience-001']).to.equal('new-contentstack-uid');
116
+ });
117
+ });
118
+ });
@@ -0,0 +1 @@
1
+ {"contentstack-audience-001":"new-contentstack-uid","no-source-audience-001":"new-contentstack-uid"}
@@ -0,0 +1,44 @@
1
+ [
2
+ {
3
+ "uid": "contentstack-audience-001",
4
+ "name": "Contentstack Test Audience",
5
+ "description": "Audience with rules",
6
+ "definition": {
7
+ "__type": "RuleCombination",
8
+ "combinationType": "AND",
9
+ "rules": [
10
+ {
11
+ "__type": "Rule",
12
+ "attribute": { "__type": "PresetAttributeReference", "ref": "DEVICE_TYPE" },
13
+ "attributeMatchOptions": { "__type": "StringMatchOptions", "value": "MOBILE" },
14
+ "attributeMatchCondition": "STRING_EQUALS",
15
+ "invertCondition": false
16
+ }
17
+ ]
18
+ }
19
+ },
20
+ {
21
+ "uid": "lytics-audience-001",
22
+ "name": "Lytics Audience",
23
+ "description": "From Lytics",
24
+ "slug": "lytics_audience",
25
+ "source": "LYTICS"
26
+ },
27
+ {
28
+ "uid": "lytics-lowercase-001",
29
+ "name": "Lytics Lowercase",
30
+ "description": "source is lowercase",
31
+ "slug": "lytics_lowercase",
32
+ "source": "lytics"
33
+ },
34
+ {
35
+ "uid": "no-source-audience-001",
36
+ "name": "No Source Audience",
37
+ "description": "Audience without source field",
38
+ "definition": {
39
+ "__type": "RuleCombination",
40
+ "combinationType": "AND",
41
+ "rules": []
42
+ }
43
+ }
44
+ ]