Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/contentstack-variants/src/export/experiences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default class ExportExperiences extends PersonalizationAdapter<ExportConf
// write experiences in to a file
log(this.exportConfig, 'Starting experiences export', 'info');
await fsUtil.makeDirectory(this.experiencesFolderPath);
await fsUtil.makeDirectory(path.resolve(sanitizePath(this.experiencesFolderPath), 'versions'));
const experiences: Array<ExperienceStruct> = (await this.getExperiences()) || [];
if (!experiences || experiences?.length < 1) {
log(this.exportConfig, 'No Experiences found with the give project', 'info');
Expand All @@ -51,6 +52,21 @@ export default class ExportExperiences extends PersonalizationAdapter<ExportConf
experienceToVariantsStrList.push(experienceToVariantsStr);
});

try {
// fetch versions of experience
const experienceVersions = (await this.getExperienceVersions(experience.uid)) || [];
if (experienceVersions.length > 0) {
fsUtil.writeFile(
path.resolve(sanitizePath(this.experiencesFolderPath), 'versions', `${experience.uid}.json`),
experienceVersions,
);
} else {
log(this.exportConfig, `No versions found for experience ${experience.name}`, 'info');
}
} catch (error) {
log(this.exportConfig, `Failed to fetch versions of experience ${experience.name}`, 'error');
}

try {
// fetch content of experience
const { variant_groups: [variantGroup] = [] } =
Expand Down
108 changes: 99 additions & 9 deletions packages/contentstack-variants/src/import/experiences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import values from 'lodash/values';
import cloneDeep from 'lodash/cloneDeep';
import { sanitizePath } from '@contentstack/cli-utilities';
import { PersonalizationAdapter, fsUtil, lookUpAudiences, lookUpEvents } from '../utils';
import { APIConfig, ImportConfig, ExperienceStruct, CreateExperienceInput, LogType } from '../types';
import {
APIConfig,
ImportConfig,
ExperienceStruct,
CreateExperienceInput,
LogType,
CreateExperienceVersionInput,
} from '../types';
export default class Experiences extends PersonalizationAdapter<ImportConfig> {
private createdCTs: string[];
private mapperDirPath: string;
Expand All @@ -28,6 +35,8 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
private cmsVariantGroups: Record<string, unknown>;
private experiencesUidMapper: Record<string, string>;
private pendingVariantAndVariantGrpForExperience: string[];
private audiencesUid: Record<string, string>;
private eventsUid: Record<string, string>;
private personalizationConfig: ImportConfig['modules']['personalization'];
private audienceConfig: ImportConfig['modules']['personalization']['audiences'];
private experienceConfig: ImportConfig['modules']['personalization']['experiences'];
Expand All @@ -49,15 +58,26 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
sanitizePath(this.personalizationConfig.dirName),
sanitizePath(this.personalizationConfig.experiences.dirName),
);
this.experiencesPath = join(sanitizePath(this.experiencesDirPath), sanitizePath(this.personalizationConfig.experiences.fileName));
this.experiencesPath = join(
sanitizePath(this.experiencesDirPath),
sanitizePath(this.personalizationConfig.experiences.fileName),
);
this.experienceConfig = this.personalizationConfig.experiences;
this.audienceConfig = this.personalizationConfig.audiences;
this.mapperDirPath = resolve(sanitizePath(this.config.backupDir), 'mapper', sanitizePath(this.personalizationConfig.dirName));
this.mapperDirPath = resolve(
sanitizePath(this.config.backupDir),
'mapper',
sanitizePath(this.personalizationConfig.dirName),
);
this.expMapperDirPath = resolve(sanitizePath(this.mapperDirPath), sanitizePath(this.experienceConfig.dirName));
this.experiencesUidMapperPath = resolve(sanitizePath(this.expMapperDirPath), 'uid-mapping.json');
this.cmsVariantGroupPath = resolve(sanitizePath(this.expMapperDirPath), 'cms-variant-groups.json');
this.cmsVariantPath = resolve(sanitizePath(this.expMapperDirPath), 'cms-variants.json');
this.audiencesMapperPath = resolve(sanitizePath(this.mapperDirPath), sanitizePath(this.audienceConfig.dirName), 'uid-mapping.json');
this.audiencesMapperPath = resolve(
sanitizePath(this.mapperDirPath),
sanitizePath(this.audienceConfig.dirName),
'uid-mapping.json',
);
this.eventsMapperPath = resolve(sanitizePath(this.mapperDirPath), 'events', 'uid-mapping.json');
this.failedCmsExpPath = resolve(sanitizePath(this.expMapperDirPath), 'failed-cms-experience.json');
this.failedCmsExpPath = resolve(sanitizePath(this.expMapperDirPath), 'failed-cms-experience.json');
Expand All @@ -78,6 +98,8 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
this.pendingVariantAndVariantGrpForExperience = [];
this.cTsSuccessPath = resolve(sanitizePath(this.config.backupDir), 'mapper', 'content_types', 'success.json');
this.createdCTs = [];
this.audiencesUid = (fsUtil.readFile(this.audiencesMapperPath, true) as Record<string, string>) || {};
this.eventsUid = (fsUtil.readFile(this.eventsMapperPath, true) as Record<string, string>) || {};
}

