Skip to content

Commit

Permalink
add validation schemas to readContentModelFromFolder
Browse files Browse the repository at this point in the history
  • Loading branch information
IvanKiral committed Sep 23, 2024
1 parent 0e64975 commit 960b03d
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 57 deletions.
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.

5 changes: 3 additions & 2 deletions 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.0",
Expand All @@ -69,4 +70,4 @@
"uuid": "^9.0.1",
"vitest": "^2.0.5"
}
}
}
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
109 changes: 60 additions & 49 deletions src/modules/sync/utils/getContentModel.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
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 { superiorFromEntries } from "../../../utils/object.js";
import { notNullOrUndefined } from "../../../utils/typeguards.js";
import { Either } from "../../../utils/types.js";
import {
assetFoldersFileName,
collectionsFileName,
Expand All @@ -16,60 +21,66 @@ import {
import { fetchModel, transformSyncModel } from "../generateSyncModel.js";
import { FileContentModel } from "../types/fileContentModel.js";
import {
AssetFolderSyncModel,
ContentTypeSnippetsSyncModel,
ContentTypeSyncModel,
LanguageSyncModel,
SpaceSyncModel,
TaxonomySyncModel,
WebSpotlightSyncModel,
} from "../types/syncModel.js";
SyncAssetFolderSchema,
SyncCollectionsSchema,
SyncLanguageSchema,
SyncSnippetsSchema,
SyncSpacesSchema,
SyncTaxonomySchema,
SyncTypesSchema,
SyncWebSpotlightSchema,
} from "../validation/syncSchemas.js";
import { getRequiredCodenames } from "./contentTypeHelpers.js";
import { fetchRequiredAssetsByCodename, fetchRequiredContentItemsByCodename } from "./fetchers.js";

type ParseWithError<Result> = Either<ParseResult<Result>, ParseError>;
type ParseError = { success: false; error: Error };
type ParseResult<Result> = { success: true; result: Result };

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>;

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

const isError = (a: ParseWithError<unknown>): a is ParseError => !a.success;

const isErrorEntry = <EntityName>(
tuple: readonly [EntityName, ParseWithError<unknown>],
): tuple is [EntityName, ParseError] => isError(tuple[1]);

const errors = parseReults.filter(isErrorEntry).map(([, val]) => val.error);

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

return superiorFromEntries(
parseReults.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,
};
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
6 changes: 5 additions & 1 deletion src/utils/object.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SuperiorOmit } from "./types.js";
import { ObjectFromTuple, SuperiorOmit } from "./types.js";

export const omit = <T extends object, K extends keyof T>(obj: T, props: K[]): SuperiorOmit<T, K> =>
Object.fromEntries(Object.entries(obj).filter(([key]) => !props.includes(key as K))) as SuperiorOmit<T, K>;
Expand All @@ -22,3 +22,7 @@ type ObjectPerKey<Key extends string, Value> = Key extends any ? { [k in Key]: V

export const makeObjectWithKey = <Key extends string, Value>(key: Key, value: Value): ObjectPerKey<Key, Value> =>
({ [key]: value }) as ObjectPerKey<Key, Value>;

export const superiorFromEntries = <InputType extends readonly (readonly [string, any])[]>(
entries: InputType,
): ObjectFromTuple<InputType> => Object.fromEntries(entries) as ObjectFromTuple<InputType>;
20 changes: 20 additions & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,23 @@ export type CombineTuples<Tuple1 extends ReadonlyArray<unknown>, Tuple2 extends
export type AddPropToObjectTuple<Tuple extends ReadonlyArray<Object>, ToAdd extends object> =
ReadonlyArray<Object> extends Tuple ? never
: { [Key in keyof Tuple]: ToAdd & Tuple[Key] };

export type Either<Left, Right> = Left | Right;

/**
* Maps a tuple of key-value pairs to an object type.
*
* @example
* // For the following tuple type:
* type Tuple = [['name', string], ['age', number]];
*
* // The resulting type will be:
* type Result = ObjectFromTuple<Tuple>;
* // {
* // name: string;
* // age: number;
* // }
*/
export type ObjectFromTuple<T extends readonly (readonly [string, any])[]> = {
[K in T[number][0]]: Extract<T[number], readonly [K, any]>[1];
};

0 comments on commit 960b03d

Please sign in to comment.