Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Devrel 1202/files validation #78

Merged
merged 9 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
"open": "^10.1.0",
"ts-pattern": "^5.3.1",
"yargs": "^17.7.2",
"zod": "^3.23.8"
"zod": "^3.23.8",
"zod-validation-error": "^3.4.0"
},
"devDependencies": {
"@kontent-ai/eslint-config": "^1.0.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import chalk from "chalk";
import { defaultCodename, defaultName, emptyId } from "../../../../constants/ids.js";
import { logInfo, LogOptions } from "../../../../log.js";
import { zip } from "../../../../utils/array.js";
import { second } from "../../../../utils/function.js";
import { serially } from "../../../../utils/requests.js";
import { notNullOrUndefined } from "../../../../utils/typeguards.js";
import { MapValues, ReplaceReferences } from "../../../../utils/types.js";
Expand Down Expand Up @@ -252,8 +253,3 @@ const createDefaultWorkflowData = (wf: Workflow): WorkflowModels.IUpdateWorkflow
published_step: { ...wf.published_step, codename: "published", name: "Published" },
archived_step: { ...wf.archived_step, codename: "archived", name: "Archived" },
});

const second = <Original, Guarded extends Original, First, Rest extends ReadonlyArray<unknown>>(
guard: (value: Original) => value is Guarded,
) =>
(tuple: readonly [First, Original, ...Rest]): tuple is readonly [First, Guarded, ...Rest] => guard(tuple[1]);
11 changes: 9 additions & 2 deletions src/modules/sync/diffEnvironments.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import chalk from "chalk";

import { logInfo, LogOptions } from "../../log.js";
import { logError, logInfo, LogOptions } from "../../log.js";
import { createClient } from "../../utils/client.js";
import { diff } from "./diff.js";
import { fetchModel, transformSyncModel } from "./generateSyncModel.js";
Expand Down Expand Up @@ -44,7 +44,14 @@ export const diffEnvironmentsInternal = async (params: DiffEnvironmentsParams, c
);