/**
Expand All @@ -91,19 +113,25 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
if (existsSync(this.experiencesPath)) {
try {
const experiences = fsUtil.readFile(this.experiencesPath, true) as ExperienceStruct[];
const audiencesUid = (fsUtil.readFile(this.audiencesMapperPath, true) as Record<string, string>) || {};
const eventsUid = (fsUtil.readFile(this.eventsMapperPath, true) as Record<string, string>) || {};

for (const experience of experiences) {
const { uid, ...restExperienceData } = experience;
//check whether reference audience exists or not that referenced in variations having __type equal to AudienceBasedVariation & targeting
let experienceReqObj: CreateExperienceInput = lookUpAudiences(restExperienceData, audiencesUid);
let experienceReqObj: CreateExperienceInput = lookUpAudiences(restExperienceData, this.audiencesUid);
//check whether events exists or not that referenced in metrics
experienceReqObj = lookUpEvents(experienceReqObj, eventsUid);
experienceReqObj = lookUpEvents(experienceReqObj, this.eventsUid);

const expRes = await this.createExperience(experienceReqObj);
const expRes = (await this.createExperience(experienceReqObj)) as ExperienceStruct;
//map old experience uid to new experience uid
this.experiencesUidMapper[uid] = expRes?.uid ?? '';

try {
// import versions of experience
await this.importExperienceVersions(expRes, uid);
} catch (error) {
this.log(this.config, `Error while importing experience versions of ${expRes.uid}`, 'error');
this.log(this.config, error, 'error');
}
}
fsUtil.writeFile(this.experiencesUidMapperPath, this.experiencesUidMapper);
this.log(this.config, this.$t(this.messages.CREATE_SUCCESS, { module: 'Experiences' }), 'info');
Expand All @@ -129,6 +157,68 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
}
}

/**
* function import experience versions from a JSON file and creates them in the project.
*/
async importExperienceVersions(experience: ExperienceStruct, oldExperienceUid: string) {
const versionsPath = resolve(sanitizePath(this.experiencesDirPath), 'versions', `${oldExperienceUid}.json`);

if (!existsSync(versionsPath)) {
return;
}

const versions = fsUtil.readFile(versionsPath, true) as ExperienceStruct[];
const versionMap: Record<string, CreateExperienceVersionInput | undefined> = {
ACTIVE: undefined,
DRAFT: undefined,
PAUSE: undefined,
};

// Process each version and map them by status
versions.forEach((version) => {
let versionReqObj = lookUpAudiences(version, this.audiencesUid) as CreateExperienceVersionInput;
versionReqObj = lookUpEvents(version, this.eventsUid) as CreateExperienceVersionInput;

if (versionReqObj && versionReqObj.status) {
versionMap[versionReqObj.status] = versionReqObj;
}
});

// Prioritize updating or creating versions based on the order: ACTIVE -> DRAFT -> PAUSE
return await this.handleVersionUpdateOrCreate(experience, versionMap);
}

// Helper method to handle version update or creation logic
private async handleVersionUpdateOrCreate(
experience: ExperienceStruct,
versionMap: Record<string, CreateExperienceVersionInput | undefined>,
) {
const { ACTIVE, DRAFT, PAUSE } = versionMap;
let latestVersionUsed = false;

if (ACTIVE) {
await this.updateExperienceVersion(experience.uid, experience.latestVersion, ACTIVE);
latestVersionUsed = true;
}

if (DRAFT) {
if (latestVersionUsed) {
await this.createExperienceVersion(experience.uid, DRAFT);
} else {
await this.updateExperienceVersion(experience.uid, experience.latestVersion, DRAFT);
latestVersionUsed = true;
}
}

if (PAUSE) {
if (latestVersionUsed) {
await this.createExperienceVersion(experience.uid, PAUSE);
} else {
await this.updateExperienceVersion(experience.uid, experience.latestVersion, PAUSE);
}
}
}

