Skip to content

Commit

Permalink
Add asset folders to model sync
Browse files Browse the repository at this point in the history
  • Loading branch information
JiriLojda committed Sep 3, 2024
1 parent 804fee4 commit dbb4aa8
Show file tree
Hide file tree
Showing 18 changed files with 280 additions and 2 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 @@ -2,3 +2,4 @@ export const contentTypesFileName = "contentTypes.json";
export const contentTypeSnippetsFileName = "contentTypeSnippets.json";
export const taxonomiesFileName = "taxonomyGroups.json";
export const webSpotlightFileName = "webSpotlight.json";
export const assetFoldersFileName = "assetFolders.json";
7 changes: 7 additions & 0 deletions src/modules/sync/diff.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { assetFoldersHandler } from "./diff/assetFolder.js";
import { Handler } from "./diff/combinators.js";
import { makeContentTypeHandler, wholeContentTypesHandler } from "./diff/contentType.js";
import { makeContentTypeSnippetHandler, wholeContentTypeSnippetsHandler } from "./diff/contentTypeSnippet.js";
Expand Down Expand Up @@ -58,12 +59,18 @@ export const diff = (params: DiffParams): DiffModel => {
params.targetEnvModel.webSpotlight,
);

const assetFoldersDiffModel = assetFoldersHandler(
params.sourceEnvModel.assetFolders,
params.targetEnvModel.assetFolders,
);

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,
assetFolders: assetFoldersDiffModel,
};
};

Expand Down
12 changes: 12 additions & 0 deletions src/modules/sync/diff/assetFolder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AssetFolderSyncModel } from "../types/syncModel.js";
import { baseHandler, Handler, makeArrayHandler, makeObjectHandler } from "./combinators.js";

export const assetFolderHandler: Handler<AssetFolderSyncModel> = makeObjectHandler({
name: baseHandler,
folders: { contextfulHandler: () => makeArrayHandler(f => f.codename, assetFolderHandler) },
});

export const assetFoldersHandler: Handler<ReadonlyArray<AssetFolderSyncModel>> = makeArrayHandler(
f => f.codename,
assetFolderHandler,
);
9 changes: 9 additions & 0 deletions src/modules/sync/generateSyncModel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AssetContracts,
AssetFolderContracts,
ContentItemContracts,
ManagementClient,
TaxonomyContracts,
Expand All @@ -18,6 +19,7 @@ import {
taxonomiesFileName,
webSpotlightFileName,
} from "./constants/filename.js";
import { transformAssetFolderModel } from "./modelTransfomers/assetFolder.js";
import { transformContentTypeModel } from "./modelTransfomers/contentTypes.js";
import { transformContentTypeSnippetsModel } from "./modelTransfomers/contentTypeSnippets.js";
import { transformTaxonomyGroupsModel } from "./modelTransfomers/taxonomyGroups.js";
Expand All @@ -26,6 +28,7 @@ import { ContentTypeSnippetsWithUnionElements, ContentTypeWithUnionElements } fr
import { FileContentModel } from "./types/fileContentModel.js";
import { getRequiredIds } from "./utils/contentTypeHelpers.js";
import {
fetchAssetFolders,
fetchContentTypes,
fetchContentTypeSnippets,
fetchRequiredAssets,
Expand All @@ -39,6 +42,7 @@ export type EnvironmentModel = {
contentTypeSnippets: ReadonlyArray<ContentTypeSnippetsWithUnionElements>;
contentTypes: ReadonlyArray<ContentTypeWithUnionElements>;
webSpotlight: WebSpotlightContracts.IWebSpotlightStatus;
assetFolders: ReadonlyArray<AssetFolderContracts.IAssetFolderContract>;
assets: ReadonlyArray<AssetContracts.IAssetModelContract>;
items: ReadonlyArray<ContentItemContracts.IContentItemModelContract>;
};
Expand All @@ -52,6 +56,8 @@ export const fetchModel = async (client: ManagementClient): Promise<EnvironmentM

const webSpotlight = await fetchWebSpotlight(client);

const assetFolders = await fetchAssetFolders(client);

const allIds = [...contentTypes, ...contentTypeSnippets].reduce<{ assetIds: Set<string>; itemIds: Set<string> }>(
(previous, type) => {
const ids = getRequiredIds(type.elements);
Expand All @@ -74,6 +80,7 @@ export const fetchModel = async (client: ManagementClient): Promise<EnvironmentM
webSpotlight,
assets,
items,
assetFolders,
};
};

Expand All @@ -82,12 +89,14 @@ export const transformSyncModel = (environmentModel: EnvironmentModel, logOption
const contentTypeSnippetModel = transformContentTypeSnippetsModel(environmentModel, logOptions);
const taxonomyGroupsModel = transformTaxonomyGroupsModel(environmentModel.taxonomyGroups);
const webSpotlightModel = transformWebSpotlightModel(environmentModel);
const assetFoldersModel = environmentModel.assetFolders.map(transformAssetFolderModel);

return {
contentTypes: contentTypeModel,
contentTypeSnippets: contentTypeSnippetModel,
taxonomyGroups: taxonomyGroupsModel,
webSpotlight: webSpotlightModel,
assetFolders: assetFoldersModel,
};
};

Expand Down
11 changes: 11 additions & 0 deletions src/modules/sync/modelTransfomers/assetFolder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AssetFolderContracts } from "@kontent-ai/management-sdk";

