Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
65 changes: 62 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@

# Kontent.ai Model Generator

The Kontent.ai Model Generator is a developer tool that streamlines working with Kontent.ai by generating strongly typed objects and TypeScript models. It supports the generation of four distinct model types, each tailored to specific use cases:
The Kontent.ai Model Generator is a developer tool that streamlines working with Kontent.ai by generating strongly typed objects and TypeScript models. It supports the generation of five distinct model types, each tailored to specific use cases:

| Model type | Description | Compatibility |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| [delivery-sdk](#delivery-sdk-models) | Generates TypeScript models for the [JS Delivery SDK](https://www.npmjs.com/package/@kontent-ai/delivery-sdk). These models include content types, taxonomies, and codename-based types representing elements such as workflow steps, languages, and more. | `@kontent-ai/delivery-sdk` version `16.0.0` or higher |
| [migration-toolkit](#migration-toolkit-models) | Creates TypeScript models for the [Migration Toolkit](https://www.npmjs.com/package/@kontent-ai/migration-toolkit). These models help simplify and standardize the process of writing migration scripts. | `@kontent-ai/migration-toolkit` version `2.6.0` or higher |
| [sync-sdk](#sync-sdk-models) | Generates TypeScript models for the [Sync SDK](https://www.npmjs.com/package/@kontent-ai/sync-sdk). These models provide type-safe access to environment metadata including languages, content types, workflows, collections, and taxonomies. | `@kontent-ai/sync-sdk` version `1.0.0` or higher |
| [environment](#environment-models) | Generates JavaScript objects (not TypeScript types) representing the entire structure of your environment — including content types, workflows, languages, and taxonomies. These objects provide comprehensive access to environment metadata. | Can be used in any project. No external dependencies are required. |
| [items](#item-models) | Produces TypeScript types for all item codenames, along with objects containing the id and codename of each item. This is particularly useful when referencing a set of items in your code, enabling type-safe access instead of relying on hardcoded strings. | Can be used in any project. No external dependencies are required. |

Expand Down Expand Up @@ -153,6 +154,63 @@ Configuration
| `formatOptions` | Prettier configuration for formatting generated code |
| `baseUrl` | Can be used to override default Kontent.ai URLs |

## Sync SDK models

> [!TIP]
> Recommended: Using these models is highly encouraged when working with the Sync SDK, as they provide robust type
> safety and streamline development.

Basic usage

```bash
npx @kontent-ai/model-generator@latest sync-sdk
--environmentId=<id>
--managementApiKey=<key>
```

Usage with options

```bash
npx @kontent-ai/model-generator@latest sync-sdk
--environmentId=<id>
--managementApiKey=<key>
--outputDir=<path>
--moduleFileExtension=<js | ts | none | mts | mjs>
--addTimestamp=<true, false>
--managementBaseUrl=<proxyUrl>
```

```typescript
import { generateSyncModelsAsync } from '@kontent-ai/model-generator';

await generateSyncModelsAsync({
// required
environmentId: 'x',
managementApiKey: 'y',
moduleFileExtension: 'js',
addTimestamp: false,
createFiles: true,
outputDir: '/', // only required when createFiles is true

// optional
baseUrl: undefined,
formatOptions: { indentSize: 4, quote: 'single' }
});
```

Configuration

| Option | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `environmentId` | Id of Kontent.ai environment |
| `managementApiKey` | Management API key |
| `moduleFileExtension` | Extension used for imports in generated models. |
| `addTimestamp` | Indicates if models contain timestamp |
| `createFiles` | If enabled, files will be created on FileSystem. When disabled you may iterate over the result and process the files yourself. |
| `outputDir` | Output directory path for files. Only available when `createFiles` is set to `true` |
| `formatOptions` | Prettier configuration for formatting generated code |
| `baseUrl` | Can be used to override default Kontent.ai URLs |

## Environment models

> [!WARNING]
Expand Down Expand Up @@ -312,8 +370,9 @@ To see how models are generated have a look at following sample generated models

1. `delivery-sdk` -> <https://github.com/kontent-ai/model-generator-js/tree/master/sample/delivery>
2. `migration-toolkit` -> <https://github.com/kontent-ai/model-generator-js/tree/master/sample/migration>
3. `environment` -> <https://github.com/kontent-ai/model-generator-js/tree/master/sample/environment>
4. `items` -> <https://github.com/kontent-ai/model-generator-js/tree/master/sample/items>
3. `sync-sdk` -> <https://github.com/kontent-ai/model-generator-js/tree/master/sample/sync>
4. `environment` -> <https://github.com/kontent-ai/model-generator-js/tree/master/sample/environment>
5. `items` -> <https://github.com/kontent-ai/model-generator-js/tree/master/sample/items>

## Contribution & Feedback

Expand Down
18 changes: 18 additions & 0 deletions lib/cli/actions/sync-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { generateSyncModelsAsync } from "../../generators/sync/sync-func.js";
import { parseModuleFileExtension } from "../arg.utils.js";
import type { CliArgumentsFetcher } from "../cli.models.js";
import { commandOptions } from "../command.options.js";

export async function syncActionAsync(cliFetcher: CliArgumentsFetcher): Promise<void> {
await generateSyncModelsAsync({
// required
createFiles: true,
environmentId: cliFetcher.getRequiredArgumentValue(commandOptions.environmentId.name),
managementApiKey: cliFetcher.getRequiredArgumentValue(commandOptions.managementApiKey.name),
// optional
managementBaseUrl: cliFetcher.getOptionalArgumentValue(commandOptions.managementBaseUrl.name),
outputDir: cliFetcher.getOptionalArgumentValue(commandOptions.outputDir.name),
addTimestamp: cliFetcher.getBooleanArgumentValue(commandOptions.addTimestamp.name, false),
moduleFileExtension: parseModuleFileExtension(cliFetcher.getOptionalArgumentValue(commandOptions.moduleFileExtension.name)),
});
}
2 changes: 2 additions & 0 deletions lib/cli/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { deliveryActionAsync } from "./actions/delivery-action.js";
import { environmentActionAsync } from "./actions/environment-action.js";
import { itemsActionAsync } from "./actions/items-action.js";
import { migrateActionAsync } from "./actions/migrate-action.js";
import { syncActionAsync } from "./actions/sync-action.js";
import { argumentsFetcherAsync } from "./args/args-fetcher.js";
import { cliArgs } from "./commands.js";

Expand All @@ -21,6 +22,7 @@ try {
.with("migration-toolkit", async () => await migrateActionAsync(argsFetcher))
.with("environment", async () => await environmentActionAsync(argsFetcher))
.with("items", async () => await itemsActionAsync(argsFetcher))
.with("sync-sdk", async () => await syncActionAsync(argsFetcher))
.otherwise((action) => {
throw new Error(`Invalid action '${chalk.red(action)}'`);
});
Expand Down
15 changes: 15 additions & 0 deletions lib/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ export const cliArgs = argumentsSetter()
commandOptions.managementBaseUrl,
],
})
.withCommand({
name: "sync-sdk",
description: `Generates models for '${"@kontent-ai/sync-sdk" satisfies LibraryType}' library`,
examples: [
`kontent-generate ${"sync-sdk" satisfies CliAction} --${commandOptions.environmentId.name}=x --${commandOptions.managementApiKey.name}=x`,
],
options: [
commandOptions.environmentId,
commandOptions.managementApiKey,
commandOptions.addTimestamp,
commandOptions.moduleFileExtension,
commandOptions.outputDir,
commandOptions.managementBaseUrl,
],
})
.withCommand({
name: "items",
description: "Overview of all items in the environment and their ids/codenames as well as Type representing all item codenames.",
Expand Down
17 changes: 14 additions & 3 deletions lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DeliveryApiMode, ModuleFileExtension } from "./core/core.models.js";
import type { DeliveryApiMode, LibraryType, ModuleFileExtension } from "./core/core.models.js";
import { sdkInfo } from "./sdk-info.js";

export const defaultModuleFileExtension: ModuleFileExtension = "js";
Expand All @@ -10,8 +10,19 @@ export const coreConfig = {
kontentTrackingHeaderValue: `${sdkInfo.name};${sdkInfo.version}`,
} as const;

export const syncConfig = {
npmPackageName: "@kontent-ai/sync-sdk" satisfies LibraryType,
coreTypesFilename: "sync.models",
coreClientTypesTypeName: "CoreSyncClientTypes",
coreClientTypeName: "CoreSyncClient",
sdkTypes: {
syncClientTypes: "SyncClientTypes",
syncClient: "SyncClient",
},
} as const;

export const migrationConfig = {
npmPackageName: "@kontent-ai/migration-toolkit",
npmPackageName: "@kontent-ai/migration-toolkit" satisfies LibraryType,
migrationItemsFolderName: "contentTypes",
environmentFolderName: "environment",
migrationTypesFilename: "migration",
Expand Down Expand Up @@ -40,7 +51,7 @@ export const migrationConfig = {
} as const;

export const deliveryConfig = {
npmPackageName: "@kontent-ai/delivery-sdk",
npmPackageName: "@kontent-ai/delivery-sdk" satisfies LibraryType,
systemTypesFolderName: "system",
mainSystemFilename: "main.system",
coreContentTypeName: "CoreType",
Expand Down
4 changes: 2 additions & 2 deletions lib/core/core.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import type {
TaxonomyModels,
} from "@kontent-ai/management-sdk";

export type CliAction = "delivery-sdk" | "migration-toolkit" | "environment" | "items";
export type LibraryType = "@kontent-ai/migration-toolkit" | "@kontent-ai/delivery-sdk";
export type CliAction = "delivery-sdk" | "migration-toolkit" | "environment" | "items" | "sync-sdk";
export type LibraryType = "@kontent-ai/migration-toolkit" | "@kontent-ai/delivery-sdk" | "@kontent-ai/sync-sdk";

export const moduleFileExtensions = ["js", "ts", "mjs", "mts", "none"] as const;

Expand Down
5 changes: 3 additions & 2 deletions lib/core/importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function getImporter(moduleFileExtension: ModuleFileExtension) {
const importExtension = moduleFileExtension === "none" ? "" : `.${moduleFileExtension}`;

return {
importType: (data: { readonly filePathOrPackage: LiteralUnion<LibraryType>; readonly importValue: string }): string => {
importType: (data: { readonly filePathOrPackage: LiteralUnion<LibraryType>; readonly importValue: string | string[] }): string => {
if (!data.importValue.length) {
return "";
}
Expand All @@ -15,8 +15,9 @@ export function getImporter(moduleFileExtension: ModuleFileExtension) {
const resolvedFilePath = isExternalLib
? data.filePathOrPackage
: `${getFileNameWithoutExtension(data.filePathOrPackage)}${importExtension}`;
const importValues = Array.isArray(data.importValue) ? data.importValue : [data.importValue];

return `import type { ${data.importValue} } from '${resolvedFilePath}';`;
return `import type { ${importValues.join(", ")} } from '${resolvedFilePath}';`;
},
getBarrelExportCode(filenames: readonly string[]): string {
if (!filenames.length) {
Expand Down
79 changes: 79 additions & 0 deletions lib/generators/sync/sync-func.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { EnvironmentModels } from "@kontent-ai/management-sdk";
import chalk from "chalk";
import type { Options } from "prettier";
import type { CliAction, CreateFilesConfig, GeneratedFile, GeneratedSet, ModuleFileExtension } from "../../core/core.models.js";
import { getManagementKontentFetcher } from "../../fetch/management-kontent-fetcher.js";
import { getFileManager } from "../../files/file-manager.js";
import { getSyncGenerator } from "./sync.generator.js";

export type GenerateSyncModelsConfig = {
readonly environmentId: string;
readonly addTimestamp: boolean;
readonly managementApiKey: string;
readonly moduleFileExtension: ModuleFileExtension;

readonly managementBaseUrl?: string;
readonly formatOptions?: Readonly<Options>;
} & CreateFilesConfig;

export async function generateSyncModelsAsync(config: GenerateSyncModelsConfig): Promise<readonly GeneratedFile[]> {
console.log(chalk.green("Model generator started \n"));
console.log(`Generating '${chalk.yellow("sync-sdk" satisfies CliAction)}' models\n`);

const { syncFiles, environmentInfo } = await getFilesAsync(config);

const fileManager = getFileManager({
...config,
environmentInfo,
});

const setFiles = await fileManager.getSetFilesAsync([syncFiles]);

if (config.createFiles) {
fileManager.createFiles(setFiles);
}

console.log(chalk.green("\nCompleted"));

return setFiles;
}

async function getFilesAsync(config: GenerateSyncModelsConfig): Promise<{
readonly syncFiles: GeneratedSet;
readonly environmentInfo: Readonly<EnvironmentModels.EnvironmentInformationModel>;
}> {
const kontentFetcher = getManagementKontentFetcher({
environmentId: config.environmentId,
managementApiKey: config.managementApiKey,
baseUrl: config.managementBaseUrl,
});

const environmentInfo = await kontentFetcher.getEnvironmentInfoAsync();

const [languages, taxonomies, types, snippets, collections, workflows] = await Promise.all([
kontentFetcher.getLanguagesAsync(),
kontentFetcher.getTaxonomiesAsync(),
kontentFetcher.getTypesAsync(),
kontentFetcher.getSnippetsAsync(),
kontentFetcher.getCollectionsAsync(),
kontentFetcher.getWorkflowsAsync(),
]);

const syncGenerator = getSyncGenerator({
moduleFileExtension: config.moduleFileExtension,
environmentData: {
environment: environmentInfo,
taxonomies: taxonomies,
languages: languages,
workflows: workflows,
types: types,
snippets: snippets,
collections: collections,
},
});

return {
syncFiles: syncGenerator.getSyncFiles(),
environmentInfo,
};
}
71 changes: 71 additions & 0 deletions lib/generators/sync/sync.generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type {
CollectionModels,
ContentTypeModels,
ContentTypeSnippetModels,
EnvironmentModels,
LanguageModels,
TaxonomyModels,
WorkflowModels,
} from "@kontent-ai/management-sdk";
import { syncConfig } from "../../config.js";
import { wrapComment } from "../../core/comment.utils.js";
import type { GeneratedSet, ModuleFileExtension, ObjectWithCodename } from "../../core/core.models.js";
import { sortAlphabetically, uniqueFilter } from "../../core/core.utils.js";
import { getImporter } from "../../core/importer.js";

export interface SyncGeneratorConfig {
readonly moduleFileExtension: ModuleFileExtension;

readonly environmentData: {
readonly environment: Readonly<EnvironmentModels.EnvironmentInformationModel>;
readonly types: readonly Readonly<ContentTypeModels.ContentType>[];
readonly workflows: readonly Readonly<WorkflowModels.Workflow>[];
readonly languages: readonly Readonly<LanguageModels.LanguageModel>[];
readonly collections: readonly Readonly<CollectionModels.Collection>[];
readonly snippets: readonly Readonly<ContentTypeSnippetModels.ContentTypeSnippet>[];
readonly taxonomies: readonly Readonly<TaxonomyModels.Taxonomy>[];
};
}

export function getSyncGenerator(config: SyncGeneratorConfig) {
const importer = getImporter(config.moduleFileExtension);

return {
getSyncFiles(): GeneratedSet {
return {
folderName: undefined,
files: [
{
filename: `${syncConfig.coreTypesFilename}.ts`,
text: `
${importer.importType({
filePathOrPackage: syncConfig.npmPackageName,
importValue: [syncConfig.sdkTypes.syncClientTypes, syncConfig.sdkTypes.syncClient],
})}

${wrapComment("Use as generic type when creating a sync client for increased type safety")}
export type ${syncConfig.coreClientTypesTypeName} = ${syncConfig.sdkTypes.syncClientTypes} & {
readonly languageCodenames: ${getCodenames(config.environmentData.languages)},
readonly typeCodenames: ${getCodenames(config.environmentData.types)},
readonly workflowCodenames: ${getCodenames(config.environmentData.workflows)},
readonly workflowStepCodenames: ${getCodenames(config.environmentData.workflows.flatMap((workflow) => workflow.steps))},
readonly collectionCodenames: ${getCodenames(config.environmentData.collections)},
readonly taxonomyCodenames: ${getCodenames(config.environmentData.taxonomies)},
};

${wrapComment("Type safe sync client")}
export type ${syncConfig.coreClientTypeName} = SyncClient<${syncConfig.coreClientTypesTypeName}>;
`,
},
],
};
},
};
}

function getCodenames(items: readonly ObjectWithCodename[]): string {
if (!items.length) {
return "never";
}
return sortAlphabetically(items.map((item) => `'${item.codename}'`).filter(uniqueFilter), (m) => m).join(" | ");
}
Loading