/**
* function to validate if all variant groups and variants have been created using personalization background job
* store the variant groups data in mapper/personalization/experiences/cms-variant-groups.json and the variants data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ export type ExperienceStruct = {
content_types?: string[];
} & AnyProperty;

export interface CreateExperienceVersionInput {
name: string;
__type: string;
description: string;
targeting?: ExpTargeting;
variations: ExpVariations[];
variationSplit?: string;
metrics?: ExpMetric[];
status: string;
metadata?: object;
variants: Array<ExpVariations>;
}
export interface CreateExperienceInput {
name: string;
__type: string;
Expand All @@ -140,6 +152,7 @@ export interface CreateExperienceInput {
metrics?: ExpMetric[];
status: string;
metadata?: object;
variants?: Array<ExpVariations>;
}

export interface UpdateExperienceInput {
Expand Down
14 changes: 12 additions & 2 deletions packages/contentstack-variants/src/utils/audiences-helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CreateExperienceInput } from '../types';
import { CreateExperienceInput, CreateExperienceVersionInput } from '../types';

/**
* function for substituting an old audience UID with a new one or deleting the audience information if it does not exist
Expand Down Expand Up @@ -37,10 +37,20 @@ export const lookUpAudiences = (
}
}
}
} else if (experience.variants) {
for (let index = experience.variants.length - 1; index >= 0; index--) {
const expVariations = experience.variants[index];
if (expVariations['__type'] === 'AudienceBasedVariation' && expVariations?.audiences?.length) {
updateAudiences(expVariations.audiences, audiencesUid);
if (!expVariations.audiences.length) {
experience.variants.splice(index, 1);
}
}
}
}

// Update targeting audiences
if (experience?.targeting?.hasOwnProperty('audience') && experience?.targeting?.audience?.audiences?.length) {
// Update targeting audiences
updateAudiences(experience.targeting.audience.audiences, audiencesUid);
if (!experience.targeting.audience.audiences.length) {
experience.targeting = {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
APIResponse,
VariantGroupStruct,
VariantGroup,
CreateExperienceVersionInput,
} from '../types';
import { formatErrors } from './error-helper';

Expand Down Expand Up @@ -77,6 +78,35 @@ export class PersonalizationAdapter<T> extends AdapterHelper<T, HttpClient> impl
return this.handleVariantAPIRes(data) as ExperienceStruct;
}

async getExperienceVersions(experienceUid: string): Promise<ExperienceStruct | void> {
const getExperiencesVersionsEndPoint = `/experiences/${experienceUid}/versions`;
const data = await this.apiClient.get(getExperiencesVersionsEndPoint);
return this.handleVariantAPIRes(data) as ExperienceStruct;
}

async createExperienceVersion(
experienceUid: string,
input: CreateExperienceVersionInput,
): Promise<ExperienceStruct | void> {
const createExperiencesVersionsEndPoint = `/experiences/${experienceUid}/versions`;
const data = await this.apiClient.post(createExperiencesVersionsEndPoint, input);
return this.handleVariantAPIRes(data) as ExperienceStruct;
}

async updateExperienceVersion(
experienceUid: string,
versionId: string,
input: CreateExperienceVersionInput,
): Promise<ExperienceStruct | void> {
// loop through input and remove shortId from variant
if (input?.variants) {
input.variants = input.variants.map(({ shortUid, ...rest }) => rest);
}
const updateExperiencesVersionsEndPoint = `/experiences/${experienceUid}/versions/${versionId}`;
const data = await this.apiClient.put(updateExperiencesVersionsEndPoint, input);
return this.handleVariantAPIRes(data) as ExperienceStruct;
}

async getVariantGroup(input: GetVariantGroupInput): Promise<VariantGroupStruct | void> {
if (this.cmaAPIClient) {
const getVariantGroupEndPoint = `/variant_groups`;
Expand Down
2 changes: 1 addition & 1 deletion packages/contentstack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ $ npm install -g @contentstack/cli
$ csdx COMMAND
running command...
$ csdx (--version|-v)
@contentstack/cli/1.25.1 darwin-arm64 node-v22.8.0
@contentstack/cli/1.25.1 darwin-arm64 node-v22.2.0
$ csdx --help [COMMAND]
USAGE
$ csdx COMMAND
Expand Down