import { omit } from "../../../utils/object.js";
import { AssetFolderSyncModel } from "../types/syncModel.js";

export const transformAssetFolderModel = (
environmentModel: AssetFolderContracts.IAssetFolderContract,
): AssetFolderSyncModel => ({
...omit(environmentModel, ["id", "external_id"]),
folders: environmentModel.folders.map(transformAssetFolderModel),
});
15 changes: 13 additions & 2 deletions src/modules/sync/printDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@ import {
resolveOutputPath,
} from "./utils/fileUtils.js";
import { DiffData, resolveHtmlTemplate } from "./utils/htmlRenderers.js";
import { PatchOperation } from "./types/patchOperation.js";

export const printDiff = (diffModel: DiffModel, logOptions: LogOptions) => {
logInfo(logOptions, "standard", chalk.blue.bold("TAXONOMY GROUPS:"));
logInfo(logOptions, "standard", chalk.blue.bold("ASSET FOLDERS:"));
if (diffModel.assetFolders.length) {
diffModel.assetFolders.forEach(op => printPatchOperation(op, logOptions));
} else {
logInfo(logOptions, "standard", "No asset folders to update.");
}

logInfo(logOptions, "standard", chalk.blue.bold("\nTAXONOMY GROUPS:"));
printDiffEntity(diffModel.taxonomyGroups, "taxonomy groups", logOptions);

logInfo(logOptions, "standard", chalk.blue.bold("\nCONTENT TYPE SNIPPETS:"));
Expand Down Expand Up @@ -62,7 +70,7 @@ const printDiffEntity = (
Array.from(diffObject.updated.entries()).sort().forEach(([codename, value]) => {
if (value.length) {
logInfo(logOptions, "standard", `Entity codename: ${chalk.blue(codename)}`);
value.forEach(v => logInfo(logOptions, "standard", `${chalk.yellow(JSON.stringify(v, null, 2))}\n`));
value.forEach(v => printPatchOperation(v, logOptions));
}
});
} else {
Expand Down Expand Up @@ -102,3 +110,6 @@ export const createAdvancedDiffFile = (diffData: DiffData) => {
openOutputFile(resolvedPath, logOptions);
}
};

const printPatchOperation = (operation: PatchOperation, logOptions: LogOptions) =>
logInfo(logOptions, "standard", `${chalk.yellow(JSON.stringify(operation, null, 2))}\n`);
3 changes: 3 additions & 0 deletions src/modules/sync/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ManagementClient } from "@kontent-ai/management-sdk";

