@kontent-ai/model-generator
Version:
This utility generates strongly-typed models for Delivery JS SDK, Migration toolkit or just general scripting to improve the experience when referencing Kontent.ai related objects.
513 lines (466 loc) • 20 kB
text/typescript
import type {
AssetFolderModels,
CollectionModels,
ContentTypeElements,
ContentTypeModels,
ContentTypeSnippetModels,
CustomAppModels,
EnvironmentModels,
LanguageModels,
PreviewModels,
RoleModels,
SpaceModels,
TaxonomyModels,
WebhookModels,
WorkflowModels,
} from "@kontent-ai/management-sdk";
import { match } from "ts-pattern";
import { toGuidelinesComment, wrapComment } from "../../core/comment.utils.js";
import type { EnvironmentEntity, FlattenedElement, GeneratedFile, GeneratedSet, ValidateKeys } from "../../core/core.models.js";
import { findRequired, getStringOrUndefinedAsPropertyValue, isNotUndefined, toSafePropertyValue } from "../../core/core.utils.js";
import { getFlattenedElements } from "../../core/element.utils.js";
import { resolvePropertyName } from "../../core/resolvers.js";
type WorkflowStep = {
readonly name: string;
readonly codename: string;
readonly id: string;
};
export type EnvironmentEntities = ValidateKeys<
EnvironmentEntity,
{
readonly contentTypes: readonly Readonly<ContentTypeModels.ContentType>[] | undefined;
readonly languages: readonly Readonly<LanguageModels.LanguageModel>[] | undefined;
readonly taxonomies: readonly Readonly<TaxonomyModels.Taxonomy>[] | undefined;
readonly workflows: readonly Readonly<WorkflowModels.Workflow>[] | undefined;
readonly assetFolders: readonly Readonly<AssetFolderModels.AssetFolder>[] | undefined;
readonly collections: readonly Readonly<CollectionModels.Collection>[] | undefined;
readonly roles: readonly Readonly<RoleModels.Role>[] | undefined;
readonly snippets: readonly Readonly<ContentTypeSnippetModels.ContentTypeSnippet>[] | undefined;
readonly webhooks: readonly Readonly<WebhookModels.Webhook>[] | undefined;
readonly customApps: readonly Readonly<CustomAppModels.CustomApp>[] | undefined;
readonly spaces: readonly Readonly<SpaceModels.Space>[] | undefined;
readonly previewUrls: Readonly<PreviewModels.PreviewConfiguration> | undefined;
}
>;
export type EnvironmentGeneratorConfig = {
readonly environmentInfo: Readonly<EnvironmentModels.EnvironmentInformationModel>;
readonly environmentEntities: EnvironmentEntities;
readonly entitiesToCreate: readonly EnvironmentEntity[];
};
export function getEnvironmentGenerator(config: EnvironmentGeneratorConfig) {
const generateEnvironmentModels = (): GeneratedSet => {
return {
folderName: "structure",
files: [
getEntityFile({
entitiesToCreate: config.entitiesToCreate,
entitiesProp: "languages",
filename: "languages.ts",
exportedConstName: "languages",
entities: config.environmentEntities.languages,
getCode: getLanguages,
}),
getEntityFile({
entitiesToCreate: config.entitiesToCreate,
entitiesProp: "collections",
filename: "collections.ts",
exportedConstName: "collections",
entities: config.environmentEntities.collections,
getCode: getCollections,
}),
getEntityFile({
entitiesToCreate: config.entitiesToCreate,
entitiesProp: "contentTypes",
filename: "contentTypes.ts",
exportedConstName: "contentTypes",
entities: config.environmentEntities.contentTypes,
getCode: getContentTypes,
}),
getEntityFile({
entitiesToCreate: config.entitiesToCreate,
entitiesProp: "snippets",
filename: "contentTypeSnippets.ts",
exportedConstName: "contentTypeSnippets",
entities: config.environmentEntities.snippets,
getCode: getSnippets,
}),
getEntityFile({
entitiesToCreate: config.entitiesToCreate,
entitiesProp: "workflows",
filename: "workflows.ts",
exportedConstName: "workflows",
entities: config.environmentEntities.workflows,
getCode: getWorkflows,
}),
getEntityFile({
entitiesToCreate: config.entitiesToCreate,
entitiesProp: "roles",
filename: "roles.ts",
exportedConstName: "roles",
entities: config.environmentEntities.roles,
getCode: getRoles,
}),
getEntityFile({
entitiesToCreate: config.entitiesToCreate,
entitiesProp: "assetFolders",
filename: "assetFolders.ts",
exportedConstName: "assetFolders",
entities: config.environmentEntities.assetFolders,
getCode: getAssetFolders,
}),
getEntityFile({
entitiesToCreate: config.entitiesToCreate,
entitiesProp: "webhooks",
filename: "webhooks.ts",
exportedConstName: "webhooks",
entities: config.environmentEntities.webhooks,
getCode: getWebhooks,
}),
getEntityFile({
entitiesToCreate: config.entitiesToCreate,
entitiesProp: "taxonomies",
filename: "taxonomies.ts",
exportedConstName: "taxonomies",
entities: config.environmentEntities.taxonomies,
getCode: getTaxonomies,
}),
getEntityFile({
entitiesToCreate: config.entitiesToCreate,
entitiesProp: "customApps",
filename: "customApps.ts",
exportedConstName: "customApps",
entities: config.environmentEntities.customApps,
getCode: getCustomApps,
}),
getEntityFile({
entitiesToCreate: config.entitiesToCreate,
entitiesProp: "spaces",
filename: "spaces.ts",
exportedConstName: "spaces",
entities: config.environmentEntities.spaces,
getCode: getSpaces,
}),
getEntityFile({
entitiesToCreate: config.entitiesToCreate,
entitiesProp: "previewUrls",
filename: "previewUrls.ts",
exportedConstName: "previewUrls",
entities: config.environmentEntities.previewUrls,
getCode: getPreviewUrls,
}),
].filter(isNotUndefined),
};
};
const getEntityFile = <TProp extends keyof EnvironmentEntities>({
entities,
getCode,
filename,
entitiesToCreate,
exportedConstName,
entitiesProp,
}: {
readonly entitiesToCreate: readonly EnvironmentEntity[];
readonly entitiesProp: TProp;
readonly filename: string;
readonly exportedConstName: string;
readonly entities: EnvironmentEntities[TProp];
readonly getCode: (entities: NonNullable<EnvironmentEntities[TProp]>) => string;
}): GeneratedFile | undefined => {
if (!(entities && entitiesToCreate.includes(entitiesProp))) {
return undefined;
}
return {
filename: filename,
text: wrapInConst({ constName: exportedConstName, text: getCode(entities) }),
};
};
const getLanguages = (languages: readonly Readonly<LanguageModels.LanguageModel>[]): string => {
return languages.reduce((code, language, index) => {
const isLast = index === languages.length - 1;
return `${code}\n
${wrapComment(language.name)}
${resolvePropertyName(language.codename)}: {
name: '${toSafePropertyValue(language.name)}',
codename: '${language.codename}',
id: '${language.id}',
isActive: ${language.isActive ? "true" : "false"},
isDefault: ${language.isDefault ? "true" : "false"},
fallbackLanguageId: ${getStringOrUndefinedAsPropertyValue(language.fallbackLanguage?.id)},
externalId: ${getStringOrUndefinedAsPropertyValue(language.externalId)},
}${!isLast ? ",\n" : ""}`;
}, "");
};
const getWorkflows = (workflows: readonly Readonly<WorkflowModels.Workflow>[]): string => {
return workflows.reduce((code, workflow, index) => {
const isLast = index === workflows.length - 1;
return `${code}\n
${wrapComment(workflow.name)}
${workflow.codename}: {
name: '${toSafePropertyValue(workflow.name)}',
codename: '${workflow.codename}',
id: '${workflow.id}',
steps: ${getWorkflowSteps(workflow)}
}${!isLast ? ",\n" : ""}`;
}, "");
};
const getCustomApps = (customApps: readonly Readonly<CustomAppModels.CustomApp>[]): string => {
return customApps.reduce((code, customApp, index) => {
const isLast = index === customApps.length - 1;
return `${code}\n
${wrapComment(customApp.name)}
${resolvePropertyName(customApp.codename)}: {
name: '${toSafePropertyValue(customApp.name)}',
codename: '${customApp.codename}',
sourceUrl: '${customApp.source_url}'
}${!isLast ? ",\n" : ""}`;
}, "");
};
const getSpaces = (spaces: readonly Readonly<SpaceModels.Space>[]): string => {
return spaces.reduce((code, space, index) => {
const isLast = index === spaces.length - 1;
return `${code}\n
${wrapComment(space.name)}
${resolvePropertyName(space.codename)}: {
name: '${toSafePropertyValue(space.name)}',
codename: '${space.codename}',
id: '${space.id}'
}${!isLast ? ",\n" : ""}`;
}, "");
};
const getPreviewUrls = (previewConfiguration: Readonly<PreviewModels.PreviewConfiguration>): string => {
const spaces = config.environmentEntities.spaces;
const contentTypes = config.environmentEntities.contentTypes;
if (!spaces) {
throw new Error("Spaces are expected to be present in the environment entities.");
}
if (!contentTypes) {
throw new Error("Content types are expected to be present in the environment entities.");
}
return `
spaceDomains: { ${previewConfiguration.spaceDomains.reduce((code, spaceDomain, index) => {
const isLast = index === previewConfiguration.spaceDomains.length - 1;
const space = findRequired(
spaces,
(m) => m.id === spaceDomain.space.id,
`Space with id '${spaceDomain.space.id}' not found.`,
);
return `${code}\n
${space.codename}: {
spaceName: '${toSafePropertyValue(space.name)}',
spaceCodename: '${space.codename}',
domain: '${spaceDomain.domain}'
}${!isLast ? ",\n" : ""}`;
}, "")} },
previewUrlPatterns: { ${previewConfiguration.previewUrlPatterns.reduce((code, previewUrlPattern, index) => {
const isLast = index === previewConfiguration.previewUrlPatterns.length - 1;
const contentType = findRequired(
contentTypes,
(m) => m.id === previewUrlPattern.contentType.id,
`Content type with id '${previewUrlPattern.contentType.id}' not found.`,
);
return `${code}\n
${contentType.codename}: {
contentTypeName: '${toSafePropertyValue(contentType.name)}',
contentTypeCodename: '${contentType.codename}',
urlPatterns: { ${previewUrlPattern.urlPatterns.reduce((code, urlPattern, index) => {
const isLast = index === previewUrlPattern.urlPatterns.length - 1;
const space = config.environmentEntities.spaces?.find((m) => m.id === urlPattern.space?.id);
return `${code}\n
${space?.codename ?? "default"}: {
spaceName: ${getStringOrUndefinedAsPropertyValue(space?.name)},
spaceCodename: ${getStringOrUndefinedAsPropertyValue(space?.codename)},
url: '${urlPattern.urlPattern}',
}${!isLast ? ",\n" : ""}`;
}, "")} }
}${!isLast ? ",\n" : ""}`;
}, "")} }
`;
};
const getAssetFolders = (assetFolders: readonly Readonly<AssetFolderModels.AssetFolder>[], isFirstLevel = true): string => {
return (
assetFolders.reduce(
(code, assetFolder, index) => {
const isLast = index === assetFolders.length - 1;
return `${code}\n
${wrapComment(assetFolder.name)}
${assetFolder.codename}: {
name: '${toSafePropertyValue(assetFolder.name)}',
codename: '${assetFolder.codename}',
id: '${assetFolder.id}',
externalId: ${getStringOrUndefinedAsPropertyValue(assetFolder.externalId)},
folders: ${getAssetFolders(assetFolder.folders, false)}}${!isLast ? ",\n" : ""}`;
},
!isFirstLevel ? "{" : "",
) + (!isFirstLevel ? "}" : "")
);
};
const getSnippets = (snippets: readonly Readonly<ContentTypeSnippetModels.ContentTypeSnippet>[]): string => {
return snippets.reduce((code, snippet, index) => {
const isLast = index === snippets.length - 1;
return `${code}\n
${wrapComment(snippet.name)}
${snippet.codename}: {
name: '${toSafePropertyValue(snippet.name)}',
codename: '${snippet.codename}',
id: '${snippet.id}',
externalId: ${getStringOrUndefinedAsPropertyValue(snippet.externalId)},
elements: {${getContentTypeElements(snippet.elements)}}
}${!isLast ? ",\n" : ""}`;
}, "");
};
const getContentTypes = (contentTypes: readonly Readonly<ContentTypeModels.ContentType>[]): string => {
return contentTypes.reduce((code, contentType, index) => {
const isLast = index === contentTypes.length - 1;
return `${code}\n
${wrapComment(contentType.name)}
${contentType.codename}: {
name: '${toSafePropertyValue(contentType.name)}',
codename: '${contentType.codename}',
id: '${contentType.id}',
externalId: ${getStringOrUndefinedAsPropertyValue(contentType.externalId)},
elements: {${getContentTypeElements(contentType.elements)}}
}${!isLast ? ",\n" : ""}`;
}, "");
};
const getContentTypeElements = (elements: readonly Readonly<ContentTypeElements.ContentTypeElementModel>[]): string => {
if (!config.environmentEntities.snippets) {
throw new Error("Snippets are expected to be present in the environment entities.");
}
if (!config.environmentEntities.taxonomies) {
throw new Error("Taxonomies are expected to be present in the environment entities.");
}
if (!config.environmentEntities.contentTypes) {
throw new Error("Content types are expected to be present in the environment entities.");
}
const flattenedElements = getFlattenedElements({
elements: elements,
snippets: config.environmentEntities.snippets,
taxonomies: config.environmentEntities.taxonomies,
types: config.environmentEntities.contentTypes,
});
return flattenedElements.reduce((code, element, index) => {
const isLast = index === flattenedElements.length - 1;
const elementOptions = getElementOptionsCode(element);
return `${code}
${wrapComment(element.title, {
lines: [
{
name: "Guidelines",
value: element.guidelines ? toGuidelinesComment(element.guidelines) : undefined,
},
],
})}
${element.codename}: {
name: '${toSafePropertyValue(element.title)}',
codename: '${element.codename}',
id: '${element.id}',
externalId: ${getStringOrUndefinedAsPropertyValue(element.externalId)},
required: ${element.isRequired},
type: '${element.type}'
${elementOptions ? `, options: ${elementOptions}` : ""}
}${!isLast ? ",\n" : ""}`;
}, "");
};
const getElementOptionsCode = (flattenedElement: FlattenedElement): string | undefined => {
return match(flattenedElement.originalElement)
.returnType<string | undefined>()
.with({ type: "multiple_choice" }, (element) => {
return `${element.options.reduce<string>((code, option, index) => {
const isLast = index === element.options.length - 1;
return `${code}\n
${wrapComment(option.name)}
${option.codename ? option.codename : resolvePropertyName(option.name)}: {
name: '${toSafePropertyValue(option.name)}',
id: '${option.id}',
codename: ${getStringOrUndefinedAsPropertyValue(option.codename)},
externalId: ${getStringOrUndefinedAsPropertyValue(option.external_id)}
}${!isLast ? ",\n" : ""}`;
}, "{")}}`;
})
.otherwise(() => undefined);
};
const getTaxonomies = (taxonomies: readonly Readonly<TaxonomyModels.Taxonomy>[]): string => {
return taxonomies.reduce((code, taxonomy, index) => {
const isLast = index === taxonomies.length - 1;
return `${code}\n
${wrapComment(taxonomy.name)}
${taxonomy.codename}: {
name: '${toSafePropertyValue(taxonomy.name)}',
codename: '${taxonomy.codename}',
externalId: ${getStringOrUndefinedAsPropertyValue(taxonomy.externalId)},
id: '${taxonomy.id}',
${getTaxonomyTerms(taxonomy.terms)}
}${!isLast ? ",\n" : ""}`;
}, "");
};
const getCollections = (collections: readonly Readonly<CollectionModels.Collection>[]): string => {
return collections.reduce((code, collection, index) => {
const isLast = index === collections.length - 1;
return `${code}\n
${wrapComment(collection.name)}
${collection.codename}: {
name: '${toSafePropertyValue(collection.name)}',
codename: '${collection.codename}',
id: '${collection.id}'
}${!isLast ? ",\n" : ""}`;
}, "");
};
const getRoles = (roles: readonly Readonly<RoleModels.Role>[]): string => {
return roles.reduce((code, role, index) => {
const isLast = index === roles.length - 1;
return `${code}\n
${wrapComment(role.name)}
${resolvePropertyName(role.codename ?? role.name)}: {
name: '${toSafePropertyValue(role.name)}',
codename: ${getStringOrUndefinedAsPropertyValue(role.codename)},
id: '${role.id}'
}${!isLast ? ",\n" : ""}`;
}, "");
};
const getWebhooks = (webhooks: readonly Readonly<WebhookModels.Webhook>[]): string => {
return webhooks.reduce((code, webhook, index) => {
const isLast = index === webhooks.length - 1;
return `${code}\n
${wrapComment(webhook.name)}
${resolvePropertyName(webhook.name)}: {
name: '${toSafePropertyValue(webhook.name)}',
url: '${webhook.url}',
id: '${webhook.id}'
}${!isLast ? ",\n" : ""}`;
}, "");
};
const getTaxonomyTerms = (terms: readonly Readonly<TaxonomyModels.Taxonomy>[]): string => {
return `${terms.reduce<string>((code, term, index) => {
const isLast = index === terms.length - 1;
return `${code}\n
${wrapComment(term.name)}
${term.codename}: {
name: '${toSafePropertyValue(term.name)}',
codename: '${term.codename}',
id: '${term.id}',
externalId: ${getStringOrUndefinedAsPropertyValue(term.externalId)},
${getTaxonomyTerms(term.terms)}
}${!isLast ? ",\n" : ""}`;
}, "terms: {")}}`;
};
const getWorkflowSteps = (workflow: Readonly<WorkflowModels.Workflow>): string => {
// The order of these steps should reflect the order in which they appear in the Kontent UI
const steps: readonly WorkflowStep[] = [...workflow.steps, workflow.scheduledStep, workflow.publishedStep, workflow.archivedStep];
return `{${steps.reduce((code, step) => {
return `${code}
${step.codename}: {
name: '${toSafePropertyValue(step.name)}',
codename: '${step.codename}',
id: '${step.id}'
},`;
}, "")}}`;
};
const wrapInConst = ({ constName, text }: { readonly constName: string; readonly text: string }): string => {
return `export const ${constName} = {
${text}
} as const;`;
};
return {
generateEnvironmentModels,
};
}