Skip to content

Commit

Permalink
Add spaces entity to sync-model
Browse files Browse the repository at this point in the history
  • Loading branch information
JiriLojda committed Sep 9, 2024
1 parent 6120d04 commit fad8edb
Show file tree
Hide file tree
Showing 28 changed files with 394 additions and 38 deletions.
1 change: 1 addition & 0 deletions src/modules/sync/constants/filename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
7 changes: 7 additions & 0 deletions src/modules/sync/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand All @@ -78,6 +84,7 @@ export const diff = (params: DiffParams): DiffModel => {
collections: collectionDiffModel,
webSpotlight: webSpotlightDiffModel,
assetFolders: assetFoldersDiffModel,
spaces: spacesDiffModel,
};
};

Expand Down
15 changes: 15 additions & 0 deletions src/modules/sync/diff/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,18 @@ export const baseHandler: Handler<string | boolean | number> = (sourceVal, targe
* Never creates a patch operation. Use for values that can never change (e.g. property "type" in elements).
*/
export const constantHandler: Handler<unknown> = () => [];

/**
* 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<keyof Object, "codename">): Handler<ReadonlyArray<Object>> =>
makeArrayHandler(
el => el.codename,
makeLeafObjectHandler({ [propertyName as string]: () => false } as { [k in keyof Object]: () => false }),
);
7 changes: 2 additions & 5 deletions src/modules/sync/diff/contentType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
Handler,
makeAdjustOperationHandler,
makeArrayHandler,
makeLeafObjectHandler,
makeObjectHandler,
makeOrderingHandler,
makeUnionHandler,
makeWholeObjectsHandler,
optionalHandler,
} from "./combinators.js";
import {
Expand Down Expand Up @@ -137,7 +137,4 @@ const patchOpToOrdNumber = (op: PatchOperation) => {
const contentTypeOperationsComparator = (el1: PatchOperation, el2: PatchOperation): number =>
patchOpToOrdNumber(el1) - patchOpToOrdNumber(el2);

export const wholeContentTypesHandler: Handler<ReadonlyArray<ContentTypeSyncModel>> = 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<ReadonlyArray<ContentTypeSyncModel>> = makeWholeObjectsHandler("name");
8 changes: 3 additions & 5 deletions src/modules/sync/diff/contentTypeSnippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
Handler,
makeAdjustOperationHandler,
makeArrayHandler,
makeLeafObjectHandler,
makeObjectHandler,
makeOrderingHandler,
makeUnionHandler,
makeWholeObjectsHandler,
} from "./combinators.js";
import {
makeAssetElementHandler,
Expand Down Expand Up @@ -98,7 +98,5 @@ const patchOpToOrdNumber = (op: PatchOperation) => {
const snippetOperationsComparator = (el1: PatchOperation, el2: PatchOperation): number =>
patchOpToOrdNumber(el1) - patchOpToOrdNumber(el2);

export const wholeContentTypeSnippetsHandler: Handler<ReadonlyArray<ContentTypeSnippetsSyncModel>> = 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<ReadonlyArray<ContentTypeSnippetsSyncModel>> =
makeWholeObjectsHandler("name");
21 changes: 21 additions & 0 deletions src/modules/sync/diff/space.ts
Original file line number Diff line number Diff line change
@@ -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<SpaceSyncModel> = 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<ReadonlyArray<SpaceSyncModel>> = makeWholeObjectsHandler("name");
7 changes: 2 additions & 5 deletions src/modules/sync/diff/taxonomy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import {
makeAdjustEntityHandler,
makeAdjustOperationHandler,
makeArrayHandler,
makeLeafObjectHandler,
makeObjectHandler,
makeOrderingHandler,
makeProvideHandler,
makeWholeObjectsHandler,
} from "./combinators.js";

export const taxonomyGroupHandler: Handler<TaxonomySyncModel> = makeObjectHandler({
Expand All @@ -33,10 +33,7 @@ export const taxonomyGroupHandler: Handler<TaxonomySyncModel> = makeObjectHandle
},
});

export const wholeTaxonomyGroupsHandler: Handler<ReadonlyArray<TaxonomySyncModel>> = 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<ReadonlyArray<TaxonomySyncModel>> = makeWholeObjectsHandler("name");

type FlattenedSyncTaxonomy = Replace<TaxonomySyncModel, { terms: [] }> & Readonly<{ position: ReadonlyArray<string> }>;

Expand Down
20 changes: 19 additions & 1 deletion src/modules/sync/generateSyncModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CollectionContracts,
ContentItemContracts,
ManagementClient,
SpaceContracts,
TaxonomyContracts,
WebSpotlightContracts,
} from "@kontent-ai/management-sdk";
Expand All @@ -14,18 +15,21 @@ 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";
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";
Expand All @@ -38,6 +42,7 @@ import {
fetchContentTypeSnippets,
fetchRequiredAssets,
fetchRequiredContentItems,
fetchSpaces,
fetchTaxonomies,
fetchWebSpotlight,
} from "./utils/fetchers.js";
Expand All @@ -49,6 +54,7 @@ export type EnvironmentModel = {
collections: ReadonlyArray<CollectionContracts.ICollectionContract>;
webSpotlight: WebSpotlightContracts.IWebSpotlightStatus;
assetFolders: ReadonlyArray<AssetFolderContracts.IAssetFolderContract>;
spaces: ReadonlyArray<SpaceContracts.ISpaceContract>;
assets: ReadonlyArray<AssetContracts.IAssetModelContract>;
items: ReadonlyArray<ContentItemContracts.IContentItemModelContract>;
};
Expand All @@ -64,6 +70,8 @@ export const fetchModel = async (client: ManagementClient): Promise<EnvironmentM

const assetFolders = await fetchAssetFolders(client);

const spaces = await fetchSpaces(client);

const collections = await fetchCollections(client);

const allIds = [...contentTypes, ...contentTypeSnippets].reduce<{ assetIds: Set<string>; itemIds: Set<string> }>(
Expand All @@ -75,7 +83,10 @@ export const fetchModel = async (client: ManagementClient): Promise<EnvironmentM
itemIds: new Set([...previous.itemIds, ...ids.itemIds]),
};
},
{ assetIds: new Set(), itemIds: new Set() },
{
assetIds: new Set(),
itemIds: new Set(spaces.map(s => s.web_spotlight_root_item?.id).filter(notNullOrUndefined)),
},
);

const assets = await fetchRequiredAssets(client, Array.from(allIds.assetIds));
Expand All @@ -87,6 +98,7 @@ export const fetchModel = async (client: ManagementClient): Promise<EnvironmentM
taxonomyGroups: taxonomies,
collections,
webSpotlight,
spaces,
assets,
items,
assetFolders,
Expand All @@ -100,6 +112,7 @@ export const transformSyncModel = (environmentModel: EnvironmentModel, logOption
const collectionsModel = transformCollectionsModel(environmentModel.collections);
const webSpotlightModel = transformWebSpotlightModel(environmentModel);
const assetFoldersModel = environmentModel.assetFolders.map(transformAssetFolderModel);
const spacesModel = transformSpacesModel(environmentModel);

return {
contentTypes: contentTypeModel,
Expand All @@ -108,6 +121,7 @@ export const transformSyncModel = (environmentModel: EnvironmentModel, logOption
collections: collectionsModel,
webSpotlight: webSpotlightModel,
assetFolders: assetFoldersModel,
spaces: spacesModel,
};
};

Expand Down Expand Up @@ -159,6 +173,10 @@ export const saveSyncModel = async (params: SaveModelParams) => {
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;
Expand Down
24 changes: 24 additions & 0 deletions src/modules/sync/modelTransfomers/spaceTransformers.ts
Original file line number Diff line number Diff line change
@@ -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<SpaceSyncModel> =>
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}}.`),
})) ?? [],
}));
5 changes: 4 additions & 1 deletion src/modules/sync/printDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -59,7 +62,7 @@ export const printDiff = (diffModel: DiffModel, logOptions: LogOptions) => {

const printDiffEntity = (
diffObject: DiffObject<unknown>,
entityName: "content types" | "content type snippets" | "taxonomy groups",
entityName: "content types" | "content type snippets" | "taxonomy groups" | "spaces",
logOptions: LogOptions,
) => {
if (diffObject.added.length) {
Expand Down
3 changes: 3 additions & 0 deletions src/modules/sync/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);
Expand Down
57 changes: 57 additions & 0 deletions src/modules/sync/sync/spaces.ts
Original file line number Diff line number Diff line change
@@ -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();
16 changes: 14 additions & 2 deletions src/modules/sync/types/diffModel.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,4 +31,10 @@ export type DiffModel = Readonly<{
collections: ReadonlyArray<PatchOperation>;
webSpotlight: WebSpotlightDiffModel;
assetFolders: ReadonlyArray<PatchOperation>;
spaces: DiffObject<
Replace<
RequiredCodename<SpaceModels.IAddSpaceData>,
{ collections: ReadonlyArray<SharedContracts.IReferenceObjectContract> }
>
>;
}>;
Loading

0 comments on commit fad8edb

Please sign in to comment.