import { logInfo, LogOptions } from "../../log.js";
import { serially } from "../../utils/requests.js";
import { syncAssetFolders } from "./sync/assetFolders.js";
import {
addElementsIntoSnippetsWithoutReferences,
addSnippetsReferences,
Expand All @@ -17,6 +18,8 @@ import { DiffModel } from "./types/diffModel.js";

export const sync = async (client: ManagementClient, diff: DiffModel, logOptions: LogOptions) => {
// there order of these operations is very important
await syncAssetFolders(client, diff.assetFolders, logOptions);

await syncTaxonomies(client, diff.taxonomyGroups, logOptions);

logInfo(logOptions, "standard", "Adding content type snippets");
Expand Down
52 changes: 52 additions & 0 deletions src/modules/sync/sync/assetFolders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { AssetFolderModels, ManagementClient } from "@kontent-ai/management-sdk";
import { match } from "ts-pattern";

import { logInfo, LogOptions } from "../../../log.js";
import { throwError } from "../../../utils/error.js";
import { apply } from "../../../utils/function.js";
import { omit } from "../../../utils/object.js";
import { DiffModel } from "../types/diffModel.js";
import { getTargetCodename, PatchOperation } from "../types/patchOperation.js";

export const syncAssetFolders = async (
client: ManagementClient,
operations: DiffModel["assetFolders"],
logOptions: LogOptions,
) => {
logInfo(logOptions, "standard", "Updating asset folders");

if (!operations.length) {
return;
}

await client
.modifyAssetFolders()
.withData(operations.map(convertOperation))
.toPromise();
};

const convertOperation = (operation: PatchOperation): AssetFolderModels.IModifyAssetFolderData => {
const targetCodename = apply(codename => ({ codename }), getTargetCodename(operation));

return match(omit(operation, ["path"]))
.returnType<AssetFolderModels.IModifyAssetFolderData>()
.with({ op: "addInto" }, op => ({
...op,
value: op.value as AssetFolderModels.IAssetFolderValue,
reference: targetCodename ?? undefined,
}))
.with(
{ op: "move" },
op => throwError(`Move operation is not supported for asset folders. Found operation: ${JSON.stringify(op)}`),
)
.with({ op: "replace" }, op => ({
...omit(op, ["oldValue"]),
op: "rename",
reference: targetCodename ?? throwError(`Missing target codename in ${JSON.stringify(operation)}`),
value: typeof op.value === "string" ? op.value : throwError("Invalid value type"),
}))
.otherwise(op => ({
...omit(op, ["oldValue"]),
reference: targetCodename ?? throwError(`Missing target codename in ${JSON.stringify(operation)}`),
}));
};
1 change: 1 addition & 0 deletions src/modules/sync/types/diffModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ export type DiffModel = Readonly<{
contentTypeSnippets: DiffObject<RequiredCodename<ContentTypeSnippetModels.IAddContentTypeSnippetData>>;
contentTypes: DiffObject<RequiredCodename<ContentTypeModels.IAddContentTypeData>>;
webSpotlight: WebSpotlightDiffModel;
assetFolders: ReadonlyArray<PatchOperation>;
}>;
2 changes: 2 additions & 0 deletions src/modules/sync/types/fileContentModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AssetFolderSyncModel,
ContentTypeSnippetsSyncModel,
ContentTypeSyncModel,
TaxonomySyncModel,
Expand All @@ -10,4 +11,5 @@ export type FileContentModel = Readonly<{
contentTypeSnippets: ReadonlyArray<ContentTypeSnippetsSyncModel>;
contentTypes: ReadonlyArray<ContentTypeSyncModel>;
webSpotlight: WebSpotlightSyncModel;
assetFolders: ReadonlyArray<AssetFolderSyncModel>;
}>;
6 changes: 6 additions & 0 deletions src/modules/sync/types/syncModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AssetFolderContracts,
ContentTypeContracts,
ContentTypeElements,
ContentTypeSnippetContracts,
Expand Down Expand Up @@ -123,6 +124,11 @@ export type WebSpotlightSyncModel = Replace<
{ root_type: Readonly<{ codename: string }> | null }
>;

export type AssetFolderSyncModel = Replace<
Omit<AssetFolderContracts.IAssetFolderContract, "id" | "external_id">,
Readonly<{ folders: ReadonlyArray<AssetFolderSyncModel> }>
>;

export const isSyncCustomElement = (entity: unknown): entity is SyncCustomElement =>
typeof entity === "object" && entity !== null && "type" in entity && entity.type === "custom";

Expand Down
6 changes: 6 additions & 0 deletions src/modules/sync/utils/fetchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,9 @@ export const fetchWebSpotlight = (client: ManagementClient) =>
.checkWebSpotlightStatus()
.toPromise()
.then(res => res.rawData);

export const fetchAssetFolders = (client: ManagementClient) =>
client
.listAssetFolders()
.toPromise()
.then(res => res.rawData.folders);
7 changes: 7 additions & 0 deletions src/modules/sync/utils/getContentModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as fs from "fs/promises";

import { LogOptions } from "../../../log.js";
import {
assetFoldersFileName,
contentTypesFileName,
contentTypeSnippetsFileName,
taxonomiesFileName,
Expand All @@ -11,6 +12,7 @@ import {
import { fetchModel, transformSyncModel } from "../generateSyncModel.js";
import { FileContentModel } from "../types/fileContentModel.js";
import {
AssetFolderSyncModel,
ContentTypeSnippetsSyncModel,
ContentTypeSyncModel,
TaxonomySyncModel,
Expand All @@ -36,11 +38,16 @@ export const readContentModelFromFolder = async (folderName: string): Promise<Fi
await fs.readFile(`${folderName}/${webSpotlightFileName}`, "utf8"),
) as WebSpotlightSyncModel;

const assetFolders = JSON.parse(
await fs.readFile(`${folderName}/${assetFoldersFileName}`, "utf8").catch(() => "[]"),
) as ReadonlyArray<AssetFolderSyncModel>;

return {
contentTypes,
contentTypeSnippets: snippets,
taxonomyGroups: taxonomyGroups,
webSpotlight,
assetFolders,
};
};

Expand Down
89 changes: 89 additions & 0 deletions tests/unit/syncModel/assetFolderTransformer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { AssetFolderContracts } from "@kontent-ai/management-sdk";
import { describe, expect, it } from "vitest";

import { transformAssetFolderModel } from "../../../src/modules/sync/modelTransfomers/assetFolder.js";
import { AssetFolderSyncModel } from "../../../src/modules/sync/types/syncModel.js";

describe("transformAssetFolderModel", () => {
it("correctly transforms asset folder model to sync model", () => {
const input = {
id: "01160b00-9d58-446d-bd13-5d9ceeb55d3c",
name: "folderName",
codename: "folderCodename",
external_id: "externalId",
folders: [
{
id: "folderId",
name: "nestedFolder",
codename: "nestedFolderCodename",
external_id: "nestedExternalId",
folders: [],
},
],
} as const satisfies AssetFolderContracts.IAssetFolderContract;

const expectedOutput: AssetFolderSyncModel = {
name: "folderName",
codename: "folderCodename",
folders: [
{
name: "nestedFolder",
codename: "nestedFolderCodename",
folders: [],
},
],
};

const result = transformAssetFolderModel(input);

expect(result).toStrictEqual(expectedOutput);
});

it("correctly transforms asset folder model to sync model with nested folders", () => {
const input = {
id: "01160b00-9d58-446d-bd13-5d9ceeb55d3c",
name: "folderName",
codename: "folderCodename",
external_id: "externalId",
folders: [
{
id: "folderId",
name: "nestedFolder",
codename: "nestedFolderCodename",
external_id: "nestedExternalId",
folders: [
{
id: "nestedFolderId",
name: "nestedNestedFolder",
codename: "nestedNestedFolderCodename",
external_id: "nestedNestedExternalId",
folders: [],
},
],
},
],
} as const satisfies AssetFolderContracts.IAssetFolderContract;

const expectedOutput: AssetFolderSyncModel = {
name: "folderName",
codename: "folderCodename",
folders: [
{
name: "nestedFolder",
codename: "nestedFolderCodename",
folders: [
{
name: "nestedNestedFolder",
codename: "nestedNestedFolderCodename",
folders: [],
},
],
},
],
};

const result = transformAssetFolderModel(input);

expect(result).toStrictEqual(expectedOutput);
});
});
1 change: 1 addition & 0 deletions tests/unit/syncModel/contentTypeTransformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const createEnvironmentModel = (contentTypes: ReadonlyArray<ContentTypeWithUnion
contentTypes: contentTypes,
contentTypeSnippets: [],
taxonomyGroups: [],
assetFolders: [],
webSpotlight: {
enabled: false,
root_type: null,
Expand Down
Loading

0 comments on commit dbb4aa8

Please sign in to comment.