Skip to content

Commit

Permalink
Merge pull request #1579 from contentstack/feat/CS-1402,CS-1401
Browse files Browse the repository at this point in the history
Added support for experience versioning
  • Loading branch information
aman19K authored Sep 23, 2024
2 parents 085c92d + f06afc1 commit 5c48678
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 12 deletions.
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

0 comments on commit 5c48678

Please sign in to comment.