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 5 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
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
117 changes: 62 additions & 55 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 @@ -17,66 +22,68 @@ 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";

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 workflows = JSON.parse(
await fs.readFile(`${folderName}/${workflowsFileName}`, "utf8").catch(() => "[]"),
) as ReadonlyArray<WorkflowSyncModel>;
const parseReults = [
JiriLojda marked this conversation as resolved.
Show resolved Hide resolved
["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 isError = (a: ParseWithError<unknown>): a is ParseError => !a.success;
JiriLojda marked this conversation as resolved.
Show resolved Hide resolved

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