From 69cf9d7373df33bb78a418cd22fba56d103435d0 Mon Sep 17 00:00:00 2001 From: Jiri Lojda Date: Thu, 8 Aug 2024 12:44:47 +0200 Subject: [PATCH] Add webSpotlight to syncModel including advanced diff --- package-lock.json | 6 + package.json | 1 + src/modules/sync/constants/filename.ts | 1 + src/modules/sync/diff.ts | 7 + src/modules/sync/diff/webSpotlight.ts | 21 +++ src/modules/sync/generateSyncModel.ts | 27 +++- .../sync/modelTransfomers/webSpotlight.ts | 13 ++ src/modules/sync/printDiff.ts | 29 +++- src/modules/sync/sync.ts | 24 +++- src/modules/sync/types/diffModel.ts | 8 ++ src/modules/sync/types/fileContentModel.ts | 8 +- src/modules/sync/types/syncModel.ts | 6 + src/modules/sync/utils/diffTemplate.html | 1 + src/modules/sync/utils/fetchers.ts | 6 + src/modules/sync/utils/getContentModel.ts | 27 ++-- src/modules/sync/utils/htmlRenderers.ts | 129 +++++++++--------- .../syncModel/contentTypeTransformer.test.ts | 4 + .../unit/syncModel/diff/webSpotlight.test.ts | 96 +++++++++++++ .../unit/syncModel/snippetTransformer.test.ts | 4 + .../syncModel/webSpotlightTransformer.test.ts | 55 ++++++++ tests/unit/utils/requests.test.ts | 2 +- 21 files changed, 389 insertions(+), 86 deletions(-) create mode 100644 src/modules/sync/diff/webSpotlight.ts create mode 100644 src/modules/sync/modelTransfomers/webSpotlight.ts create mode 100644 tests/unit/syncModel/diff/webSpotlight.test.ts create mode 100644 tests/unit/syncModel/webSpotlightTransformer.test.ts diff --git a/package-lock.json b/package-lock.json index 3f5e3d52..74580676 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "chalk": "^5.3.0", "node-stream-zip": "^1.15.0", "open": "^10.1.0", + "ts-pattern": "^5.2.0", "yargs": "^17.7.2", "zod": "^3.23.8" }, @@ -8822,6 +8823,11 @@ } } }, + "node_modules/ts-pattern": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.2.0.tgz", + "integrity": "sha512-aGaSpOlDcns7ZoeG/OMftWyQG1KqPVhgplhJxNCvyIXqWrumM5uIoOSarw/hmmi/T1PnuQ/uD8NaFHvLpHicDg==" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", diff --git a/package.json b/package.json index 2ed96427..e51b3ae4 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "chalk": "^5.3.0", "node-stream-zip": "^1.15.0", "open": "^10.1.0", + "ts-pattern": "^5.2.0", "yargs": "^17.7.2", "zod": "^3.23.8" }, diff --git a/src/modules/sync/constants/filename.ts b/src/modules/sync/constants/filename.ts index 55452818..eeba8a03 100644 --- a/src/modules/sync/constants/filename.ts +++ b/src/modules/sync/constants/filename.ts @@ -1,3 +1,4 @@ export const contentTypesFileName = "contentTypes.json"; export const contentTypeSnippetsFileName = "contentTypeSnippets.json"; export const taxonomiesFileName = "taxonomyGroups.json"; +export const webSpotlightFileName = "webSpotlight.json"; diff --git a/src/modules/sync/diff.ts b/src/modules/sync/diff.ts index 6dc04d0a..a6a362d2 100644 --- a/src/modules/sync/diff.ts +++ b/src/modules/sync/diff.ts @@ -7,6 +7,7 @@ import { transformTaxonomyToAddModel, transformTypeToAddModel, } from "./diff/transformToAddModel.js"; +import { webSpotlightHandler } from "./diff/webSpotlight.js"; import { DiffModel } from "./types/diffModel.js"; import { FileContentModel } from "./types/fileContentModel.js"; import { PatchOperation } from "./types/patchOperation.js"; @@ -52,11 +53,17 @@ export const diff = (params: DiffParams): DiffModel => { taxonomyGroupHandler, ); + const webSpotlightDiffModel = webSpotlightHandler( + params.sourceEnvModel.webSpotlight, + params.targetEnvModel.webSpotlight, + ); + return { // All the arrays are mutable in the SDK (even though they shouldn't) and readonly in our models. Unfortunately, TS doesn't allow casting it without casting to unknown first. taxonomyGroups: mapAdded(taxonomyDiffModel, transformTaxonomyToAddModel), contentTypeSnippets: mapAdded(snippetsDiffModel, transformSnippetToAddModel(params)), contentTypes: mapAdded(typesDiffModel, transformTypeToAddModel(params)), + webSpotlight: webSpotlightDiffModel, }; }; diff --git a/src/modules/sync/diff/webSpotlight.ts b/src/modules/sync/diff/webSpotlight.ts new file mode 100644 index 00000000..5656230e --- /dev/null +++ b/src/modules/sync/diff/webSpotlight.ts @@ -0,0 +1,21 @@ +import { match } from "ts-pattern"; + +import { WebSpotlightDiffModel } from "../types/diffModel.js"; +import { WebSpotlightSyncModel } from "../types/syncModel.js"; + +export const webSpotlightHandler = ( + source: WebSpotlightSyncModel, + target: WebSpotlightSyncModel, +): WebSpotlightDiffModel => + match({ source: source.enabled, target: target.enabled }) + .with({ source: true, target: true }, () => + source.root_type?.codename === target.root_type?.codename + ? { change: "none" } as const + : { change: "changeRootType", rootTypeCodename: source.root_type?.codename ?? "" } as const) + .with({ source: false, target: false }, () => ({ change: "none" } as const)) + .with({ source: false, target: true }, () => ({ change: "deactivate" } as const)) + .with( + { source: true, target: false }, + () => ({ change: "activate", rootTypeCodename: source.root_type?.codename ?? "" } as const), + ) + .exhaustive(); diff --git a/src/modules/sync/generateSyncModel.ts b/src/modules/sync/generateSyncModel.ts index 1a0d0b3d..d108f84f 100644 --- a/src/modules/sync/generateSyncModel.ts +++ b/src/modules/sync/generateSyncModel.ts @@ -1,4 +1,10 @@ -import { AssetContracts, ContentItemContracts, ManagementClient, TaxonomyContracts } from "@kontent-ai/management-sdk"; +import { + AssetContracts, + ContentItemContracts, + ManagementClient, + TaxonomyContracts, + WebSpotlightContracts, +} from "@kontent-ai/management-sdk"; import chalk from "chalk"; import * as fsPromises from "fs/promises"; import * as path from "path"; @@ -6,10 +12,16 @@ import * as path from "path"; import packageJson from "../../../package.json" with { type: "json" }; import { logInfo, LogOptions } from "../../log.js"; import { serializeDateForFileName } from "../../utils/files.js"; -import { contentTypesFileName, contentTypeSnippetsFileName, taxonomiesFileName } from "./constants/filename.js"; +import { + contentTypesFileName, + contentTypeSnippetsFileName, + taxonomiesFileName, + webSpotlightFileName, +} from "./constants/filename.js"; import { transformContentTypeModel } from "./modelTransfomers/contentTypes.js"; import { transformContentTypeSnippetsModel } from "./modelTransfomers/contentTypeSnippets.js"; import { transformTaxonomyGroupsModel } from "./modelTransfomers/taxonomyGroups.js"; +import { transformWebSpotlightModel } from "./modelTransfomers/webSpotlight.js"; import { ContentTypeSnippetsWithUnionElements, ContentTypeWithUnionElements } from "./types/contractModels.js"; import { FileContentModel } from "./types/fileContentModel.js"; import { getRequiredIds } from "./utils/contentTypeHelpers.js"; @@ -19,12 +31,14 @@ import { fetchRequiredAssets, fetchRequiredContentItems, fetchTaxonomies, + fetchWebSpotlight, } from "./utils/fetchers.js"; export type EnvironmentModel = { taxonomyGroups: ReadonlyArray; contentTypeSnippets: ReadonlyArray; contentTypes: ReadonlyArray; + webSpotlight: WebSpotlightContracts.IWebSpotlightStatus; assets: ReadonlyArray; items: ReadonlyArray; }; @@ -36,6 +50,8 @@ export const fetchModel = async (client: ManagementClient): Promise; itemIds: Set }>( (previous, type) => { const ids = getRequiredIds(type.elements); @@ -55,6 +71,7 @@ export const fetchModel = async (client: ManagementClient): Promise { path.resolve(folderName, taxonomiesFileName), JSON.stringify(finalModel.taxonomyGroups, null, 2), ); + await fsPromises.writeFile( + path.resolve(folderName, webSpotlightFileName), + JSON.stringify(finalModel.webSpotlight, null, 2), + ); await fsPromises.writeFile(path.resolve(folderName, "metadata.json"), JSON.stringify(finalModel.metadata, null, 2)); return folderName; diff --git a/src/modules/sync/modelTransfomers/webSpotlight.ts b/src/modules/sync/modelTransfomers/webSpotlight.ts new file mode 100644 index 00000000..dc1b13c2 --- /dev/null +++ b/src/modules/sync/modelTransfomers/webSpotlight.ts @@ -0,0 +1,13 @@ +import { EnvironmentModel } from "../generateSyncModel.js"; +import { WebSpotlightSyncModel } from "../types/syncModel.js"; + +export const transformWebSpotlightModel = (environmentModel: EnvironmentModel): WebSpotlightSyncModel => { + const rootTypeCodename = environmentModel.contentTypes + .find(type => type.id === environmentModel.webSpotlight.root_type?.id) + ?.codename; + + return ({ + enabled: environmentModel.webSpotlight.enabled, + root_type: rootTypeCodename ? { codename: rootTypeCodename } : null, + }); +}; diff --git a/src/modules/sync/printDiff.ts b/src/modules/sync/printDiff.ts index 7c1b1d2f..413d2185 100644 --- a/src/modules/sync/printDiff.ts +++ b/src/modules/sync/printDiff.ts @@ -12,6 +12,29 @@ export const printDiff = (diffModel: DiffModel, logOptions: LogOptions) => { logInfo(logOptions, "standard", chalk.blue.bold("\nCONTENT TYPES:")); printDiffEntity(diffModel.contentTypes, "content types", logOptions); + + if (diffModel.webSpotlight.change !== "none") { + logInfo(logOptions, "standard", chalk.blue.bold("\nWEB SPOTLIGHT:")); + switch (diffModel.webSpotlight.change) { + case "activate": + logInfo( + logOptions, + "standard", + `Web Spotlight is to be activated with root type: ${chalk.green(diffModel.webSpotlight.rootTypeCodename)}`, + ); + break; + case "changeRootType": + logInfo( + logOptions, + "standard", + `Web Spotlight root type is changed to: ${chalk.green(diffModel.webSpotlight.rootTypeCodename)}`, + ); + break; + case "deactivate": + logInfo(logOptions, "standard", "Web Spotlight is to be deactivated"); + break; + } + } }; const printDiffEntity = ( @@ -20,7 +43,7 @@ const printDiffEntity = ( logOptions: LogOptions, ) => { if (diffObject.added.length) { - logInfo(logOptions, "standard", `Added ${chalk.blue(entityName)}:`); + logInfo(logOptions, "standard", `${chalk.blue(entityName)} to Add:`); diffObject.added.forEach(a => { logInfo(logOptions, "standard", `${chalk.green(JSON.stringify(a, null, 2))}\n`); }); @@ -29,7 +52,7 @@ const printDiffEntity = ( } if (Array.from(diffObject.updated.values()).some(o => o.length)) { - logInfo(logOptions, "standard", `Updated ${chalk.blue(entityName)}:`); + logInfo(logOptions, "standard", `${chalk.blue(entityName)} to update:`); Array.from(diffObject.updated.entries()).sort().forEach(([codename, value]) => { if (value.length) { logInfo(logOptions, "standard", `Entity codename: ${chalk.blue(codename)}`); @@ -44,7 +67,7 @@ const printDiffEntity = ( logInfo( logOptions, "standard", - `Deleted ${chalk.blue(entityName)} with codenames: ${chalk.red(Array.from(diffObject.deleted).join(","))}\n`, + `${chalk.blue(entityName)} to delete with codenames: ${chalk.red(Array.from(diffObject.deleted).join(","))}\n`, ); } else { logInfo(logOptions, "standard", `No ${chalk.blue(entityName)} to delete.`); diff --git a/src/modules/sync/sync.ts b/src/modules/sync/sync.ts index 897dd439..e4be4b64 100644 --- a/src/modules/sync/sync.ts +++ b/src/modules/sync/sync.ts @@ -14,7 +14,7 @@ import { notNullOrUndefined } from "../../utils/typeguards.js"; import { RequiredCodename } from "../../utils/types.js"; import { elementTypes } from "./constants/elements.js"; import { ElementsTypes } from "./types/contractModels.js"; -import { DiffModel } from "./types/diffModel.js"; +import { DiffModel, WebSpotlightDiffModel } from "./types/diffModel.js"; import { getTargetCodename, PatchOperation } from "./types/patchOperation.js"; const referencingElements: ReadonlyArray = ["rich_text", "modular_content", "subpages"]; @@ -43,14 +43,17 @@ export const sync = async (client: ManagementClient, diff: DiffModel, logOptions logInfo(logOptions, "standard", "Updating content types and adding their references"); await updateContentTypesAndAddReferences(client, diff.contentTypes); + logInfo(logOptions, "standard", "Updating web spotlight"); + await updateWebSpotlight(client, diff.webSpotlight); + logInfo(logOptions, "standard", "Removing content types"); await serially(Array.from(diff.contentTypes.deleted).map(c => () => deleteContentType(client, c))); - logInfo(logOptions, "standard", "Deleting content type snippets"); + logInfo(logOptions, "standard", "Removing content type snippets"); await serially(Array.from(diff.contentTypeSnippets.deleted).map(c => () => deleteSnippet(client, c))); // replace, remove, move operations - logInfo(logOptions, "standard", "Deleting content type snippets"); + logInfo(logOptions, "standard", "Updating content type snippets"); await updateSnippets(client, diff.contentTypeSnippets.updated); }; @@ -300,6 +303,21 @@ const transformTaxonomyOperations = ( } as unknown as TaxonomyModels.IModifyTaxonomyData; }; +const updateWebSpotlight = (client: ManagementClient, diffModel: WebSpotlightDiffModel): Promise => { + switch (diffModel.change) { + case "none": + return Promise.resolve(); + case "activate": + case "changeRootType": + return client + .activateWebSpotlight() + .withData({ root_type: { codename: diffModel.rootTypeCodename } }) + .toPromise(); + case "deactivate": + return client.deactivateWebSpotlight().toPromise(); + } +}; + const createUpdateReferenceOps = ( element: ReferencingElement, ) => diff --git a/src/modules/sync/types/diffModel.ts b/src/modules/sync/types/diffModel.ts index b2aa853f..02ce3bc9 100644 --- a/src/modules/sync/types/diffModel.ts +++ b/src/modules/sync/types/diffModel.ts @@ -11,8 +11,16 @@ export type DiffObject = Readonly<{ deleted: ReadonlySet; }>; +export type WebSpotlightDiffModel = Readonly< + | { change: "none" } + | { change: "activate"; rootTypeCodename: Codename } + | { change: "changeRootType"; rootTypeCodename: Codename } + | { change: "deactivate" } +>; + export type DiffModel = Readonly<{ taxonomyGroups: DiffObject>; contentTypeSnippets: DiffObject>; contentTypes: DiffObject>; + webSpotlight: WebSpotlightDiffModel; }>; diff --git a/src/modules/sync/types/fileContentModel.ts b/src/modules/sync/types/fileContentModel.ts index 9f59faf0..f1a03fd4 100644 --- a/src/modules/sync/types/fileContentModel.ts +++ b/src/modules/sync/types/fileContentModel.ts @@ -1,7 +1,13 @@ -import { ContentTypeSnippetsSyncModel, ContentTypeSyncModel, TaxonomySyncModel } from "./syncModel.js"; +import { + ContentTypeSnippetsSyncModel, + ContentTypeSyncModel, + TaxonomySyncModel, + WebSpotlightSyncModel, +} from "./syncModel.js"; export type FileContentModel = Readonly<{ taxonomyGroups: ReadonlyArray; contentTypeSnippets: ReadonlyArray; contentTypes: ReadonlyArray; + webSpotlight: WebSpotlightSyncModel; }>; diff --git a/src/modules/sync/types/syncModel.ts b/src/modules/sync/types/syncModel.ts index 2b8ba96d..e94b2808 100644 --- a/src/modules/sync/types/syncModel.ts +++ b/src/modules/sync/types/syncModel.ts @@ -3,6 +3,7 @@ import { ContentTypeElements, ContentTypeSnippetContracts, TaxonomyContracts, + WebSpotlightContracts, } from "@kontent-ai/management-sdk"; import { CodenameReference, Replace } from "../../../utils/types.js"; @@ -117,6 +118,11 @@ export type ContentTypeSyncModel = Replace< }> >; +export type WebSpotlightSyncModel = Replace< + WebSpotlightContracts.IWebSpotlightStatus, + { root_type: Readonly<{ codename: string }> | null } +>; + export const isSyncCustomElement = (entity: unknown): entity is SyncCustomElement => typeof entity === "object" && entity !== null && "type" in entity && entity.type === "custom"; diff --git a/src/modules/sync/utils/diffTemplate.html b/src/modules/sync/utils/diffTemplate.html index 9b8ca051..1d25bb29 100644 --- a/src/modules/sync/utils/diffTemplate.html +++ b/src/modules/sync/utils/diffTemplate.html @@ -334,6 +334,7 @@

Modified entities

{{types_section}} {{snippets_section}} {{taxonomies_section}} + {{web_spotlight_section}}