From fad8edb0678a1fbd53a1c59d34f745427cea8440 Mon Sep 17 00:00:00 2001 From: Jiri Lojda Date: Mon, 9 Sep 2024 11:58:00 +0200 Subject: [PATCH] Add spaces entity to sync-model --- src/modules/sync/constants/filename.ts | 1 + src/modules/sync/diff.ts | 7 +++ src/modules/sync/diff/combinators.ts | 15 +++++ src/modules/sync/diff/contentType.ts | 7 +-- src/modules/sync/diff/contentTypeSnippet.ts | 8 +-- src/modules/sync/diff/space.ts | 21 +++++++ src/modules/sync/diff/taxonomy.ts | 7 +-- src/modules/sync/generateSyncModel.ts | 20 +++++- .../modelTransfomers/spaceTransformers.ts | 24 ++++++++ src/modules/sync/printDiff.ts | 5 +- src/modules/sync/sync.ts | 3 + src/modules/sync/sync/spaces.ts | 57 +++++++++++++++++ src/modules/sync/types/diffModel.ts | 16 ++++- src/modules/sync/types/fileContentModel.ts | 2 + src/modules/sync/types/syncModel.ts | 9 +++ src/modules/sync/utils/diffTemplate.html | 2 +- src/modules/sync/utils/fetchers.ts | 6 ++ src/modules/sync/utils/getContentModel.ts | 34 ++++++---- src/modules/sync/utils/htmlRenderers.ts | 22 ++++--- .../diff/advancedDiff.test.ts.snap.html | 30 +++++++++ .../sync/data/sync_test_source.zip | Bin 51211 -> 51860 bytes .../sync/data/sync_test_target.zip | Bin 38900 -> 39526 bytes .../syncModel/contentTypeTransformer.test.ts | 1 + tests/unit/syncModel/diff/combinators.test.ts | 28 +++++++++ tests/unit/syncModel/diff/space.test.ts | 58 ++++++++++++++++++ .../unit/syncModel/snippetTransformer.test.ts | 1 + .../unit/syncModel/spacesTransformer.test.ts | 47 ++++++++++++++ .../syncModel/webSpotlightTransformer.test.ts | 1 + 28 files changed, 394 insertions(+), 38 deletions(-) create mode 100644 src/modules/sync/diff/space.ts create mode 100644 src/modules/sync/modelTransfomers/spaceTransformers.ts create mode 100644 src/modules/sync/sync/spaces.ts create mode 100644 tests/unit/syncModel/diff/space.test.ts create mode 100644 tests/unit/syncModel/spacesTransformer.test.ts diff --git a/src/modules/sync/constants/filename.ts b/src/modules/sync/constants/filename.ts index 7f2eb991..6365e09d 100644 --- a/src/modules/sync/constants/filename.ts +++ b/src/modules/sync/constants/filename.ts @@ -4,3 +4,4 @@ export const taxonomiesFileName = "taxonomyGroups.json"; export const collectionsFileName = "collections.json"; export const webSpotlightFileName = "webSpotlight.json"; export const assetFoldersFileName = "assetFolders.json"; +export const spacesFileName = "spaces.json"; diff --git a/src/modules/sync/diff.ts b/src/modules/sync/diff.ts index e87da085..e60b978a 100644 --- a/src/modules/sync/diff.ts +++ b/src/modules/sync/diff.ts @@ -3,6 +3,7 @@ import { collectionsHandler } from "./diff/collection.js"; import { Handler } from "./diff/combinators.js"; import { makeContentTypeHandler, wholeContentTypesHandler } from "./diff/contentType.js"; import { makeContentTypeSnippetHandler, wholeContentTypeSnippetsHandler } from "./diff/contentTypeSnippet.js"; +import { spaceHandler, wholeSpacesHandler } from "./diff/space.js"; import { taxonomyGroupHandler, wholeTaxonomyGroupsHandler } from "./diff/taxonomy.js"; import { transformSnippetToAddModel, @@ -70,6 +71,11 @@ export const diff = (params: DiffParams): DiffModel => { params.targetEnvModel.assetFolders, ); + const spacesDiffModel = createDiffModel( + wholeSpacesHandler(params.sourceEnvModel.spaces, params.targetEnvModel.spaces), + spaceHandler, + ); + 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), @@ -78,6 +84,7 @@ export const diff = (params: DiffParams): DiffModel => { collections: collectionDiffModel, webSpotlight: webSpotlightDiffModel, assetFolders: assetFoldersDiffModel, + spaces: spacesDiffModel, }; }; diff --git a/src/modules/sync/diff/combinators.ts b/src/modules/sync/diff/combinators.ts index 56bb82c3..b4a8a8a7 100644 --- a/src/modules/sync/diff/combinators.ts +++ b/src/modules/sync/diff/combinators.ts @@ -333,3 +333,18 @@ export const baseHandler: Handler = (sourceVal, targe * Never creates a patch operation. Use for values that can never change (e.g. property "type" in elements). */ export const constantHandler: Handler = () => []; + +/** + * Compares objects on the "codename" property and creates "addInto", "remove" and "replace" operations for the whole objects. + * You should then use a different handler for comparing the same objects (those in the "replace" operations). + * The main puprose of this is to determine what objects (e.g. types or spaces) need to be added, removed or replaced. + * + * @param propertyName - the name of any property it is used to mark any two objects with the same codename as different + */ +export const makeWholeObjectsHandler = < + Object extends { readonly codename: string }, +>(propertyName: Omit): Handler> => + makeArrayHandler( + el => el.codename, + makeLeafObjectHandler({ [propertyName as string]: () => false } as { [k in keyof Object]: () => false }), + ); diff --git a/src/modules/sync/diff/contentType.ts b/src/modules/sync/diff/contentType.ts index 603680ac..4a89d258 100644 --- a/src/modules/sync/diff/contentType.ts +++ b/src/modules/sync/diff/contentType.ts @@ -10,10 +10,10 @@ import { Handler, makeAdjustOperationHandler, makeArrayHandler, - makeLeafObjectHandler, makeObjectHandler, makeOrderingHandler, makeUnionHandler, + makeWholeObjectsHandler, optionalHandler, } from "./combinators.js"; import { @@ -137,7 +137,4 @@ const patchOpToOrdNumber = (op: PatchOperation) => { const contentTypeOperationsComparator = (el1: PatchOperation, el2: PatchOperation): number => patchOpToOrdNumber(el1) - patchOpToOrdNumber(el2); -export const wholeContentTypesHandler: Handler> = makeArrayHandler( - t => t.codename, - makeLeafObjectHandler({ name: () => false }), // always replace types with the same codename as this handler only handles whole types not their parts -); +export const wholeContentTypesHandler: Handler> = makeWholeObjectsHandler("name"); diff --git a/src/modules/sync/diff/contentTypeSnippet.ts b/src/modules/sync/diff/contentTypeSnippet.ts index 97683088..9ec051d0 100644 --- a/src/modules/sync/diff/contentTypeSnippet.ts +++ b/src/modules/sync/diff/contentTypeSnippet.ts @@ -5,10 +5,10 @@ import { Handler, makeAdjustOperationHandler, makeArrayHandler, - makeLeafObjectHandler, makeObjectHandler, makeOrderingHandler, makeUnionHandler, + makeWholeObjectsHandler, } from "./combinators.js"; import { makeAssetElementHandler, @@ -98,7 +98,5 @@ const patchOpToOrdNumber = (op: PatchOperation) => { const snippetOperationsComparator = (el1: PatchOperation, el2: PatchOperation): number => patchOpToOrdNumber(el1) - patchOpToOrdNumber(el2); -export const wholeContentTypeSnippetsHandler: Handler> = makeArrayHandler( - s => s.codename, - makeLeafObjectHandler({ name: () => false }), // always replace snippets with the same codename as this handler only handles whole snippets not their parts -); +export const wholeContentTypeSnippetsHandler: Handler> = + makeWholeObjectsHandler("name"); diff --git a/src/modules/sync/diff/space.ts b/src/modules/sync/diff/space.ts new file mode 100644 index 00000000..2f663d41 --- /dev/null +++ b/src/modules/sync/diff/space.ts @@ -0,0 +1,21 @@ +import { zip } from "../../../utils/array.js"; +import { SpaceSyncModel } from "../types/syncModel.js"; +import { + baseHandler, + Handler, + makeLeafObjectHandler, + makeObjectHandler, + makeWholeObjectsHandler, + optionalHandler, +} from "./combinators.js"; + +export const spaceHandler: Handler = makeObjectHandler({ + name: baseHandler, + web_spotlight_root_item: optionalHandler(makeLeafObjectHandler({})), + collections: (source, target) => + source.length !== target.length || zip(source, target).some(([s, t]) => s.codename !== t.codename) + ? [{ op: "replace", path: "", value: source, oldValue: target }] + : [], +}); + +export const wholeSpacesHandler: Handler> = makeWholeObjectsHandler("name"); diff --git a/src/modules/sync/diff/taxonomy.ts b/src/modules/sync/diff/taxonomy.ts index 26cf6662..76e02547 100644 --- a/src/modules/sync/diff/taxonomy.ts +++ b/src/modules/sync/diff/taxonomy.ts @@ -11,10 +11,10 @@ import { makeAdjustEntityHandler, makeAdjustOperationHandler, makeArrayHandler, - makeLeafObjectHandler, makeObjectHandler, makeOrderingHandler, makeProvideHandler, + makeWholeObjectsHandler, } from "./combinators.js"; export const taxonomyGroupHandler: Handler = makeObjectHandler({ @@ -33,10 +33,7 @@ export const taxonomyGroupHandler: Handler = makeObjectHandle }, }); -export const wholeTaxonomyGroupsHandler: Handler> = makeArrayHandler( - g => g.codename, - makeLeafObjectHandler({ name: () => false }), // always replace taxonomy groups with the same codename as this handler only handles whole taxonomy groups not their parts -); +export const wholeTaxonomyGroupsHandler: Handler> = makeWholeObjectsHandler("name"); type FlattenedSyncTaxonomy = Replace & Readonly<{ position: ReadonlyArray }>; diff --git a/src/modules/sync/generateSyncModel.ts b/src/modules/sync/generateSyncModel.ts index d4dd39c0..a2f40b1b 100644 --- a/src/modules/sync/generateSyncModel.ts +++ b/src/modules/sync/generateSyncModel.ts @@ -4,6 +4,7 @@ import { CollectionContracts, ContentItemContracts, ManagementClient, + SpaceContracts, TaxonomyContracts, WebSpotlightContracts, } from "@kontent-ai/management-sdk"; @@ -14,11 +15,13 @@ 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 { notNullOrUndefined } from "../../utils/typeguards.js"; import { assetFoldersFileName, collectionsFileName, contentTypesFileName, contentTypeSnippetsFileName, + spacesFileName, taxonomiesFileName, webSpotlightFileName, } from "./constants/filename.js"; @@ -26,6 +29,7 @@ import { transformAssetFolderModel } from "./modelTransfomers/assetFolder.js"; import { transformCollectionsModel } from "./modelTransfomers/collections.js"; import { transformContentTypeModel } from "./modelTransfomers/contentTypes.js"; import { transformContentTypeSnippetsModel } from "./modelTransfomers/contentTypeSnippets.js"; +import { transformSpacesModel } from "./modelTransfomers/spaceTransformers.js"; import { transformTaxonomyGroupsModel } from "./modelTransfomers/taxonomyGroups.js"; import { transformWebSpotlightModel } from "./modelTransfomers/webSpotlight.js"; import { ContentTypeSnippetsWithUnionElements, ContentTypeWithUnionElements } from "./types/contractModels.js"; @@ -38,6 +42,7 @@ import { fetchContentTypeSnippets, fetchRequiredAssets, fetchRequiredContentItems, + fetchSpaces, fetchTaxonomies, fetchWebSpotlight, } from "./utils/fetchers.js"; @@ -49,6 +54,7 @@ export type EnvironmentModel = { collections: ReadonlyArray; webSpotlight: WebSpotlightContracts.IWebSpotlightStatus; assetFolders: ReadonlyArray; + spaces: ReadonlyArray; assets: ReadonlyArray; items: ReadonlyArray; }; @@ -64,6 +70,8 @@ export const fetchModel = async (client: ManagementClient): Promise; itemIds: Set }>( @@ -75,7 +83,10 @@ export const fetchModel = async (client: ManagementClient): Promise s.web_spotlight_root_item?.id).filter(notNullOrUndefined)), + }, ); const assets = await fetchRequiredAssets(client, Array.from(allIds.assetIds)); @@ -87,6 +98,7 @@ export const fetchModel = async (client: ManagementClient): Promise { path.resolve(folderName, collectionsFileName), JSON.stringify(finalModel.collections, null, 2), ); + await fsPromises.writeFile( + path.resolve(folderName, spacesFileName), + JSON.stringify(finalModel.spaces, 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/spaceTransformers.ts b/src/modules/sync/modelTransfomers/spaceTransformers.ts new file mode 100644 index 00000000..5ce29905 --- /dev/null +++ b/src/modules/sync/modelTransfomers/spaceTransformers.ts @@ -0,0 +1,24 @@ +import { throwError } from "../../../utils/error.js"; +import { omit } from "../../../utils/object.js"; +import { EnvironmentModel } from "../generateSyncModel.js"; +import { SpaceSyncModel } from "../types/syncModel.js"; + +export const transformSpacesModel = ( + environmentModel: EnvironmentModel, +): ReadonlyArray => + environmentModel.spaces.map(space => ({ + ...omit(space, ["id"]), + web_spotlight_root_item: space.web_spotlight_root_item + ? { + codename: environmentModel.items.find(i => i.id === space.web_spotlight_root_item?.id)?.codename + ?? throwError( + `Cannot find web spotlight root item { id: ${space.web_spotlight_root_item.id} } for space { codename: ${space.codename}}.`, + ), + } + : undefined, + collections: space.collections + ?.map(collection => ({ + codename: environmentModel.collections.find(i => i.id === collection.id)?.codename + ?? throwError(`Cannot find collection { id: ${collection.id} } for space { codename: ${space.codename}}.`), + })) ?? [], + })); diff --git a/src/modules/sync/printDiff.ts b/src/modules/sync/printDiff.ts index b2031a48..a6228979 100644 --- a/src/modules/sync/printDiff.ts +++ b/src/modules/sync/printDiff.ts @@ -29,6 +29,9 @@ export const printDiff = (diffModel: DiffModel, logOptions: LogOptions) => { logInfo(logOptions, "standard", "No collections to update."); } + logInfo(logOptions, "standard", chalk.blue.bold("\nSPACES:")); + printDiffEntity(diffModel.spaces, "spaces", logOptions); + logInfo(logOptions, "standard", chalk.blue.bold("\nTAXONOMY GROUPS:")); printDiffEntity(diffModel.taxonomyGroups, "taxonomy groups", logOptions); @@ -59,7 +62,7 @@ export const printDiff = (diffModel: DiffModel, logOptions: LogOptions) => { const printDiffEntity = ( diffObject: DiffObject, - entityName: "content types" | "content type snippets" | "taxonomy groups", + entityName: "content types" | "content type snippets" | "taxonomy groups" | "spaces", logOptions: LogOptions, ) => { if (diffObject.added.length) { diff --git a/src/modules/sync/sync.ts b/src/modules/sync/sync.ts index a495ee85..056a9b3f 100644 --- a/src/modules/sync/sync.ts +++ b/src/modules/sync/sync.ts @@ -11,6 +11,7 @@ import { deleteSnippet, updateSnippets, } from "./sync/snippets.js"; +import { syncSpaces } from "./sync/spaces.js"; import { syncTaxonomies } from "./sync/taxonomy.js"; import { addTypesWithoutReferences, deleteContentType, updateContentTypesAndAddReferences } from "./sync/types.js"; import { isOp } from "./sync/utils.js"; @@ -24,6 +25,8 @@ export const sync = async (client: ManagementClient, diff: DiffModel, logOptions logInfo(logOptions, "standard", "Syncing Collections"); await syncAddAndReplaceCollections(client, diff.collections); + await syncSpaces(client, diff.spaces, logOptions); + await syncRemoveCollections(client, diff.collections); await syncTaxonomies(client, diff.taxonomyGroups, logOptions); diff --git a/src/modules/sync/sync/spaces.ts b/src/modules/sync/sync/spaces.ts new file mode 100644 index 00000000..4ee30e02 --- /dev/null +++ b/src/modules/sync/sync/spaces.ts @@ -0,0 +1,57 @@ +import { ManagementClient, SpaceModels } from "@kontent-ai/management-sdk"; +import { match, P } from "ts-pattern"; + +import { logInfo, LogOptions } from "../../../log.js"; +import { throwError } from "../../../utils/error.js"; +import { serially } from "../../../utils/requests.js"; +import { DiffModel } from "../types/diffModel.js"; +import { PatchOperation } from "../types/patchOperation.js"; + +export const syncSpaces = async ( + client: ManagementClient, + model: DiffModel["spaces"], + logOptions: LogOptions, +) => { + logInfo(logOptions, "standard", "Adding new spaces"); + + await serially(model.added.map(space => () => + client + .addSpace() + .withData(space as SpaceModels.IAddSpaceData) + .toPromise() + )); + + logInfo(logOptions, "standard", "Updating spaces"); + + await serially([...model.updated].map(([spaceCodename, operations]) => () => + client + .modifySpace() + .bySpaceCodename(spaceCodename) + .withData(operations.map(convertOperation)) + .toPromise() + )); + + logInfo(logOptions, "standard", "Removing spaces"); + + await serially([...model.deleted].map(spaceCodename => () => + client + .deleteSpace() + .bySpaceCodename(spaceCodename) + .toPromise() + )); +}; + +const convertOperation = (operation: PatchOperation): SpaceModels.IModifySpaceData => + match(operation) + .with({ op: "replace" }, op => + match(op.path.split("/").pop()) + .with( + P.union("name", "web_spotlight_root_item", "collections"), + pName => ({ op: "replace", property_name: pName, value: op.value as any }) as const, + ) + .otherwise(() => + throwError(`Patch operation "${JSON.stringify(op)}" has missing or invalid property name in path.`) + )) + .with({ op: P.union("addInto", "remove", "move") }, () => + throwError(`Patch operation "${operation.op}" is not supported for spaces.`)) + .exhaustive(); diff --git a/src/modules/sync/types/diffModel.ts b/src/modules/sync/types/diffModel.ts index 472bbf92..9a3f7543 100644 --- a/src/modules/sync/types/diffModel.ts +++ b/src/modules/sync/types/diffModel.ts @@ -1,6 +1,12 @@ -import { ContentTypeModels, ContentTypeSnippetModels, TaxonomyModels } from "@kontent-ai/management-sdk"; +import { + ContentTypeModels, + ContentTypeSnippetModels, + SharedContracts, + SpaceModels, + TaxonomyModels, +} from "@kontent-ai/management-sdk"; -import { RequiredCodename } from "../../../utils/types.js"; +import { Replace, RequiredCodename } from "../../../utils/types.js"; import { PatchOperation } from "./patchOperation.js"; type Codename = string; @@ -25,4 +31,10 @@ export type DiffModel = Readonly<{ collections: ReadonlyArray; webSpotlight: WebSpotlightDiffModel; assetFolders: ReadonlyArray; + spaces: DiffObject< + Replace< + RequiredCodename, + { collections: ReadonlyArray } + > + >; }>; diff --git a/src/modules/sync/types/fileContentModel.ts b/src/modules/sync/types/fileContentModel.ts index f89dd46d..8dee9941 100644 --- a/src/modules/sync/types/fileContentModel.ts +++ b/src/modules/sync/types/fileContentModel.ts @@ -3,6 +3,7 @@ import { CollectionSyncModel, ContentTypeSnippetsSyncModel, ContentTypeSyncModel, + SpaceSyncModel, TaxonomySyncModel, WebSpotlightSyncModel, } from "./syncModel.js"; @@ -14,4 +15,5 @@ export type FileContentModel = Readonly<{ webSpotlight: WebSpotlightSyncModel; assetFolders: ReadonlyArray; collections: ReadonlyArray; + spaces: ReadonlyArray; }>; diff --git a/src/modules/sync/types/syncModel.ts b/src/modules/sync/types/syncModel.ts index d950d82d..3699bd59 100644 --- a/src/modules/sync/types/syncModel.ts +++ b/src/modules/sync/types/syncModel.ts @@ -4,6 +4,7 @@ import { ContentTypeContracts, ContentTypeElements, ContentTypeSnippetContracts, + SpaceContracts, TaxonomyContracts, WebSpotlightContracts, } from "@kontent-ai/management-sdk"; @@ -132,6 +133,14 @@ export type AssetFolderSyncModel = Replace< Readonly<{ folders: ReadonlyArray }> >; +export type SpaceSyncModel = Replace< + Omit, + Readonly<{ + web_spotlight_root_item?: Readonly<{ codename: string }>; + collections: ReadonlyArray>; + }> +>; + 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 fcc3047f..c34b31af 100644 --- a/src/modules/sync/utils/diffTemplate.html +++ b/src/modules/sync/utils/diffTemplate.html @@ -345,7 +345,7 @@

Modified entities

state from {{datetime_generated}}
{{types_section}} {{snippets_section}} {{taxonomies_section}} {{asset_folders_section}} {{collections_section}} - {{web_spotlight_section}} + {{web_spotlight_section}} {{spaces_section}}