diff --git a/packages/contentstack-variants/src/export/experiences.ts b/packages/contentstack-variants/src/export/experiences.ts index 0b65ef3076..fee1d5ef3c 100644 --- a/packages/contentstack-variants/src/export/experiences.ts +++ b/packages/contentstack-variants/src/export/experiences.ts @@ -34,6 +34,7 @@ export default class ExportExperiences extends PersonalizationAdapter = (await this.getExperiences()) || []; if (!experiences || experiences?.length < 1) { log(this.exportConfig, 'No Experiences found with the give project', 'info'); @@ -51,6 +52,21 @@ export default class ExportExperiences extends PersonalizationAdapter 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] = [] } = diff --git a/packages/contentstack-variants/src/import/experiences.ts b/packages/contentstack-variants/src/import/experiences.ts index ea4a1c27e6..d30c8911a9 100644 --- a/packages/contentstack-variants/src/import/experiences.ts +++ b/packages/contentstack-variants/src/import/experiences.ts @@ -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 { private createdCTs: string[]; private mapperDirPath: string; @@ -28,6 +35,8 @@ export default class Experiences extends PersonalizationAdapter { private cmsVariantGroups: Record; private experiencesUidMapper: Record; private pendingVariantAndVariantGrpForExperience: string[]; + private audiencesUid: Record; + private eventsUid: Record; private personalizationConfig: ImportConfig['modules']['personalization']; private audienceConfig: ImportConfig['modules']['personalization']['audiences']; private experienceConfig: ImportConfig['modules']['personalization']['experiences']; @@ -49,15 +58,26 @@ export default class Experiences extends PersonalizationAdapter { 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'); @@ -78,6 +98,8 @@ export default class Experiences extends PersonalizationAdapter { 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) || {}; + this.eventsUid = (fsUtil.readFile(this.eventsMapperPath, true) as Record) || {}; } /** @@ -91,19 +113,25 @@ export default class Experiences extends PersonalizationAdapter { if (existsSync(this.experiencesPath)) { try { const experiences = fsUtil.readFile(this.experiencesPath, true) as ExperienceStruct[]; - const audiencesUid = (fsUtil.readFile(this.audiencesMapperPath, true) as Record) || {}; - const eventsUid = (fsUtil.readFile(this.eventsMapperPath, true) as Record) || {}; 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'); @@ -129,6 +157,68 @@ export default class Experiences extends PersonalizationAdapter { } } + /** + * 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 = { + 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, + ) { + 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 diff --git a/packages/contentstack-variants/src/types/personalization-api-adapter.ts b/packages/contentstack-variants/src/types/personalization-api-adapter.ts index 2036511706..baec467586 100644 --- a/packages/contentstack-variants/src/types/personalization-api-adapter.ts +++ b/packages/contentstack-variants/src/types/personalization-api-adapter.ts @@ -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; +} export interface CreateExperienceInput { name: string; __type: string; @@ -140,6 +152,7 @@ export interface CreateExperienceInput { metrics?: ExpMetric[]; status: string; metadata?: object; + variants?: Array; } export interface UpdateExperienceInput { diff --git a/packages/contentstack-variants/src/utils/audiences-helper.ts b/packages/contentstack-variants/src/utils/audiences-helper.ts index 285025413e..2b6a7d4881 100644 --- a/packages/contentstack-variants/src/utils/audiences-helper.ts +++ b/packages/contentstack-variants/src/utils/audiences-helper.ts @@ -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 @@ -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 = {}; diff --git a/packages/contentstack-variants/src/utils/personalization-api-adapter.ts b/packages/contentstack-variants/src/utils/personalization-api-adapter.ts index 531b22d698..40a24ac73f 100644 --- a/packages/contentstack-variants/src/utils/personalization-api-adapter.ts +++ b/packages/contentstack-variants/src/utils/personalization-api-adapter.ts @@ -22,6 +22,7 @@ import { APIResponse, VariantGroupStruct, VariantGroup, + CreateExperienceVersionInput, } from '../types'; import { formatErrors } from './error-helper'; @@ -77,6 +78,35 @@ export class PersonalizationAdapter extends AdapterHelper impl return this.handleVariantAPIRes(data) as ExperienceStruct; } + async getExperienceVersions(experienceUid: string): Promise { + 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 { + 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 { + // 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 { if (this.cmaAPIClient) { const getVariantGroupEndPoint = `/variant_groups`; diff --git a/packages/contentstack/README.md b/packages/contentstack/README.md index 18ec0a5b68..ed98442025 100644 --- a/packages/contentstack/README.md +++ b/packages/contentstack/README.md @@ -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