import { CliCommand, type ParentCliCommandDefinition } from '@causa/cli'; import { WorkspaceFunction } from '@causa/workspace'; import { AllowMissing } from '@causa/workspace/validation'; import { Allow, IsArray, IsObject, IsString } from 'class-validator'; import type { LoadSchemasResult, ObjectSchemaWithoutDatabases, Schema, SchemaDatabase, } from './model.types.js'; /** * A function reading the UTF-8 contents of a file at the given absolute path. * * Used by schema parsers to fetch the raw text of every file the loader touches — both files discovered via the input * paths and files pulled in transitively via `$ref` resolution. */ export type SchemaFileReader = (path: string) => Promise; /** * The `model` parent command, grouping all commands related to business modelling, e.g. generating code from schemas. */ export const modelCommandDefinition: ParentCliCommandDefinition = { name: 'model', description: 'Manages business modelling, and code generation from it.', }; /** * Describes a schema that has been generated by a code generator. */ export type GeneratedSchema = { /** * The name of the generated class, function, or type. */ name: string; /** * The path to the file where the generated code is located. */ file: string; }; /** * A collection of generated schemas, where keys are URIs to the input schema files (possibly including a fragment * identifier). */ export type GeneratedSchemas = Record; /** * The output of {@link ModelGenerateCode}, which contains the {@link GeneratedSchemas} for each code generator. * Keys are code generator names. */ export type GeneratorsOutput = Record; /** * Runs all the configured model code generators. * Returns the list of files generated by the code generators. */ @CliCommand({ parent: modelCommandDefinition, name: 'generateCode', description: `Runs all the configured model code generators.`, summary: `Runs all the configured model code generators.`, aliases: ['genCode'], outputFn: (output) => { const files = Object.values(output) .flatMap((schemas) => Object.values(schemas)) .map(({ file }) => file); console.log([...new Set(files)].join('\n')); }, }) export abstract class ModelGenerateCode extends WorkspaceFunction< Promise > {} /** * Runs the given model code generator, and returns the list of files generated by it. * This should be implemented by each code generator for its corresponding {@link ModelRunCodeGenerator.generator} name. */ export abstract class ModelRunCodeGenerator extends WorkspaceFunction< Promise > { /** * The name of the code generator to run. */ @IsString() readonly generator!: string; /** * The configuration for the code generator. */ @IsObject() readonly configuration!: Record; /** * The output of all previous generators that have run. */ @IsObject() readonly previousGeneratorsOutput!: GeneratorsOutput; } /** * Inputs for a code generator, parsed from its configuration. */ export type CodeGeneratorInputs = { /** * Whether to include schema files for events referenced in the project. * This is only provided for information. If this is `true`, {@link CodeGeneratorInputs.files} will contain the schema * files for the events. */ includeEvents: boolean; /** * The globs to use to find schema files. * This is only provided for information. If this is set, {@link CodeGeneratorInputs.files} will contain the schema * files matching the globs. */ globs: string[]; /** * The files to use as input for the code generator. */ files: string[]; /** * A list of properties that may exist in JSONSchema files and contain nested schemas. * This should be passed to the code generator configuration. */ nestedSchemas?: string[]; /** * If `true`, JSONSchema files that are referenced by other schemas should be included in the input data, along with * their nested schemas. * This should be passed to the code generator configuration. */ includeFullReferences?: boolean; }; /** * Parses the inputs for a code generator, based on its configuration. * This expects standard properties in the configuration (all are optional): * - `includeEvents`: boolean. * - `globs`: string[]. * - `nestedSchemas`: string[]. * - `includeReferences`: boolean. */ export abstract class ModelParseCodeGeneratorInputs extends WorkspaceFunction< Promise > { /** * The configuration for the code generator. */ @IsObject() readonly configuration!: Record; } /** * Loads and parses one or more schema files into the format-neutral schema model, transitively following references to * pull in any files reachable from the input set. * * Implementations are selected based on the workspace's `model.schema` configuration. */ export abstract class ModelSchemaParse extends WorkspaceFunction< Promise > { /** * Absolute paths of the initial schema files to load. */ @IsArray() @IsString({ each: true }) readonly paths!: string[]; /** * Optional reader used to fetch the raw text of every file the loader touches. When omitted, implementations default * to reading from the filesystem. */ @Allow() @AllowMissing() readonly fileReader?: SchemaFileReader; } /** * Extracts a {@link SchemaDatabase} binding from an object schema's causa extensions. * * No implementation is provided in this package: engine modules each register their own implementation, whose * `_supports` decides whether the schema is persisted in that engine (e.g. by looking up an engine-specific causa * extension). */ export abstract class ModelSchemaExtractDatabase extends WorkspaceFunction { /** * The object schema to derive a database binding from, before its own `databases` field is computed. */ @IsObject() readonly schema!: ObjectSchemaWithoutDatabases; } /** * The action to perform when writing schema-formatted contents through {@link ModelSchemaWrite}. */ export type SchemaWriteAction = | { /** * Apply (create or update) a single schema in the file. */ type: 'apply'; /** * The schema whose body should be written. The target location is identified by {@link Schema.path}. */ schema: Schema; } | { /** * Delete a schema from the file. */ type: 'delete'; /** * Absolute path of the schema to delete. */ path: string; } | { /** * Rename a nested schema within a file. Updates the parent key, sets the schema's title to the new leaf name, * and rewrites every in-file ref containing the old fragment. */ type: 'rename'; /** * The old JSON Pointer fragment of the schema, e.g. `#/$defs/Foo`. */ oldFragment: string; /** * The new JSON Pointer fragment of the schema, e.g. `#/$defs/Bar`. */ newFragment: string; }; /** * Writes schema-formatted file contents, applying one of: creating/updating a schema, deleting a schema, or renaming a * nested schema within the file. * * Implementations are selected based on the workspace's `model.schema` configuration. They are pure transformers: * callers are responsible for reading the source file and persisting the returned contents. */ export abstract class ModelSchemaWrite extends WorkspaceFunction< Promise > { /** * The current text of the file the schema lives in. May be empty when creating a new file. */ @IsString() readonly contents!: string; /** * The write action to perform. */ @IsObject() readonly action!: SchemaWriteAction; }