diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 5ed05b6..0b9dd25 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -15,7 +15,7 @@ const referenceSidebar = [ ], }, { - text: 'Domain', + text: 'Contracts And Domain', collapsed: false, items: [ { text: 'AggregateRoot', link: '/reference/aggregate-root' }, @@ -28,7 +28,7 @@ const referenceSidebar = [ ], }, { - text: 'Adapters', + text: 'Pub/Sub', collapsed: false, items: [ { text: 'Consumer', link: '/reference/consumer' }, @@ -37,12 +37,38 @@ const referenceSidebar = [ link: '/reference/amqp-message-bus-adapter', }, { text: 'InMemoryPubSub', link: '/reference/in-memory-pub-sub' }, + ], + }, + { + text: 'DB', + collapsed: false, + items: [ { text: 'InMemoryRepository', link: '/reference/in-memory-repository' }, { text: 'MongoRepository', link: '/reference/mongo-repository' }, + ], + }, + { + text: 'UI', + collapsed: false, + items: [ { text: 'ExpressKernelServer', link: '/reference/express-kernel-server' }, { text: 'Route', link: '/reference/route' }, - { text: 'Scheduler', link: '/reference/scheduler' }, - { text: 'WinstonLogger', link: '/reference/winston-logger' }, + ], + }, + { + text: 'Scheduling', + collapsed: false, + items: [{ text: 'Scheduler', link: '/reference/scheduler' }], + }, + { + text: 'Logs', + collapsed: false, + items: [{ text: 'WinstonLogger', link: '/reference/winston-logger' }], + }, + { + text: 'WebSocket', + collapsed: false, + items: [ { text: 'WebSocketEventHub', link: '/reference/web-socket-event-hub' }, { text: 'WebSocketRealtimeServer', @@ -52,6 +78,36 @@ const referenceSidebar = [ }, ]; +const gettingStartedSidebar = [ + { + text: 'Getting started', + items: [ + { text: 'Introduction', link: '/getting-started/introduction' }, + { text: 'Package map', link: '/getting-started/package-map' }, + { text: 'Installation', link: '/getting-started/installation' }, + { + text: 'Application startup', + link: '/getting-started/application-startup', + }, + ], + }, +]; + +const guideSidebar = [ + { + text: 'Guides', + items: [ + { + text: 'Dependency injection', + link: '/guides/dependency-injection', + }, + { text: 'Adapters', link: '/guides/adapters' }, + { text: 'AMQP pub/sub', link: '/guides/amqp-pubsub' }, + { text: 'HTTP routes', link: '/guides/http-routes' }, + ], + }, +]; + export default defineConfig({ lang: 'en-US', title: 'DDD Kernel', @@ -77,55 +133,21 @@ export default defineConfig({ siteTitle: 'DDD Kernel', nav: [ - { text: 'Guide', link: '/getting-started/introduction' }, - { text: 'Adapters', link: '/guides/adapters' }, + { text: 'Getting Started', link: '/getting-started/introduction' }, + { text: 'Guide', link: '/guides/dependency-injection' }, { text: 'Reference', link: '/reference/' }, ], sidebar: { - '/getting-started/': [ - { - text: 'Getting started', - items: [ - { text: 'Introduction', link: '/getting-started/introduction' }, - { text: 'Installation', link: '/getting-started/installation' }, - { - text: 'Application startup', - link: '/getting-started/application-startup', - }, - ], - }, - { - text: 'Guides', - items: [ - { - text: 'Dependency injection', - link: '/guides/dependency-injection', - }, - { text: 'Adapters', link: '/guides/adapters' }, - { text: 'AMQP pub/sub', link: '/guides/amqp-pubsub' }, - { text: 'HTTP routes', link: '/guides/http-routes' }, - ], - }, - ], - '/guides/': [ - { - text: 'Guides', - items: [ - { - text: 'Dependency injection', - link: '/guides/dependency-injection', - }, - { text: 'Adapters', link: '/guides/adapters' }, - { text: 'AMQP pub/sub', link: '/guides/amqp-pubsub' }, - { text: 'HTTP routes', link: '/guides/http-routes' }, - ], - }, - ...referenceSidebar, - ], + '/getting-started/': gettingStartedSidebar, + '/guides/': guideSidebar, '/reference/': referenceSidebar, }, + socialLinks: [ + { icon: 'github', link: 'https://github.com/haskou/ddd-kernel' }, + ], + outline: { level: [2, 3], label: 'On this page', diff --git a/docs/getting-started/application-startup.md b/docs/getting-started/application-startup.md index 4d6d75c..a6903fd 100644 --- a/docs/getting-started/application-startup.md +++ b/docs/getting-started/application-startup.md @@ -11,9 +11,14 @@ import { applicationConsumers } from './apps/ApplicationConsumers.js'; import { recurringSchedulers } from './apps/ApplicationSchedulers.js'; import GetUserByIdRoute from './apps/api/routes/GetUserByIdRoute.js'; -const kernel = new Kernel(); +const kernel = new Kernel({ + sourceDirectory: 'src', + servicesYamlPath: 'config/container/services.yaml', +}); -await kernel.dependencyInjection(); +await kernel.dependencyInjection({ + containerBuild: process.env.CONTAINER_BUILD === 'true', +}); kernel.registerRoutes(GetUserByIdRoute); kernel.registerConsumers(...applicationConsumers); kernel.registerSchedulers(...recurringSchedulers); @@ -31,6 +36,9 @@ kernel.logger.info('Application running on port 3000'); `CONTAINER_BUILD=true` regenerates `config/container/services.yaml`. Without it, the generated YAML is loaded. +`new Kernel(...)` configures defaults for the runtime. `kernel.dependencyInjection(...)` +can override the DI-specific values for a particular boot. + Call `kernel.shutdown()` from your process signal handlers to stop consumers, stop schedulers, close servers, flush logs and release connections registered through shutdown hooks: diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 4a78440..ae9ac24 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,28 +1,69 @@ # Installation -Install the package in an application: +Install the kernel package in an application: ```bash yarn add @haskou/ddd-kernel ``` -Install only the peer dependencies required by the adapters you import. For -example, AMQP requires `amqplib`: +That is enough for the core kernel, contracts, domain primitives, dependency +injection and in-memory adapters. + +Install extra packages only for the adapters your application imports. + +## Pub/Sub Adapters + +The in-memory pub/sub adapter has no extra runtime dependencies. + +The AMQP adapter uses `amqplib`: ```bash yarn add amqplib ``` -Express routes require: +## DB Adapters + +The in-memory repository adapter has no extra runtime dependencies. + +The MongoDB repository adapter uses `mongodb`: + +```bash +yarn add mongodb +``` + +## UI Adapters + +The Express adapter uses `express`, `routing-controllers` and decorator +metadata packages: ```bash yarn add express routing-controllers reflect-metadata class-transformer class-validator ``` -MongoDB repositories require: +Install `cors` only when enabling `routingControllersOptions.cors`: ```bash -yarn add mongodb +yarn add cors +``` + +## Other Adapters + +Schedulers use `node-cron`: + +```bash +yarn add node-cron +``` + +The Winston logger uses `winston`: + +```bash +yarn add winston +``` + +WebSocket helpers use `ws`: + +```bash +yarn add ws ``` ## TypeScript Resolution diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md index a89312b..01a2ff0 100644 --- a/docs/getting-started/introduction.md +++ b/docs/getting-started/introduction.md @@ -15,3 +15,12 @@ Use it when you want: The core package does not force a database or HTTP framework into your app. Use adapter subpaths only when a service needs them. + +The recommended dependency direction is: + +1. Domain and application code depend on contracts and domain primitives. +2. Adapters implement those contracts at the infrastructure boundary. +3. Bootstrap code chooses the adapter for the current runtime. + +For the available imports and adapters, see the +[package map](/getting-started/package-map). diff --git a/docs/getting-started/package-map.md b/docs/getting-started/package-map.md new file mode 100644 index 0000000..531f6c7 --- /dev/null +++ b/docs/getting-started/package-map.md @@ -0,0 +1,74 @@ +# Concepts And Package Map + +`@haskou/ddd-kernel` is split into three layers: + +- **Core**: lifecycle, dependency injection, kernel logging contracts and the + `Kernel` runtime. +- **Contracts and domain primitives**: stable interfaces and base classes that + application code depends on. +- **Adapters**: optional infrastructure implementations for a contract or + runtime boundary. + +Application code should usually depend on contracts and domain primitives. +Bootstrap code chooses adapters. + +## Core + +| Area | Import | Purpose | +| ---------------------- | ----------------------------------------- | ------------------------------------------------------------------------ | +| Kernel runtime | `@haskou/ddd-kernel` | Registers consumers, routes, schedulers, runtimes and shutdown hooks. | +| 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. | + +`node-dependency-injection` and `fs-extra` are package dependencies because the +core DI implementation uses them directly. Applications do not install them +separately. + +## Contracts And Domain + +| Area | Import | Purpose | +| ----------------- | ------------------------------------- | -------------------------------------------------------------------------------------------- | +| Domain | `@haskou/ddd-kernel/domain` | `AggregateRoot`, `DomainEvent`, domain event bus contracts and consumer/publisher contracts. | +| Pub/sub contracts | `@haskou/ddd-kernel/contracts/pubsub` | Generic message bus, consumer registration and handler contracts. | +| DB contracts | `@haskou/ddd-kernel/contracts/db` | Repository-oriented persistence contracts. | +| UI contracts | `@haskou/ddd-kernel/contracts/ui` | Route contracts and HTTP status constants. | + +## Adapters + +| Group | Adapter | Import | Extra packages | +| --------- | -------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Pub/sub | In-memory | `@haskou/ddd-kernel/adapters/pubsub/in-memory` | None | +| Pub/sub | AMQP | `@haskou/ddd-kernel/adapters/pubsub/amqp` | `amqplib` | +| DB | In-memory repository | `@haskou/ddd-kernel/adapters/db/in-memory` | None | +| DB | Mongo repository | `@haskou/ddd-kernel/adapters/db/mongo` | `mongodb` | +| UI | Express server and HTTP error handling | `@haskou/ddd-kernel/adapters/ui/express` | `express`, `routing-controllers`, `reflect-metadata`, `class-transformer`, `class-validator`; `cors` only when `routingControllersOptions.cors` is enabled | +| UI | Route base class | `@haskou/ddd-kernel/adapters/ui/routes` | Same runtime as the HTTP server that executes routes | +| Kernel | Console logger | `@haskou/ddd-kernel/adapters/kernel/console` | None | +| Logs | Winston logger | `@haskou/ddd-kernel/logs` | `winston` | +| Scheduler | Cron scheduler base | `@haskou/ddd-kernel/scheduler` | `node-cron` | +| WebSocket | Event hub and realtime server | `@haskou/ddd-kernel/websocket` | `ws` | + +Adapters are optional. Importing the kernel or domain contracts does not require +Express, AMQP, MongoDB, Winston or WebSocket packages. + +## Application Boundary + +Use constructor injection inside consumers, schedulers, routes and services. +Use bootstrap code to choose infrastructure: + +```ts +await kernel.dependencyInjection({ + overrides: [ + { + token: UserRepository, + useClass: + process.env.NODE_ENV === 'test' + ? InMemoryUserRepository + : MongoUserRepository, + }, + ], +}); +``` + +That keeps domain/application code stable while adapters change per runtime. diff --git a/docs/guides/adapters.md b/docs/guides/adapters.md index 373c3a7..16e34d0 100644 --- a/docs/guides/adapters.md +++ b/docs/guides/adapters.md @@ -7,6 +7,16 @@ Adapters live under the same high-level groups as contracts: - `adapters/ui` implements HTTP/UI runtime concerns. - `adapters/kernel` implements kernel-level defaults such as logging. +The package already includes these adapters: + +| Group | Included adapters | +| ------------------- | ------------------------------------------------------------------------- | +| Pub/sub | `InMemoryPubSub`, `AmqpMessageBusAdapter`, consumer middleware primitives | +| DB | `InMemoryRepository`, `MongoRepository` | +| UI | `ExpressKernelServer`, `HttpErrorHandler`, route base classes | +| Kernel/logging | `ConsoleKernelLogger`, `WinstonLogger` | +| Scheduler/WebSocket | `Scheduler`, `WebSocketEventHub`, `WebSocketRealtimeServer` | + An adapter should implement a contract or abstract class from the domain/kernel surface and stay free of application-specific concepts. diff --git a/docs/reference/console-kernel-logger.md b/docs/reference/console-kernel-logger.md index 750faa6..5d92b93 100644 --- a/docs/reference/console-kernel-logger.md +++ b/docs/reference/console-kernel-logger.md @@ -12,3 +12,15 @@ const kernel = new Kernel({ It implements `KernelLogger` with `console.debug`, `console.info`, `console.warn` and `console.error`. + +You usually do not need to configure it explicitly. `Kernel` creates one when +no `logger` option is provided: + +```ts +const kernel = new Kernel(); + +kernel.logger.info('Application running'); +``` + +Use a custom `KernelLogger` or `WinstonLogger` when the application needs +structured logs, files or a central logging backend. diff --git a/docs/reference/dependency-injection.md b/docs/reference/dependency-injection.md index c5f4394..224e09c 100644 --- a/docs/reference/dependency-injection.md +++ b/docs/reference/dependency-injection.md @@ -1,6 +1,19 @@ # DependencyInjection -Wrapper around `node-dependency-injection`. +Dependency injection wrapper used by `Kernel`. + +Most applications should configure DI through the kernel: + +```ts +const kernel = new Kernel(); + +await kernel.dependencyInjection({ + containerBuild: process.env.NODE_ENV === 'production', +}); +``` + +Use `DependencyInjection` directly only when a test or custom runtime needs to +build the container without a full kernel instance. ```ts import { DependencyInjection } from '@haskou/ddd-kernel/dependency-injection'; @@ -14,7 +27,18 @@ const di = DependencyInjection.configure({ await di.compile(); ``` -Most applications call `kernel.dependencyInjection()` instead. +## Options + +| Option | Required | Purpose | +| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `containerBuild` | No | Regenerate `services.yaml` from default-exported classes when `true`; load the existing YAML when `false`. If omitted through the kernel, `CONTAINER_BUILD=true` is used as fallback. | +| `servicesYamlPath` | Yes for direct `DependencyInjection.configure` | Path to the generated or committed container YAML file. | +| `sourceDirectory` | Yes for direct `DependencyInjection.configure` | Source tree scanned when generating the container. | +| `overrides` | No | Runtime or test replacements applied before container compilation. | + +The generated container follows the project convention: one default-exported +class per file. Application classes should receive dependencies through their +constructor. ## Overrides diff --git a/docs/reference/index.md b/docs/reference/index.md index a745289..6c503c4 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -1,3 +1,43 @@ # Reference -Reference pages document the main classes and adapters exported by the package. +Reference pages are grouped by the layer they belong to. + +## Kernel + +- [Kernel](/reference/kernel): application runtime, lifecycle and registration + API. +- [DependencyInjection](/reference/dependency-injection): DI container setup, + container build mode and overrides. +- [ConsoleKernelLogger](/reference/console-kernel-logger): default + `KernelLogger` implementation. + +## Contracts And Domain + +- [AggregateRoot](/reference/aggregate-root) +- [DomainEvent](/reference/domain-event) +- [DomainEventConsumer](/reference/domain-event-consumer) +- [DomainEventPublisher](/reference/domain-event-publisher) + +## Pub/Sub + +- [Consumer](/reference/consumer): base class for application consumers. +- [InMemoryPubSub](/reference/in-memory-pub-sub): in-memory pub/sub adapter. +- [AmqpMessageBusAdapter](/reference/amqp-message-bus-adapter): AMQP + domain-event adapter. + +## DB + +- [InMemoryRepository](/reference/in-memory-repository) +- [MongoRepository](/reference/mongo-repository) + +## UI + +- [ExpressKernelServer](/reference/express-kernel-server) +- [Route](/reference/route) + +## Scheduling, Logs And WebSocket + +- [Scheduler](/reference/scheduler) +- [WinstonLogger](/reference/winston-logger) +- [WebSocketEventHub](/reference/web-socket-event-hub) +- [WebSocketRealtimeServer](/reference/web-socket-realtime-server) diff --git a/docs/reference/kernel.md b/docs/reference/kernel.md index d7780c2..85e1b1c 100644 --- a/docs/reference/kernel.md +++ b/docs/reference/kernel.md @@ -24,6 +24,92 @@ await kernel.runConsumers(); The kernel core does not own concrete HTTP, AMQP, Mongo or logging implementations. Use optional adapters for those concerns. +## Constructor Options + +```ts +const kernel = new Kernel({ + logger, + servicesYamlPath: 'config/container/services.yaml', + sourceDirectory: 'src', +}); +``` + +| Option | Default | Purpose | +| ------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| `di` | Created by `kernel.dependencyInjection()` | Provide an already configured `DependencyInjection` instance. Useful in tests or custom bootstrap code. | +| `logger` | `new ConsoleKernelLogger()` | Logger exposed as `kernel.logger` and `Kernel.logger`. Any `KernelLogger` implementation can be used. | +| `servicesYamlPath` | `config/container/services.yaml` | Default container file used by `kernel.dependencyInjection()`. | +| `sourceDirectory` | `src` | Default source tree scanned when the container is generated. | + +`servicesYamlPath` and `sourceDirectory` can also be passed directly to +`kernel.dependencyInjection(...)`; the method-level value wins. + +## Dependency Injection + +Most applications call this once during startup: + +```ts +await kernel.dependencyInjection({ + containerBuild: process.env.NODE_ENV === 'production', + overrides: [], +}); +``` + +| Option | Default | Purpose | +| ------------------ | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| `containerBuild` | `process.env.CONTAINER_BUILD === 'true'` | Regenerate `services.yaml` from source classes when `true`; load the existing YAML when `false`. | +| `overrides` | `[]` | Replace a token with `useClass`, `useValue` or `useFactory`. | +| `servicesYamlPath` | Constructor option or `config/container/services.yaml` | Container YAML path for this compile. | +| `sourceDirectory` | Constructor option or `src` | Source tree used when generating the container. | + +Prefer constructor injection in application classes. Use overrides at bootstrap +or test setup to choose infrastructure. + +## Registration API + +```ts +kernel.registerRoutes(GetUserByIdRoute); +kernel.registerConsumers(RegisterUserWhenCreated); +kernel.registerSchedulers(UserCleanupScheduler); +kernel.registerConsumerMiddleware(retryMiddleware, tracingMiddleware); +kernel.registerShutdownHook(() => server.close()); +``` + +Class registration resolves through `Kernel.di`. Instance registration is +available for consumers and schedulers: + +```ts +kernel.registerConsumerInstances(consumer); +kernel.registerSchedulerInstances(scheduler); +``` + +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 +explicitly: + +```ts +await kernel.runRuntimes(HttpRuntime); +kernel.registerShutdownHook(() => server.close()); +``` + +## Running And Shutdown + +```ts +await kernel.runInitializers(); +await kernel.runConsumers(); +await kernel.runSchedulers(); +await kernel.runRuntimes(); + +process.once('SIGTERM', () => { + void kernel.shutdown(); +}); +``` + +`shutdown()` stops registered consumers, schedulers, runtimes and explicit +shutdown hooks. It also supports common resource methods such as `close`, +`stop`, `flush` and `shutdown`. + ## Import ```ts diff --git a/docs/reference/route.md b/docs/reference/route.md index cb11521..e4259da 100644 --- a/docs/reference/route.md +++ b/docs/reference/route.md @@ -6,8 +6,19 @@ Base class for HTTP routes. import Route from '@haskou/ddd-kernel/adapters/ui/routes'; export default class GetUserRoute extends Route { - private readonly finder = this.get(UserFinder); + constructor(private readonly finder: UserFinder) { + super(); + } } ``` -`get()` resolves services through `Kernel.di`. +Routes are registered with the kernel and resolved through constructor +injection: + +```ts +kernel.registerRoutes(GetUserRoute); +``` + +`Route` still exposes `get()` for compatibility with older code, but +constructor injection is the recommended path because it keeps routes easier to +test.