const sourceModel = "folderName" in params && params.folderName !== undefined
? await readContentModelFromFolder(params.folderName)
? await readContentModelFromFolder(params.folderName).catch(e => {
if (e instanceof AggregateError) {
logError(params, `Parsing model validation errors:\n${e.errors.map(e => e.message).join("\n")}`);
process.exit(1);
}
logError(params, JSON.stringify(e, Object.getOwnPropertyNames(e)));
process.exit(1);
})
: transformSyncModel(
await fetchModel(
createClient({
Expand Down
11 changes: 9 additions & 2 deletions src/modules/sync/syncModelRun.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ManagementClient } from "@kontent-ai/management-sdk";

import { LogOptions } from "../../log.js";
import { logError, LogOptions } from "../../log.js";
import { createClient } from "../../utils/client.js";
import { diff } from "./diff.js";
import { fetchModel, transformSyncModel } from "./generateSyncModel.js";
Expand Down Expand Up @@ -57,7 +57,14 @@ export const getDiffModel = async (
}

const sourceModel = "folderName" in params
? await readContentModelFromFolder(params.folderName)
? await readContentModelFromFolder(params.folderName).catch(e => {
if (e instanceof AggregateError) {
logError(params, `Parsing model validation errors:\n${e.errors.map(e => e.message).join("\n")}`);
process.exit(1);
}
logError(params, JSON.stringify(e, Object.getOwnPropertyNames(e)));
process.exit(1);
})
: transformSyncModel(
await fetchModel(createClient({
environmentId: params.sourceEnvironmentId,
Expand Down
20 changes: 10 additions & 10 deletions src/modules/sync/types/syncModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,16 @@ export type SyncSubpagesElement =
& ReplaceReferences<ContentTypeElements.ISubpagesElement>
& Pick<SyncLinkedItemsElement, "default">; // The property is missing in the SDK type

type SyncSnippetCustomElement = SnippetElement<SyncCustomElement>;
type SyncSnippetMultipleChoiceElement = SnippetElement<SyncMultipleChoiceElement>;
type SyncSnippetAssetElement = SnippetElement<SyncAssetElement>;
type SyncSnippetRichTextElement = SnippetElement<SyncRichTextElement>;
type SyncSnippetTaxonomyElement = SnippetElement<SyncTaxonomyElement>;
type SyncSnippetLinkedItemsElement = SnippetElement<SyncLinkedItemsElement>;
type SyncSnippetGuidelinesElement = SnippetElement<SyncGuidelinesElement>;
type SyncSnippetTextElement = SnippetElement<SyncTextElement>;
type SyncSnippetDateTimeElement = SnippetElement<SyncDateTimeElement>;
type SyncSnippetNumberElement = SnippetElement<SyncNumberElement>;
export type SyncSnippetCustomElement = SnippetElement<SyncCustomElement>;
export type SyncSnippetMultipleChoiceElement = SnippetElement<SyncMultipleChoiceElement>;
export type SyncSnippetAssetElement = SnippetElement<SyncAssetElement>;
export type SyncSnippetRichTextElement = SnippetElement<SyncRichTextElement>;
export type SyncSnippetTaxonomyElement = SnippetElement<SyncTaxonomyElement>;
export type SyncSnippetLinkedItemsElement = SnippetElement<SyncLinkedItemsElement>;
export type SyncSnippetGuidelinesElement = SnippetElement<SyncGuidelinesElement>;
export type SyncSnippetTextElement = SnippetElement<SyncTextElement>;
export type SyncSnippetDateTimeElement = SnippetElement<SyncDateTimeElement>;
export type SyncSnippetNumberElement = SnippetElement<SyncNumberElement>;

export type SyncSnippetElement =
| SyncSnippetCustomElement
Expand Down
112 changes: 57 additions & 55 deletions src/modules/sync/utils/getContentModel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { ManagementClient } from "@kontent-ai/management-sdk";
import * as fs from "fs/promises";
import { z } from "zod";
import { fromError } from "zod-validation-error";

import { LogOptions } from "../../../log.js";
import { throwError } from "../../../utils/error.js";
import { second } from "../../../utils/function.js";
import { superiorFromEntries } from "../../../utils/object.js";
import { notNullOrUndefined } from "../../../utils/typeguards.js";
import {
assetFoldersFileName,
Expand All @@ -17,66 +22,63 @@ import {
import { fetchModel, transformSyncModel } from "../generateSyncModel.js";
import { FileContentModel } from "../types/fileContentModel.js";
import {
AssetFolderSyncModel,
ContentTypeSnippetsSyncModel,
ContentTypeSyncModel,
LanguageSyncModel,
SpaceSyncModel,
TaxonomySyncModel,
WebSpotlightSyncModel,
WorkflowSyncModel,
} from "../types/syncModel.js";
SyncAssetFolderSchema,
SyncCollectionsSchema,
SyncLanguageSchema,
SyncSnippetsSchema,
SyncSpacesSchema,
SyncTaxonomySchema,
SyncTypesSchema,
SyncWebSpotlightSchema,
SyncWorkflowSchema,
} from "../validation/syncSchemas.js";
import { getRequiredCodenames } from "./contentTypeHelpers.js";
import { fetchRequiredAssetsByCodename, fetchRequiredContentItemsByCodename } from "./fetchers.js";

export const readContentModelFromFolder = async (folderName: string): Promise<FileContentModel> => {
// in future we should use typeguard to check whether the content is valid
const contentTypes = JSON.parse(
await fs.readFile(`${folderName}/${contentTypesFileName}`, "utf8"),
) as ReadonlyArray<ContentTypeSyncModel>;

const snippets = JSON.parse(
await fs.readFile(`${folderName}/${contentTypeSnippetsFileName}`, "utf8"),
) as ReadonlyArray<ContentTypeSnippetsSyncModel>;
const taxonomyGroups = JSON.parse(
await fs.readFile(`${folderName}/${taxonomiesFileName}`, "utf8"),
) as ReadonlyArray<TaxonomySyncModel>;

const collections = JSON.parse(
await fs.readFile(`${folderName}/${collectionsFileName}`, "utf8"),
) as ReadonlyArray<TaxonomySyncModel>;

const webSpotlight = JSON.parse(
await fs.readFile(`${folderName}/${webSpotlightFileName}`, "utf8"),
) as WebSpotlightSyncModel;

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

const spaces = JSON.parse(
await fs.readFile(`${folderName}/${spacesFileName}`, "utf8").catch(() => "[]"),
) as ReadonlyArray<SpaceSyncModel>;
type ParseWithError<Result> = ParseResult<Result> | ParseError;
type ParseError = { success: false; error: Error };
type ParseResult<Result> = { success: true; result: Result };

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

const workflows = JSON.parse(
await fs.readFile(`${folderName}/${workflowsFileName}`, "utf8").catch(() => "[]"),
) as ReadonlyArray<WorkflowSyncModel>;
export const readContentModelFromFolder = async (folderName: string): Promise<FileContentModel> => {
const parseResults = [
["contentTypes", await parseSchema(SyncTypesSchema, folderName, contentTypesFileName)],
["contentTypeSnippets", await parseSchema(SyncSnippetsSchema, folderName, contentTypeSnippetsFileName)],
["taxonomyGroups", await parseSchema(SyncTaxonomySchema, folderName, taxonomiesFileName)],
["collections", await parseSchema(SyncCollectionsSchema, folderName, collectionsFileName)],
["webSpotlight", await parseSchema(SyncWebSpotlightSchema, folderName, webSpotlightFileName)],
["assetFolders", await parseSchema(SyncAssetFolderSchema, folderName, assetFoldersFileName)],
["spaces", await parseSchema(SyncSpacesSchema, folderName, spacesFileName)],
["languages", await parseSchema(SyncLanguageSchema, folderName, languagesFileName)],
["workflows", await parseSchema(SyncWorkflowSchema, folderName, workflowsFileName)],
] as const;

const errors = parseResults.filter(r => second<ParseWithError<unknown>, ParseError, string, []>(x => !x.success)(r))
.map(([, value]) => value.error);

if (errors.length) {
throw new AggregateError(errors);
}

return superiorFromEntries(
parseResults.map(([key, value]) =>
value.success ? [key, value.result] : throwError("Error with parsing the model from folder.")
),
);
};

return {
contentTypes,
contentTypeSnippets: snippets,
taxonomyGroups: taxonomyGroups,
collections,
webSpotlight,
assetFolders,
spaces,
languages,
workflows,
};
const parseSchema = async <Output>(
schema: z.ZodType<Output, z.ZodTypeDef, unknown>,
folderName: string,
filename: string,
): Promise<ParseWithError<Output>> => {
const result = schema.safeParse(JSON.parse(await fs.readFile(`${folderName}/${filename}`, "utf8")));

return result.success
? { success: true, result: result.data }
: {
success: false,
error: new Error(fromError(result.error, { unionSeparator: " or\n", prefix: filename }).message),
};
};

type AssetItemsCodenames = Readonly<{
Expand Down
3 changes: 3 additions & 0 deletions src/modules/sync/validation/commonSchemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { z } from "zod";

export const CodenameReferenceSchema = z.strictObject({ codename: z.string() });
Loading