From 620729e8d18227b1ad27b6082f0953f1f5303b2d Mon Sep 17 00:00:00 2001 From: Hasko Date: Mon, 29 Jun 2026 11:49:23 +0200 Subject: [PATCH 1/6] =?UTF-8?q?feat(kernel):=20=E2=9C=A8=20Improve=20envir?= =?UTF-8?q?onment=20schema=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/installation.md | 24 +++++++ docs/reference/kernel.md | 36 +++++++++- package.json | 2 +- src/Kernel.ts | 32 +++++++-- src/kernel/KernelEnvironmentVariable.ts | 18 +++-- .../KernelEnvironmentVariableDefinition.ts | 7 ++ tests/kernel.test.mjs | 69 +++++++++++++++++++ tests/typescript-module-resolution.test.mjs | 13 ++++ 8 files changed, 190 insertions(+), 11 deletions(-) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index ae9ac24..8c926ea 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -11,26 +11,46 @@ injection and in-memory adapters. Install extra packages only for the adapters your application imports. +The core package does not load optional adapter dependencies from the root +import. Install the peer packages listed below only when importing the matching +subpath. + ## Pub/Sub Adapters The in-memory pub/sub adapter has no extra runtime dependencies. +```ts +import { InMemoryPubSub } from '@haskou/ddd-kernel/adapters/pubsub/in-memory'; +``` + The AMQP adapter uses `amqplib`: ```bash yarn add amqplib ``` +```ts +import { AmqpMessageBusAdapter } from '@haskou/ddd-kernel/adapters/pubsub/amqp'; +``` + ## DB Adapters The in-memory repository adapter has no extra runtime dependencies. +```ts +import { InMemoryRepository } from '@haskou/ddd-kernel/adapters/db/in-memory'; +``` + The MongoDB repository adapter uses `mongodb`: ```bash yarn add mongodb ``` +```ts +import { MongoRepository } from '@haskou/ddd-kernel/adapters/db/mongo'; +``` + ## UI Adapters The Express adapter uses `express`, `routing-controllers` and decorator @@ -40,6 +60,10 @@ metadata packages: yarn add express routing-controllers reflect-metadata class-transformer class-validator ``` +```ts +import { ExpressKernelServer } from '@haskou/ddd-kernel/adapters/ui/express'; +``` + Install `cors` only when enabling `routingControllersOptions.cors`: ```bash diff --git a/docs/reference/kernel.md b/docs/reference/kernel.md index bab3f4e..9748fda 100644 --- a/docs/reference/kernel.md +++ b/docs/reference/kernel.md @@ -84,9 +84,14 @@ creating the kernel: ```ts const environmentSchema = { - ENABLE_JOBS: { defaultValue: false, type: 'boolean' }, + ENABLE_JOBS: { + defaultValue: false, + description: 'Enables background schedulers.', + type: 'boolean', + }, HTTP_PORT: { required: true, type: 'number' }, - SERVICE_NAME: { type: 'string' }, + NODE_ENV: { choices: ['local', 'test', 'production'], type: 'string' }, + SERVICE_NAME: { sensitive: false, type: 'string' }, } as const; const kernel = new Kernel({ environmentSchema }); @@ -95,12 +100,39 @@ kernel.loadEnvironmentVariables(); kernel.environment.HTTP_PORT; // number kernel.environment.ENABLE_JOBS; // boolean +kernel.environment.NODE_ENV; // 'local' | 'test' | 'production' | undefined kernel.environment.SERVICE_NAME; // string | undefined ``` Required variables throw `KernelEnvironmentValidationError` when they are missing. `number` and `boolean` values are parsed after `.env` files are loaded. Boolean values accept `true`, `false`, `1`, `0`, `yes`, `no`, `on` and `off`. +Blank numeric values are rejected instead of being coerced to `0`. + +`choices` restricts the allowed runtime values and narrows the TypeScript type +when the schema is declared `as const`: + +```ts +const environmentSchema = { + NODE_ENV: { choices: ['local', 'test'], type: 'string' }, +} as const; + +const kernel = new Kernel({ environmentSchema }); + +kernel.environment.NODE_ENV; // 'local' | 'test' | undefined +``` + +Schema entries also accept metadata for generated documentation and operational +tools: + +| Field | Purpose | +| -------------- | ----------------------------------------------------------------------- | +| `type` | Runtime parser and TypeScript primitive: `string`, `number`, `boolean`. | +| `required` | Throws when the variable is missing. | +| `defaultValue` | Used when the variable is absent. | +| `choices` | Restricts accepted values and narrows the inferred TypeScript type. | +| `description` | Human-readable explanation for generated docs or audits. | +| `sensitive` | Marks values that should not be logged or displayed by tooling. | ## Dependency Injection diff --git a/package.json b/package.json index 2889f04..a22d622 100644 --- a/package.json +++ b/package.json @@ -244,7 +244,7 @@ "prepack": "yarn build", "test": "yarn build && c8 node --test \"tests/**/*.test.mjs\"", "build:coverage": "rm -rf dist && tsc -p tsconfig.coverage.json", - "test:coverage": "yarn build:coverage && c8 --all --src src --include \"src/**/*.ts\" --exclude \"src/**/index.ts\" --exclude \"src/contracts/**/*.ts\" --exclude \"src/**/*.d.ts\" --exclude \"src/**/*Options.ts\" --exclude \"src/**/*Context.ts\" --exclude \"src/**/*Handler.ts\" --exclude \"src/**/*Message.ts\" --exclude \"src/**/*Metadata.ts\" --exclude \"src/**/*Registration.ts\" --exclude \"src/**/*Resolver.ts\" --exclude \"src/**/*Authenticator.ts\" --exclude \"src/**/*Consumer.ts\" --exclude \"src/**/*Publisher.ts\" --exclude \"src/**/*Class.ts\" --exclude \"src/**/*Definition.ts\" --exclude \"src/**/*Alias.ts\" --exclude \"src/**/*Internals.ts\" --exclude \"src/**/*Expression.ts\" --exclude \"src/**/*Constructor.ts\" --exclude \"src/**/*Attributes.ts\" --exclude \"src/infrastructure/lifecycle/**/*.ts\" --exclude \"src/kernel/**/*.ts\" --extension .ts --exclude-after-remap --reporter text --reporter lcov node --test \"tests/**/*.test.mjs\"", + "test:coverage": "yarn build:coverage && c8 --all --src src --include \"src/**/*.ts\" --exclude \"src/**/index.ts\" --exclude \"src/contracts/**/*.ts\" --exclude \"src/**/*.d.ts\" --exclude \"src/**/*Options.ts\" --exclude \"src/**/*Context.ts\" --exclude \"src/**/*Handler.ts\" --exclude \"src/**/*Message.ts\" --exclude \"src/**/*Metadata.ts\" --exclude \"src/**/*Registration.ts\" --exclude \"src/**/*Resolver.ts\" --exclude \"src/**/*Authenticator.ts\" --exclude \"src/**/*Consumer.ts\" --exclude \"src/**/*Publisher.ts\" --exclude \"src/**/*Class.ts\" --exclude \"src/**/*Definition.ts\" --exclude \"src/**/*Alias.ts\" --exclude \"src/**/*Internals.ts\" --exclude \"src/**/*Expression.ts\" --exclude \"src/**/*Constructor.ts\" --exclude \"src/**/*Attributes.ts\" --exclude \"src/infrastructure/lifecycle/**/*.ts\" --exclude \"src/kernel/KernelDefaultEnvironment.ts\" --exclude \"src/kernel/KernelEnvironment.ts\" --exclude \"src/kernel/KernelEnvironmentForSchema.ts\" --exclude \"src/kernel/KernelEnvironmentSchema.ts\" --exclude \"src/kernel/KernelEnvironmentValue.ts\" --exclude \"src/kernel/KernelEnvironmentVariable.ts\" --exclude \"src/kernel/KernelEnvironmentVariablePrimitive.ts\" --exclude \"src/kernel/KernelEnvironmentVariableType.ts\" --exclude \"src/kernel/ShutdownCandidate.ts\" --extension .ts --exclude-after-remap --reporter text --reporter lcov node --test \"tests/**/*.test.mjs\"", "typecheck": "tsc -p tsconfig.json --noEmit" }, "keywords": [ diff --git a/src/Kernel.ts b/src/Kernel.ts index 25be5d0..b528973 100644 --- a/src/Kernel.ts +++ b/src/Kernel.ts @@ -1,8 +1,8 @@ import dotenv, { type DotenvConfigOutput } from 'dotenv'; import path from 'node:path'; -import type { Consumer } from './adapters/pubsub/index.js'; -import type { Route } from './adapters/ui/routes/index.js'; +import type { Consumer } from './adapters/pubsub/Consumer.js'; +import type { Route } from './adapters/ui/routes/Route.js'; import type { ConsumerMiddleware, KernelLogger, @@ -135,6 +135,20 @@ export class Kernel< } } + private static assertEnvironmentVariableChoice( + name: string, + value: KernelEnvironmentValue, + schema: KernelEnvironmentSchema, + ): void { + const choices = schema[name]?.choices; + + if (choices && !choices.includes(value)) { + throw new KernelEnvironmentValidationError( + `Environment variable "${name}" must be one of: ${choices.join(', ')}.`, + ); + } + } + private static getEnvironmentVariablesPath( environment: string, options: KernelEnvironmentVariablesOptions< @@ -193,13 +207,23 @@ export class Kernel< const definition = schema[name]; if (definition.type === 'boolean') { - return Kernel.parseBooleanEnvironmentVariable(name, value); + const parsedValue = Kernel.parseBooleanEnvironmentVariable(name, value); + + Kernel.assertEnvironmentVariableChoice(name, parsedValue, schema); + + return parsedValue; } if (definition.type === 'number') { - return Kernel.parseNumberEnvironmentVariable(name, value); + const parsedValue = Kernel.parseNumberEnvironmentVariable(name, value); + + Kernel.assertEnvironmentVariableChoice(name, parsedValue, schema); + + return parsedValue; } + Kernel.assertEnvironmentVariableChoice(name, value, schema); + return value; } diff --git a/src/kernel/KernelEnvironmentVariable.ts b/src/kernel/KernelEnvironmentVariable.ts index 7d1f6da..b896343 100644 --- a/src/kernel/KernelEnvironmentVariable.ts +++ b/src/kernel/KernelEnvironmentVariable.ts @@ -1,3 +1,4 @@ +import type { KernelEnvironmentValue } from './KernelEnvironmentValue.js'; import type { KernelEnvironmentVariableDefinition } from './KernelEnvironmentVariableDefinition.js'; import type { KernelEnvironmentVariablePrimitive } from './KernelEnvironmentVariablePrimitive.js'; import type { KernelEnvironmentVariableType } from './KernelEnvironmentVariableType.js'; @@ -7,11 +8,20 @@ export type KernelEnvironmentVariable< > = TDefinition extends KernelEnvironmentVariableDefinition< infer TType extends KernelEnvironmentVariableType, - infer TRequired extends boolean + infer TRequired extends boolean, + infer TChoices extends readonly KernelEnvironmentValue[] | undefined > ? TDefinition extends { readonly defaultValue: unknown } - ? KernelEnvironmentVariablePrimitive + ? TChoices extends readonly KernelEnvironmentVariablePrimitive[] + ? TChoices[number] + : KernelEnvironmentVariablePrimitive : TRequired extends true - ? KernelEnvironmentVariablePrimitive - : KernelEnvironmentVariablePrimitive | undefined + ? TChoices extends readonly KernelEnvironmentVariablePrimitive[] + ? TChoices[number] + : KernelEnvironmentVariablePrimitive + : + | (TChoices extends readonly KernelEnvironmentVariablePrimitive[] + ? TChoices[number] + : KernelEnvironmentVariablePrimitive) + | undefined : never; diff --git a/src/kernel/KernelEnvironmentVariableDefinition.ts b/src/kernel/KernelEnvironmentVariableDefinition.ts index 25b0cf4..d6a508c 100644 --- a/src/kernel/KernelEnvironmentVariableDefinition.ts +++ b/src/kernel/KernelEnvironmentVariableDefinition.ts @@ -1,11 +1,18 @@ +import type { KernelEnvironmentValue } from './KernelEnvironmentValue.js'; import type { KernelEnvironmentVariablePrimitive } from './KernelEnvironmentVariablePrimitive.js'; import type { KernelEnvironmentVariableType } from './KernelEnvironmentVariableType.js'; export interface KernelEnvironmentVariableDefinition< TType extends KernelEnvironmentVariableType = KernelEnvironmentVariableType, TRequired extends boolean = boolean, + TChoices extends readonly KernelEnvironmentValue[] | undefined = + | readonly KernelEnvironmentVariablePrimitive[] + | undefined, > { + readonly choices?: TChoices; readonly defaultValue?: KernelEnvironmentVariablePrimitive; + readonly description?: string; readonly required?: TRequired; + readonly sensitive?: boolean; readonly type: TType; } diff --git a/tests/kernel.test.mjs b/tests/kernel.test.mjs index d3c6f15..9f340ca 100644 --- a/tests/kernel.test.mjs +++ b/tests/kernel.test.mjs @@ -731,3 +731,72 @@ test('keeps typed string environment variables as strings', () => { } } }); + +test('validates typed environment variable choices', () => { + const previousNodeEnvironment = process.env.NODE_ENV; + const previousHttpPort = process.env.HTTP_PORT; + const previousEnableJobs = process.env.ENABLE_JOBS; + + process.env.NODE_ENV = 'test'; + process.env.HTTP_PORT = '3000'; + process.env.ENABLE_JOBS = 'false'; + + try { + const kernel = new Kernel({ + environmentSchema: { + ENABLE_JOBS: { choices: [true, false], type: 'boolean' }, + HTTP_PORT: { choices: [3000, 3001], type: 'number' }, + NODE_ENV: { choices: ['local', 'test'], type: 'string' }, + }, + }); + + kernel.loadEnvironmentVariables(); + + assert.equal(kernel.environment.NODE_ENV, 'test'); + assert.equal(kernel.environment.HTTP_PORT, 3000); + assert.equal(kernel.environment.ENABLE_JOBS, false); + } finally { + if (previousNodeEnvironment === undefined) { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = previousNodeEnvironment; + } + + if (previousHttpPort === undefined) { + delete process.env.HTTP_PORT; + } else { + process.env.HTTP_PORT = previousHttpPort; + } + + if (previousEnableJobs === undefined) { + delete process.env.ENABLE_JOBS; + } else { + process.env.ENABLE_JOBS = previousEnableJobs; + } + } +}); + +test('throws when typed environment variable choices do not match', () => { + const previousNodeEnvironment = process.env.NODE_ENV; + + process.env.NODE_ENV = 'production'; + + try { + const kernel = new Kernel({ + environmentSchema: { + NODE_ENV: { choices: ['local', 'test'], type: 'string' }, + }, + }); + + assert.throws( + () => kernel.loadEnvironmentVariables(), + /Environment variable "NODE_ENV" must be one of: local, test/, + ); + } finally { + if (previousNodeEnvironment === undefined) { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = previousNodeEnvironment; + } + } +}); diff --git a/tests/typescript-module-resolution.test.mjs b/tests/typescript-module-resolution.test.mjs index 4891c48..21aaeb5 100644 --- a/tests/typescript-module-resolution.test.mjs +++ b/tests/typescript-module-resolution.test.mjs @@ -39,8 +39,18 @@ test('exports types for TypeScript moduleResolution node consumers', async () => import type { MessageBus, PublisherHook } from '@haskou/ddd-kernel/contracts/pubsub'; import type { DomainMessageBus } from '@haskou/ddd-kernel/domain'; import type { SchedulerErrorPolicy } from '@haskou/ddd-kernel/scheduler'; + import Kernel from '@haskou/ddd-kernel'; import { ExpressKernelServer } from '@haskou/ddd-kernel/adapters/ui/express'; + const environmentSchema = { + ENABLE_JOBS: { choices: [true, false], type: 'boolean' }, + HTTP_PORT: { choices: [3000, 3001], type: 'number' }, + NODE_ENV: { choices: ['local', 'test'], type: 'string' }, + } as const; + const kernel = new Kernel({ environmentSchema }); + const nodeEnvironment: 'local' | 'test' | undefined = kernel.environment.NODE_ENV; + const httpPort: 3000 | 3001 | undefined = kernel.environment.HTTP_PORT; + const enableJobs: true | false | undefined = kernel.environment.ENABLE_JOBS; const middleware: ConsumerMiddleware | undefined = undefined; const messageBus: MessageBus | undefined = undefined; const domainMessageBus: DomainMessageBus | undefined = undefined; @@ -51,6 +61,9 @@ test('exports types for TypeScript moduleResolution node consumers', async () => void messageBus; void domainMessageBus; void hook; + void nodeEnvironment; + void httpPort; + void enableJobs; void policy; void ExpressKernelServer; `, From 48a53cabd9e13f627faa96541796730ceb6fdec7 Mon Sep 17 00:00:00 2001 From: Hasko Date: Mon, 29 Jun 2026 11:59:47 +0200 Subject: [PATCH 2/6] =?UTF-8?q?break(kernel):=20=F0=9F=92=A5=20Register=20?= =?UTF-8?q?consumers=20and=20routes=20through=20contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/package-map.md | 14 ++++----- docs/reference/consumer.md | 4 +++ docs/reference/kernel.md | 20 +++++++++++++ docs/reference/route.md | 3 ++ src/Kernel.ts | 32 +++++++++++---------- src/adapters/pubsub/Consumer.ts | 3 +- src/adapters/ui/routes/Route.ts | 3 +- src/contracts/kernel/KernelConsumer.ts | 5 ++++ src/contracts/kernel/KernelRoute.ts | 1 + src/contracts/kernel/index.ts | 2 ++ tests/typescript-module-resolution.test.mjs | 6 +++- 11 files changed, 68 insertions(+), 25 deletions(-) create mode 100644 src/contracts/kernel/KernelConsumer.ts create mode 100644 src/contracts/kernel/KernelRoute.ts diff --git a/docs/getting-started/package-map.md b/docs/getting-started/package-map.md index d7f23c2..e46898a 100644 --- a/docs/getting-started/package-map.md +++ b/docs/getting-started/package-map.md @@ -14,13 +14,13 @@ Bootstrap code chooses adapters. ## Core -| Area | Import | Purpose | -| ---------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| Kernel runtime | `@haskou/ddd-kernel` | Registers consumers, routes, schedulers, runtimes and shutdown hooks. | -| Environment variables | `@haskou/ddd-kernel` | Loads `.env.` files and exposes typed `kernel.environment` when a schema is configured. | -| Dependency injection | `@haskou/ddd-kernel/dependency-injection` | Wraps `node-dependency-injection` and container YAML generation/loading. | -| Lifecycle | `@haskou/ddd-kernel/lifecycle` | Runtime and initializer contracts. | -| Kernel logger contract | `@haskou/ddd-kernel/contracts/kernel` | `KernelLogger` interface. | +| Area | Import | Purpose | +| --------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| Kernel runtime | `@haskou/ddd-kernel` | Registers consumers, routes, schedulers, runtimes and shutdown hooks. | +| Environment variables | `@haskou/ddd-kernel` | Loads `.env.` files and exposes typed `kernel.environment` when a schema is configured. | +| Dependency injection | `@haskou/ddd-kernel/dependency-injection` | Wraps `node-dependency-injection` and container YAML generation/loading. | +| Lifecycle | `@haskou/ddd-kernel/lifecycle` | Runtime and initializer contracts. | +| Kernel contracts | `@haskou/ddd-kernel/contracts/kernel` | `KernelConsumer`, `KernelRoute`, `KernelLogger`, middleware and shutdown contracts. | `dotenv`, `node-dependency-injection` and `fs-extra` are package dependencies because the core environment and DI implementations use them directly. diff --git a/docs/reference/consumer.md b/docs/reference/consumer.md index b738b5c..367c884 100644 --- a/docs/reference/consumer.md +++ b/docs/reference/consumer.md @@ -18,6 +18,10 @@ Consumers are registered by class: kernel.registerConsumers(RegisterUserWhenCreated); ``` +`Consumer` implements the core `KernelConsumer` contract. The kernel only +requires `queueName` and `init()`, so a custom transport can provide its own +consumer class without extending this adapter base class. + ## Middleware Register consumer middleware when you need idempotency, retries, tracing or diff --git a/docs/reference/kernel.md b/docs/reference/kernel.md index 9748fda..5d3c0ed 100644 --- a/docs/reference/kernel.md +++ b/docs/reference/kernel.md @@ -177,6 +177,26 @@ kernel.registerConsumerInstances(consumer); kernel.registerSchedulerInstances(scheduler); ``` +`registerConsumers` accepts classes that implement the `KernelConsumer` +contract. The provided pub/sub `Consumer` base class already implements it, but +custom consumers can implement the contract directly when they do not need that +adapter base class: + +```ts +import type { KernelConsumer } from '@haskou/ddd-kernel/contracts/kernel'; + +export default class CustomConsumer implements KernelConsumer { + public readonly queueName = 'custom.consumer'; + + public async init() { + // Subscribe to the transport and bind handlers here. + } +} +``` + +`registerRoutes` accepts classes assignable to `KernelRoute`. The provided HTTP +`Route` base class extends that contract. + Runtimes and initializers are passed to their run methods as classes. Runtimes are resolved through DI, executed and automatically added to shutdown hooks. When an object is already built outside the kernel, register the shutdown action diff --git a/docs/reference/route.md b/docs/reference/route.md index e4259da..4f43494 100644 --- a/docs/reference/route.md +++ b/docs/reference/route.md @@ -19,6 +19,9 @@ injection: kernel.registerRoutes(GetUserRoute); ``` +`Route` extends the core `KernelRoute` contract. HTTP adapters can use that +contract without making the kernel depend on a concrete UI adapter. + `Route` still exposes `get()` for compatibility with older code, but constructor injection is the recommended path because it keeps routes easier to test. diff --git a/src/Kernel.ts b/src/Kernel.ts index b528973..9a4481b 100644 --- a/src/Kernel.ts +++ b/src/Kernel.ts @@ -1,11 +1,11 @@ import dotenv, { type DotenvConfigOutput } from 'dotenv'; import path from 'node:path'; -import type { Consumer } from './adapters/pubsub/Consumer.js'; -import type { Route } from './adapters/ui/routes/Route.js'; import type { ConsumerMiddleware, + KernelConsumer, KernelLogger, + KernelRoute, ShutdownHook, } from './contracts/index.js'; import type { ServiceClass } from './infrastructure/dependency-injection/index.js'; @@ -44,9 +44,9 @@ export class Kernel< ); private readonly consumerMiddlewares: ConsumerMiddleware[] = []; - private readonly consumersList: Consumer[] = []; + private readonly consumersList: KernelConsumer[] = []; private readonly loggerInstance: KernelLogger; - private readonly routesList: ServiceClass[] = []; + private readonly routesList: ServiceClass[] = []; private readonly schedulersList: Scheduler[] = []; private readonly shutdownHooks: ShutdownHook[] = []; private dependencyInjectionInstance: DependencyInjection | undefined; @@ -73,7 +73,7 @@ export class Kernel< return path.resolve(Kernel.rootDirectory, 'config'); } - public static get consumers(): Consumer[] { + public static get consumers(): KernelConsumer[] { return Kernel.getActiveKernel().consumers; } @@ -101,7 +101,7 @@ export class Kernel< return process.cwd(); } - public static get routes(): ServiceClass[] { + public static get routes(): ServiceClass[] { return Kernel.getActiveKernel().routes; } @@ -305,9 +305,9 @@ export class Kernel< } private getConsumerFromClass( - ClassDefinition: ServiceClass, - ): Consumer { - return this.di.getService(ClassDefinition); + ClassDefinition: ServiceClass, + ): KernelConsumer { + return this.di.getService(ClassDefinition); } private getInitializerFromClass( @@ -326,7 +326,7 @@ export class Kernel< return this.di.getService(ClassDefinition); } - public get consumers(): Consumer[] { + public get consumers(): KernelConsumer[] { return this.consumersList; } @@ -350,7 +350,7 @@ export class Kernel< return this.loggerInstance; } - public get routes(): ServiceClass[] { + public get routes(): ServiceClass[] { return this.routesList; } @@ -408,7 +408,7 @@ export class Kernel< return result; } - public getRoutes(): ServiceClass[] { + public getRoutes(): ServiceClass[] { return this.routes; } @@ -419,18 +419,20 @@ export class Kernel< } public registerConsumers( - ...ClassDefinitions: ServiceClass[] + ...ClassDefinitions: ServiceClass[] ): void { for (const ClassDefinition of ClassDefinitions) { this.consumersList.push(this.getConsumerFromClass(ClassDefinition)); } } - public registerConsumerInstances(...consumers: Consumer[]): void { + public registerConsumerInstances(...consumers: KernelConsumer[]): void { this.consumersList.push(...consumers); } - public registerRoutes(...ClassDefinitions: ServiceClass[]): void { + public registerRoutes( + ...ClassDefinitions: ServiceClass[] + ): void { this.routesList.push(...ClassDefinitions); } diff --git a/src/adapters/pubsub/Consumer.ts b/src/adapters/pubsub/Consumer.ts index 4f1cc35..76930d3 100644 --- a/src/adapters/pubsub/Consumer.ts +++ b/src/adapters/pubsub/Consumer.ts @@ -1,3 +1,4 @@ +import type { KernelConsumer } from '../../contracts/kernel/index.js'; import type { DomainEventConsumer } from '../../domain/DomainEventConsumer.js'; import type { DomainEvent, @@ -7,7 +8,7 @@ import type { import { Kernel } from '../../Kernel.js'; import { ConsumerMiddlewarePipeline } from './ConsumerMiddlewarePipeline.js'; -export abstract class Consumer { +export abstract class Consumer implements KernelConsumer { constructor(private readonly consumer: DomainEventConsumer) {} private async runMiddleware( diff --git a/src/adapters/ui/routes/Route.ts b/src/adapters/ui/routes/Route.ts index fc0eaa9..c4e7c28 100644 --- a/src/adapters/ui/routes/Route.ts +++ b/src/adapters/ui/routes/Route.ts @@ -1,6 +1,7 @@ +import { KernelRoute } from '../../../contracts/kernel/index.js'; import { Kernel } from '../../../Kernel.js'; -export abstract class Route { +export abstract class Route extends KernelRoute { public get(service: unknown): T { return Kernel.di.getService(service); } diff --git a/src/contracts/kernel/KernelConsumer.ts b/src/contracts/kernel/KernelConsumer.ts new file mode 100644 index 0000000..9b4c974 --- /dev/null +++ b/src/contracts/kernel/KernelConsumer.ts @@ -0,0 +1,5 @@ +export interface KernelConsumer { + readonly queueName: string; + + init(): Promise; +} diff --git a/src/contracts/kernel/KernelRoute.ts b/src/contracts/kernel/KernelRoute.ts new file mode 100644 index 0000000..bd48fc2 --- /dev/null +++ b/src/contracts/kernel/KernelRoute.ts @@ -0,0 +1 @@ +export abstract class KernelRoute {} diff --git a/src/contracts/kernel/index.ts b/src/contracts/kernel/index.ts index 93f32c3..c4a6292 100644 --- a/src/contracts/kernel/index.ts +++ b/src/contracts/kernel/index.ts @@ -3,8 +3,10 @@ export * from './ConsumerExecutionContext.js'; export * from './ConsumerNext.js'; export * from './HandlerContext.js'; export * from './IdempotencyStore.js'; +export * from './KernelConsumer.js'; export * from './KernelLogger.js'; export * from './KernelMiddleware.js'; +export * from './KernelRoute.js'; export * from './RetryDelayResolver.js'; export * from './RetryPredicate.js'; export * from './ServiceResolver.js'; diff --git a/tests/typescript-module-resolution.test.mjs b/tests/typescript-module-resolution.test.mjs index 21aaeb5..1ea21e1 100644 --- a/tests/typescript-module-resolution.test.mjs +++ b/tests/typescript-module-resolution.test.mjs @@ -35,7 +35,7 @@ test('exports types for TypeScript moduleResolution node consumers', async () => await writeFile( path.join(temporaryDirectory, 'index.ts'), ` - import type { ConsumerMiddleware } from '@haskou/ddd-kernel/contracts/kernel'; + import type { ConsumerMiddleware, KernelConsumer, KernelRoute } from '@haskou/ddd-kernel/contracts/kernel'; import type { MessageBus, PublisherHook } from '@haskou/ddd-kernel/contracts/pubsub'; import type { DomainMessageBus } from '@haskou/ddd-kernel/domain'; import type { SchedulerErrorPolicy } from '@haskou/ddd-kernel/scheduler'; @@ -52,12 +52,16 @@ test('exports types for TypeScript moduleResolution node consumers', async () => const httpPort: 3000 | 3001 | undefined = kernel.environment.HTTP_PORT; const enableJobs: true | false | undefined = kernel.environment.ENABLE_JOBS; const middleware: ConsumerMiddleware | undefined = undefined; + const kernelConsumer: KernelConsumer | undefined = undefined; + const kernelRoute: KernelRoute | undefined = undefined; const messageBus: MessageBus | undefined = undefined; const domainMessageBus: DomainMessageBus | undefined = undefined; const hook: PublisherHook | undefined = undefined; const policy: SchedulerErrorPolicy | undefined = undefined; void middleware; + void kernelConsumer; + void kernelRoute; void messageBus; void domainMessageBus; void hook; From f819c41abff599342e7cbda4d7052b4560fc93ca Mon Sep 17 00:00:00 2001 From: Hasko Date: Mon, 29 Jun 2026 12:07:52 +0200 Subject: [PATCH 3/6] =?UTF-8?q?fix(kernel):=20=F0=9F=90=9B=20Constrain=20e?= =?UTF-8?q?nvironment=20schema=20choices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/Kernel.ts | 1 + src/kernel/KernelEnvironmentVariable.ts | 29 ++++--------------- .../KernelEnvironmentVariableDefinition.ts | 9 ++++-- .../KernelEnvironmentVariableResolvedValue.ts | 16 ++++++++++ tests/typescript-module-resolution.test.mjs | 10 +++++++ 6 files changed, 40 insertions(+), 27 deletions(-) create mode 100644 src/kernel/KernelEnvironmentVariableResolvedValue.ts diff --git a/package.json b/package.json index a22d622..829bdb8 100644 --- a/package.json +++ b/package.json @@ -244,7 +244,7 @@ "prepack": "yarn build", "test": "yarn build && c8 node --test \"tests/**/*.test.mjs\"", "build:coverage": "rm -rf dist && tsc -p tsconfig.coverage.json", - "test:coverage": "yarn build:coverage && c8 --all --src src --include \"src/**/*.ts\" --exclude \"src/**/index.ts\" --exclude \"src/contracts/**/*.ts\" --exclude \"src/**/*.d.ts\" --exclude \"src/**/*Options.ts\" --exclude \"src/**/*Context.ts\" --exclude \"src/**/*Handler.ts\" --exclude \"src/**/*Message.ts\" --exclude \"src/**/*Metadata.ts\" --exclude \"src/**/*Registration.ts\" --exclude \"src/**/*Resolver.ts\" --exclude \"src/**/*Authenticator.ts\" --exclude \"src/**/*Consumer.ts\" --exclude \"src/**/*Publisher.ts\" --exclude \"src/**/*Class.ts\" --exclude \"src/**/*Definition.ts\" --exclude \"src/**/*Alias.ts\" --exclude \"src/**/*Internals.ts\" --exclude \"src/**/*Expression.ts\" --exclude \"src/**/*Constructor.ts\" --exclude \"src/**/*Attributes.ts\" --exclude \"src/infrastructure/lifecycle/**/*.ts\" --exclude \"src/kernel/KernelDefaultEnvironment.ts\" --exclude \"src/kernel/KernelEnvironment.ts\" --exclude \"src/kernel/KernelEnvironmentForSchema.ts\" --exclude \"src/kernel/KernelEnvironmentSchema.ts\" --exclude \"src/kernel/KernelEnvironmentValue.ts\" --exclude \"src/kernel/KernelEnvironmentVariable.ts\" --exclude \"src/kernel/KernelEnvironmentVariablePrimitive.ts\" --exclude \"src/kernel/KernelEnvironmentVariableType.ts\" --exclude \"src/kernel/ShutdownCandidate.ts\" --extension .ts --exclude-after-remap --reporter text --reporter lcov node --test \"tests/**/*.test.mjs\"", + "test:coverage": "yarn build:coverage && c8 --all --src src --include \"src/**/*.ts\" --exclude \"src/**/index.ts\" --exclude \"src/contracts/**/*.ts\" --exclude \"src/**/*.d.ts\" --exclude \"src/**/*Options.ts\" --exclude \"src/**/*Context.ts\" --exclude \"src/**/*Handler.ts\" --exclude \"src/**/*Message.ts\" --exclude \"src/**/*Metadata.ts\" --exclude \"src/**/*Registration.ts\" --exclude \"src/**/*Resolver.ts\" --exclude \"src/**/*Authenticator.ts\" --exclude \"src/**/*Consumer.ts\" --exclude \"src/**/*Publisher.ts\" --exclude \"src/**/*Class.ts\" --exclude \"src/**/*Definition.ts\" --exclude \"src/**/*Alias.ts\" --exclude \"src/**/*Internals.ts\" --exclude \"src/**/*Expression.ts\" --exclude \"src/**/*Constructor.ts\" --exclude \"src/**/*Attributes.ts\" --exclude \"src/infrastructure/lifecycle/**/*.ts\" --exclude \"src/kernel/KernelDefaultEnvironment.ts\" --exclude \"src/kernel/KernelEnvironment.ts\" --exclude \"src/kernel/KernelEnvironmentForSchema.ts\" --exclude \"src/kernel/KernelEnvironmentSchema.ts\" --exclude \"src/kernel/KernelEnvironmentValue.ts\" --exclude \"src/kernel/KernelEnvironmentVariable.ts\" --exclude \"src/kernel/KernelEnvironmentVariablePrimitive.ts\" --exclude \"src/kernel/KernelEnvironmentVariableResolvedValue.ts\" --exclude \"src/kernel/KernelEnvironmentVariableType.ts\" --exclude \"src/kernel/ShutdownCandidate.ts\" --extension .ts --exclude-after-remap --reporter text --reporter lcov node --test \"tests/**/*.test.mjs\"", "typecheck": "tsc -p tsconfig.json --noEmit" }, "keywords": [ diff --git a/src/Kernel.ts b/src/Kernel.ts index 9a4481b..fb9d42d 100644 --- a/src/Kernel.ts +++ b/src/Kernel.ts @@ -31,6 +31,7 @@ export type { KernelEnvironmentSchema } from './kernel/KernelEnvironmentSchema.j export type { KernelEnvironmentValue } from './kernel/KernelEnvironmentValue.js'; export type { KernelEnvironmentVariableDefinition } from './kernel/KernelEnvironmentVariableDefinition.js'; export type { KernelEnvironmentVariablePrimitive } from './kernel/KernelEnvironmentVariablePrimitive.js'; +export type { KernelEnvironmentVariableResolvedValue } from './kernel/KernelEnvironmentVariableResolvedValue.js'; export type { KernelEnvironmentVariableType } from './kernel/KernelEnvironmentVariableType.js'; export type { KernelEnvironmentVariablesOptions } from './kernel/KernelEnvironmentVariablesOptions.js'; export type { KernelOptions } from './kernel/KernelOptions.js'; diff --git a/src/kernel/KernelEnvironmentVariable.ts b/src/kernel/KernelEnvironmentVariable.ts index b896343..a16fe5f 100644 --- a/src/kernel/KernelEnvironmentVariable.ts +++ b/src/kernel/KernelEnvironmentVariable.ts @@ -1,27 +1,10 @@ -import type { KernelEnvironmentValue } from './KernelEnvironmentValue.js'; import type { KernelEnvironmentVariableDefinition } from './KernelEnvironmentVariableDefinition.js'; -import type { KernelEnvironmentVariablePrimitive } from './KernelEnvironmentVariablePrimitive.js'; -import type { KernelEnvironmentVariableType } from './KernelEnvironmentVariableType.js'; +import type { KernelEnvironmentVariableResolvedValue } from './KernelEnvironmentVariableResolvedValue.js'; export type KernelEnvironmentVariable< TDefinition extends KernelEnvironmentVariableDefinition, -> = - TDefinition extends KernelEnvironmentVariableDefinition< - infer TType extends KernelEnvironmentVariableType, - infer TRequired extends boolean, - infer TChoices extends readonly KernelEnvironmentValue[] | undefined - > - ? TDefinition extends { readonly defaultValue: unknown } - ? TChoices extends readonly KernelEnvironmentVariablePrimitive[] - ? TChoices[number] - : KernelEnvironmentVariablePrimitive - : TRequired extends true - ? TChoices extends readonly KernelEnvironmentVariablePrimitive[] - ? TChoices[number] - : KernelEnvironmentVariablePrimitive - : - | (TChoices extends readonly KernelEnvironmentVariablePrimitive[] - ? TChoices[number] - : KernelEnvironmentVariablePrimitive) - | undefined - : never; +> = TDefinition extends { readonly defaultValue: unknown } + ? KernelEnvironmentVariableResolvedValue + : TDefinition extends { readonly required: true } + ? KernelEnvironmentVariableResolvedValue + : KernelEnvironmentVariableResolvedValue | undefined; diff --git a/src/kernel/KernelEnvironmentVariableDefinition.ts b/src/kernel/KernelEnvironmentVariableDefinition.ts index d6a508c..b0b60b7 100644 --- a/src/kernel/KernelEnvironmentVariableDefinition.ts +++ b/src/kernel/KernelEnvironmentVariableDefinition.ts @@ -1,16 +1,19 @@ -import type { KernelEnvironmentValue } from './KernelEnvironmentValue.js'; import type { KernelEnvironmentVariablePrimitive } from './KernelEnvironmentVariablePrimitive.js'; import type { KernelEnvironmentVariableType } from './KernelEnvironmentVariableType.js'; export interface KernelEnvironmentVariableDefinition< TType extends KernelEnvironmentVariableType = KernelEnvironmentVariableType, TRequired extends boolean = boolean, - TChoices extends readonly KernelEnvironmentValue[] | undefined = + TChoices extends + | readonly KernelEnvironmentVariablePrimitive[] + | undefined = | readonly KernelEnvironmentVariablePrimitive[] | undefined, > { readonly choices?: TChoices; - readonly defaultValue?: KernelEnvironmentVariablePrimitive; + readonly defaultValue?: TChoices extends readonly KernelEnvironmentVariablePrimitive[] + ? TChoices[number] + : KernelEnvironmentVariablePrimitive; readonly description?: string; readonly required?: TRequired; readonly sensitive?: boolean; diff --git a/src/kernel/KernelEnvironmentVariableResolvedValue.ts b/src/kernel/KernelEnvironmentVariableResolvedValue.ts new file mode 100644 index 0000000..766dd64 --- /dev/null +++ b/src/kernel/KernelEnvironmentVariableResolvedValue.ts @@ -0,0 +1,16 @@ +import type { KernelEnvironmentValue } from './KernelEnvironmentValue.js'; +import type { KernelEnvironmentVariableDefinition } from './KernelEnvironmentVariableDefinition.js'; +import type { KernelEnvironmentVariablePrimitive } from './KernelEnvironmentVariablePrimitive.js'; +import type { KernelEnvironmentVariableType } from './KernelEnvironmentVariableType.js'; + +export type KernelEnvironmentVariableResolvedValue< + TDefinition extends KernelEnvironmentVariableDefinition, +> = TDefinition extends { + readonly choices: readonly (infer TChoice extends KernelEnvironmentValue)[]; +} + ? TChoice + : TDefinition extends KernelEnvironmentVariableDefinition< + infer TType extends KernelEnvironmentVariableType + > + ? KernelEnvironmentVariablePrimitive + : never; diff --git a/tests/typescript-module-resolution.test.mjs b/tests/typescript-module-resolution.test.mjs index 1ea21e1..e75515e 100644 --- a/tests/typescript-module-resolution.test.mjs +++ b/tests/typescript-module-resolution.test.mjs @@ -51,6 +51,12 @@ test('exports types for TypeScript moduleResolution node consumers', async () => const nodeEnvironment: 'local' | 'test' | undefined = kernel.environment.NODE_ENV; const httpPort: 3000 | 3001 | undefined = kernel.environment.HTTP_PORT; const enableJobs: true | false | undefined = kernel.environment.ENABLE_JOBS; + const invalidNumberChoicesSchema = { + HTTP_PORT: { choices: ['3000'], type: 'number' }, + } as const; + const invalidDefaultChoicesSchema = { + NODE_ENV: { choices: ['local', 'test'], defaultValue: 'production', type: 'string' }, + } as const; const middleware: ConsumerMiddleware | undefined = undefined; const kernelConsumer: KernelConsumer | undefined = undefined; const kernelRoute: KernelRoute | undefined = undefined; @@ -68,6 +74,10 @@ test('exports types for TypeScript moduleResolution node consumers', async () => void nodeEnvironment; void httpPort; void enableJobs; + // @ts-expect-error choices must match the declared environment variable type. + void new Kernel({ environmentSchema: invalidNumberChoicesSchema }); + // @ts-expect-error defaultValue must be one of the declared choices. + void new Kernel({ environmentSchema: invalidDefaultChoicesSchema }); void policy; void ExpressKernelServer; `, From 61625d88d1e9272154e61a47ba4817058244dbdb Mon Sep 17 00:00:00 2001 From: Hasko Date: Mon, 29 Jun 2026 12:14:30 +0200 Subject: [PATCH 4/6] =?UTF-8?q?fix(kernel):=20=F0=9F=90=9B=20Validate=20en?= =?UTF-8?q?vironment=20schema=20inputs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/Kernel.ts | 1 + src/kernel/KernelEnvironmentSchemaInput.ts | 27 +++++++++++++++++++ .../KernelEnvironmentVariablesOptions.ts | 5 +++- src/kernel/KernelOptions.ts | 5 +++- 5 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 src/kernel/KernelEnvironmentSchemaInput.ts diff --git a/package.json b/package.json index 829bdb8..9ca7e3d 100644 --- a/package.json +++ b/package.json @@ -244,7 +244,7 @@ "prepack": "yarn build", "test": "yarn build && c8 node --test \"tests/**/*.test.mjs\"", "build:coverage": "rm -rf dist && tsc -p tsconfig.coverage.json", - "test:coverage": "yarn build:coverage && c8 --all --src src --include \"src/**/*.ts\" --exclude \"src/**/index.ts\" --exclude \"src/contracts/**/*.ts\" --exclude \"src/**/*.d.ts\" --exclude \"src/**/*Options.ts\" --exclude \"src/**/*Context.ts\" --exclude \"src/**/*Handler.ts\" --exclude \"src/**/*Message.ts\" --exclude \"src/**/*Metadata.ts\" --exclude \"src/**/*Registration.ts\" --exclude \"src/**/*Resolver.ts\" --exclude \"src/**/*Authenticator.ts\" --exclude \"src/**/*Consumer.ts\" --exclude \"src/**/*Publisher.ts\" --exclude \"src/**/*Class.ts\" --exclude \"src/**/*Definition.ts\" --exclude \"src/**/*Alias.ts\" --exclude \"src/**/*Internals.ts\" --exclude \"src/**/*Expression.ts\" --exclude \"src/**/*Constructor.ts\" --exclude \"src/**/*Attributes.ts\" --exclude \"src/infrastructure/lifecycle/**/*.ts\" --exclude \"src/kernel/KernelDefaultEnvironment.ts\" --exclude \"src/kernel/KernelEnvironment.ts\" --exclude \"src/kernel/KernelEnvironmentForSchema.ts\" --exclude \"src/kernel/KernelEnvironmentSchema.ts\" --exclude \"src/kernel/KernelEnvironmentValue.ts\" --exclude \"src/kernel/KernelEnvironmentVariable.ts\" --exclude \"src/kernel/KernelEnvironmentVariablePrimitive.ts\" --exclude \"src/kernel/KernelEnvironmentVariableResolvedValue.ts\" --exclude \"src/kernel/KernelEnvironmentVariableType.ts\" --exclude \"src/kernel/ShutdownCandidate.ts\" --extension .ts --exclude-after-remap --reporter text --reporter lcov node --test \"tests/**/*.test.mjs\"", + "test:coverage": "yarn build:coverage && c8 --all --src src --include \"src/**/*.ts\" --exclude \"src/**/index.ts\" --exclude \"src/contracts/**/*.ts\" --exclude \"src/**/*.d.ts\" --exclude \"src/**/*Options.ts\" --exclude \"src/**/*Context.ts\" --exclude \"src/**/*Handler.ts\" --exclude \"src/**/*Message.ts\" --exclude \"src/**/*Metadata.ts\" --exclude \"src/**/*Registration.ts\" --exclude \"src/**/*Resolver.ts\" --exclude \"src/**/*Authenticator.ts\" --exclude \"src/**/*Consumer.ts\" --exclude \"src/**/*Publisher.ts\" --exclude \"src/**/*Class.ts\" --exclude \"src/**/*Definition.ts\" --exclude \"src/**/*Alias.ts\" --exclude \"src/**/*Internals.ts\" --exclude \"src/**/*Expression.ts\" --exclude \"src/**/*Constructor.ts\" --exclude \"src/**/*Attributes.ts\" --exclude \"src/infrastructure/lifecycle/**/*.ts\" --exclude \"src/kernel/KernelDefaultEnvironment.ts\" --exclude \"src/kernel/KernelEnvironment.ts\" --exclude \"src/kernel/KernelEnvironmentForSchema.ts\" --exclude \"src/kernel/KernelEnvironmentSchema.ts\" --exclude \"src/kernel/KernelEnvironmentSchemaInput.ts\" --exclude \"src/kernel/KernelEnvironmentValue.ts\" --exclude \"src/kernel/KernelEnvironmentVariable.ts\" --exclude \"src/kernel/KernelEnvironmentVariablePrimitive.ts\" --exclude \"src/kernel/KernelEnvironmentVariableResolvedValue.ts\" --exclude \"src/kernel/KernelEnvironmentVariableType.ts\" --exclude \"src/kernel/ShutdownCandidate.ts\" --extension .ts --exclude-after-remap --reporter text --reporter lcov node --test \"tests/**/*.test.mjs\"", "typecheck": "tsc -p tsconfig.json --noEmit" }, "keywords": [ diff --git a/src/Kernel.ts b/src/Kernel.ts index fb9d42d..a6e451a 100644 --- a/src/Kernel.ts +++ b/src/Kernel.ts @@ -28,6 +28,7 @@ export type { KernelDefaultEnvironment } from './kernel/KernelDefaultEnvironment export type { KernelEnvironment } from './kernel/KernelEnvironment.js'; export type { KernelEnvironmentForSchema } from './kernel/KernelEnvironmentForSchema.js'; export type { KernelEnvironmentSchema } from './kernel/KernelEnvironmentSchema.js'; +export type { KernelEnvironmentSchemaInput } from './kernel/KernelEnvironmentSchemaInput.js'; export type { KernelEnvironmentValue } from './kernel/KernelEnvironmentValue.js'; export type { KernelEnvironmentVariableDefinition } from './kernel/KernelEnvironmentVariableDefinition.js'; export type { KernelEnvironmentVariablePrimitive } from './kernel/KernelEnvironmentVariablePrimitive.js'; diff --git a/src/kernel/KernelEnvironmentSchemaInput.ts b/src/kernel/KernelEnvironmentSchemaInput.ts new file mode 100644 index 0000000..b24f51c --- /dev/null +++ b/src/kernel/KernelEnvironmentSchemaInput.ts @@ -0,0 +1,27 @@ +import type { KernelEnvironmentSchema } from './KernelEnvironmentSchema.js'; +import type { KernelEnvironmentVariableDefinition } from './KernelEnvironmentVariableDefinition.js'; +import type { KernelEnvironmentVariablePrimitive } from './KernelEnvironmentVariablePrimitive.js'; +import type { KernelEnvironmentVariableType } from './KernelEnvironmentVariableType.js'; + +export type KernelEnvironmentSchemaInput< + TSchema extends KernelEnvironmentSchema, +> = { + readonly [TKey in keyof TSchema]: TSchema[TKey] extends { + readonly choices: infer TChoices extends + readonly KernelEnvironmentVariablePrimitive[]; + readonly type: infer TType extends KernelEnvironmentVariableType; + } + ? KernelEnvironmentVariableDefinition< + TType, + TSchema[TKey] extends { readonly required: true } ? true : boolean, + TChoices + > + : TSchema[TKey] extends { + readonly type: infer TType extends KernelEnvironmentVariableType; + } + ? KernelEnvironmentVariableDefinition< + TType, + TSchema[TKey] extends { readonly required: true } ? true : boolean + > + : never; +}; diff --git a/src/kernel/KernelEnvironmentVariablesOptions.ts b/src/kernel/KernelEnvironmentVariablesOptions.ts index b24f0e0..d636a18 100644 --- a/src/kernel/KernelEnvironmentVariablesOptions.ts +++ b/src/kernel/KernelEnvironmentVariablesOptions.ts @@ -1,9 +1,12 @@ import type { KernelEnvironmentSchema } from './KernelEnvironmentSchema.js'; +import type { KernelEnvironmentSchemaInput } from './KernelEnvironmentSchemaInput.js'; export interface KernelEnvironmentVariablesOptions< TSchema extends KernelEnvironmentSchema | undefined = undefined, > { readonly override?: boolean; readonly path?: string; - readonly schema?: TSchema; + readonly schema?: TSchema extends KernelEnvironmentSchema + ? TSchema & KernelEnvironmentSchemaInput + : TSchema; } diff --git a/src/kernel/KernelOptions.ts b/src/kernel/KernelOptions.ts index 401bd2a..c31f46e 100644 --- a/src/kernel/KernelOptions.ts +++ b/src/kernel/KernelOptions.ts @@ -1,12 +1,15 @@ import type { KernelLogger } from '../contracts/index.js'; import type { DependencyInjection } from '../infrastructure/dependency-injection/index.js'; import type { KernelEnvironmentSchema } from './KernelEnvironmentSchema.js'; +import type { KernelEnvironmentSchemaInput } from './KernelEnvironmentSchemaInput.js'; export interface KernelOptions< TEnvironmentSchema extends KernelEnvironmentSchema | undefined = undefined, > { readonly di?: DependencyInjection; - readonly environmentSchema?: TEnvironmentSchema; + readonly environmentSchema?: TEnvironmentSchema extends KernelEnvironmentSchema + ? TEnvironmentSchema & KernelEnvironmentSchemaInput + : TEnvironmentSchema; readonly logger?: KernelLogger; readonly servicesYamlPath?: string; readonly sourceDirectory?: string; From 62ba856614b9b70ad40c8c199697f18cf1624977 Mon Sep 17 00:00:00 2001 From: Hasko Date: Mon, 29 Jun 2026 13:00:44 +0200 Subject: [PATCH 5/6] =?UTF-8?q?chore(format):=20=F0=9F=8E=A8=20Apply=20upd?= =?UTF-8?q?ated=20dependency=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/kernel/KernelEnvironmentVariableDefinition.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/kernel/KernelEnvironmentVariableDefinition.ts b/src/kernel/KernelEnvironmentVariableDefinition.ts index b0b60b7..40eae6c 100644 --- a/src/kernel/KernelEnvironmentVariableDefinition.ts +++ b/src/kernel/KernelEnvironmentVariableDefinition.ts @@ -5,10 +5,8 @@ export interface KernelEnvironmentVariableDefinition< TType extends KernelEnvironmentVariableType = KernelEnvironmentVariableType, TRequired extends boolean = boolean, TChoices extends - | readonly KernelEnvironmentVariablePrimitive[] - | undefined = - | readonly KernelEnvironmentVariablePrimitive[] - | undefined, + readonly KernelEnvironmentVariablePrimitive[] | undefined = + readonly KernelEnvironmentVariablePrimitive[] | undefined, > { readonly choices?: TChoices; readonly defaultValue?: TChoices extends readonly KernelEnvironmentVariablePrimitive[] From 35229f8b6cd3701e1b18c058eeb5df3f19b36dd1 Mon Sep 17 00:00:00 2001 From: Hasko Date: Mon, 29 Jun 2026 13:13:08 +0200 Subject: [PATCH 6/6] =?UTF-8?q?fix(kernel):=20=F0=9F=90=9B=20Make=20route?= =?UTF-8?q?=20registration=20contract=20nominal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/contracts/kernel/KernelRoute.ts | 4 +++- tests/typescript-module-resolution.test.mjs | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/contracts/kernel/KernelRoute.ts b/src/contracts/kernel/KernelRoute.ts index bd48fc2..b9c00a0 100644 --- a/src/contracts/kernel/KernelRoute.ts +++ b/src/contracts/kernel/KernelRoute.ts @@ -1 +1,3 @@ -export abstract class KernelRoute {} +export abstract class KernelRoute { + declare protected readonly kernelRouteContract: never; +} diff --git a/tests/typescript-module-resolution.test.mjs b/tests/typescript-module-resolution.test.mjs index e75515e..958b26b 100644 --- a/tests/typescript-module-resolution.test.mjs +++ b/tests/typescript-module-resolution.test.mjs @@ -48,6 +48,7 @@ test('exports types for TypeScript moduleResolution node consumers', async () => NODE_ENV: { choices: ['local', 'test'], type: 'string' }, } as const; const kernel = new Kernel({ environmentSchema }); + class ArbitraryService {} const nodeEnvironment: 'local' | 'test' | undefined = kernel.environment.NODE_ENV; const httpPort: 3000 | 3001 | undefined = kernel.environment.HTTP_PORT; const enableJobs: true | false | undefined = kernel.environment.ENABLE_JOBS; @@ -78,6 +79,8 @@ test('exports types for TypeScript moduleResolution node consumers', async () => void new Kernel({ environmentSchema: invalidNumberChoicesSchema }); // @ts-expect-error defaultValue must be one of the declared choices. void new Kernel({ environmentSchema: invalidDefaultChoicesSchema }); + // @ts-expect-error registered routes must intentionally extend KernelRoute. + kernel.registerRoutes(ArbitraryService); void policy; void ExpressKernelServer; `,