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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ jobs:
- name: Run typecheck
run: yarn typecheck

- name: Run tests
run: yarn test

- name: Run tests with coverage
run: yarn build && yarn c8 --all --src src --include "src/**/*.ts" --extension .ts --exclude-after-remap --reporter text --reporter lcov --check-coverage=false node --test "tests/**/*.test.mjs"
run: yarn test:coverage

- name: Build docs
run: yarn docs:build
Expand Down
160 changes: 47 additions & 113 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,142 +1,76 @@
# @haskou/ddd-kernel

[![CI](https://github.com/haskou/ddd-kernel/actions/workflows/ci.yml/badge.svg)](https://github.com/haskou/ddd-kernel/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/haskou/ddd-kernel/branch/main/graph/badge.svg)](https://codecov.io/gh/haskou/ddd-kernel)
[![npm](https://img.shields.io/npm/v/@haskou/ddd-kernel.svg)](https://www.npmjs.com/package/@haskou/ddd-kernel)
[![license](https://img.shields.io/npm/l/@haskou/ddd-kernel.svg)](LICENSE)

## Status

This package is currently in early 0.x development. It is used to extract reusable DDD infrastructure from real TypeScript services, but public APIs may still change while the kernel is being hardened.

# @haskou/ddd-kernel

Framework-agnostic DDD kernel for TypeScript applications and microservices.

The expected startup pattern mirrors `pigeon-swarm-node`: application classes
are exported as `default`, `node-dependency-injection` generates or loads
`services.yaml`, and the kernel resolves consumers, schedulers, initializers and
runtimes through the configured container. Constructor injection is the normal
path for application services; `Kernel.di` and `this.get()` exist for framework
boundaries and backwards compatibility.

```ts
import { applicationConsumers } from './apps/ApplicationConsumers.js';
import { applicationInitializers } from './apps/ApplicationInitializers.js';
import { applicationRuntimes } from './apps/ApplicationRuntimes.js';
import { recurringSchedulers } from './apps/ApplicationSchedulers.js';
import { Kernel } from '@haskou/ddd-kernel';

const kernel = new Kernel();

await kernel.dependencyInjection();

kernel.registerConsumers(...applicationConsumers);
await kernel.runInitializers(...applicationInitializers);
await kernel.runConsumers();

kernel.registerSchedulers(...recurringSchedulers);
await kernel.runSchedulers();

await kernel.runRuntimes(...applicationRuntimes);
```
`@haskou/ddd-kernel` provides the runtime foundation shared by services that
are built around aggregates, domain events, consumers, schedulers and explicit
composition roots. The package keeps application bootstrapping consistent while
leaving transport, persistence and logging choices behind replaceable adapters.

Normal application code should not register factories manually. Application
services, repositories, adapters, schedulers, runtimes and consumers should be
default-exported classes so the autowire flow can resolve them.
## Scope

## Imports
The core package is responsible for application lifecycle and dependency
composition. It includes contracts and primitives for:

```ts
import { Kernel } from '@haskou/ddd-kernel';
import { DependencyInjection } from '@haskou/ddd-kernel/dependency-injection';
import { AggregateRoot, DomainEvent } from '@haskou/ddd-kernel/domain';
import { Scheduler } from '@haskou/ddd-kernel/scheduler';
```
- dependency injection and service resolution
- startup and graceful shutdown hooks
- consumers and consumer middleware
- schedulers and scheduler error policies
- runtimes
- domain events and aggregate roots
- repositories and pub/sub contracts
- logging contracts

Optional adapters:
The kernel does not own HTTP, AMQP, MongoDB, WebSocket or logger implementation
details. Those integrations are exposed as optional adapters, so applications
only depend on the infrastructure they actually use.

```ts
import { InMemoryRepository } from '@haskou/ddd-kernel/adapters/db/in-memory';
import { MongoRepository } from '@haskou/ddd-kernel/adapters/db/mongo';
import { InMemoryPubSub } from '@haskou/ddd-kernel/adapters/pubsub/in-memory';
import { ExpressKernelServer } from '@haskou/ddd-kernel/adapters/ui/express';
```
## Architecture

Contracts:
The package separates stable contracts from concrete infrastructure:

```ts
import type { Repository, UnitOfWork } from '@haskou/ddd-kernel/contracts/db';
import type { Consumer, PubSub } from '@haskou/ddd-kernel/contracts/pubsub';
```
- `@haskou/ddd-kernel` contains the kernel, lifecycle, DI integration and domain
primitives.
- `@haskou/ddd-kernel/adapters/*` contains optional adapter entrypoints.
- Applications register their own consumers, schedulers, runtimes and adapters
at the composition root.

UI helpers:
Constructor injection is the preferred application pattern. Direct service
lookup remains available for compatibility and integration boundaries, but it is
not the primary dependency model.

```ts
import Route from '@haskou/ddd-kernel/adapters/ui/routes';
import { HttpRouteStatusEnum } from '@haskou/ddd-kernel/contracts/ui';
```
## Stability

## Dependency Injection
This project is still in the `0.x` line. The current API is intentionally small
and covered by tests, but breaking changes may still happen while the kernel is
being extracted and hardened from production service patterns.

Default setup:

```ts
const kernel = new Kernel();
await kernel.dependencyInjection();
```

This uses:

- `src` as the source directory.
- `config/container/services.yaml` as the generated or loaded container file.
- `CONTAINER_BUILD=true` to regenerate the YAML through autowire.

Override paths when needed:

```ts
const kernel = new Kernel({
servicesYamlPath: 'config/container/services.yaml',
sourceDirectory: 'src',
});
```

Prefer constructor injection for consumers, schedulers, routes, repositories and
application services:

```ts
export default class RegisterUserWhenCreated {
constructor(private readonly finder: UserByIdFinder) {}
}
```

Use `Kernel.di.getService(...)` only at the composition boundary or in legacy
code that still extends a base class exposing `this.get()`.

## Example
## Documentation

See `example/` for a small route/application/domain setup that imports the
package through `file:..`.
Usage guides, adapter authoring notes and API reference pages are published at:

```bash
cd example
yarn install
yarn typecheck
yarn build
```
https://haskou.github.io/ddd-kernel/

## Documentation
The README is intentionally limited to project orientation. Installation,
startup, DI, AMQP, routes and adapter examples live in the documentation site.

Full documentation is available at **https://haskou.github.io/ddd-kernel/**
## Release Branches

The documentation includes installation, quick start, examples, error handling, serialization notes, and one reference page per exported class.
CI publishes npm versions from pull requests merged into the default branch
according to the source branch prefix:

## Scripts
| Branch prefix | npm version bump |
| --- | --- |
| `fix/*` | Patch |
| `feat/*` | Minor |
| `break/*` | Major |

```bash
yarn lint
yarn typecheck
yarn build
```
Other branch names run validation only and do not publish.

## License

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,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\" --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/**/*.ts\" --extension .ts --exclude-after-remap --reporter text --reporter lcov node --test \"tests/**/*.test.mjs\"",
Comment thread
haskou marked this conversation as resolved.
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [
Expand Down
1 change: 1 addition & 0 deletions tests/adapters/pubsub/amqp/AmqpMessageBusAdapter.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ test('consumes DLX messages with success, nack and no-message paths', async () =
channel.getMessages = [false, successMessage, failingMessage];

await adapter.consumeDlx('queue', TestDomainEvent, async () => {}, 3);
channel.eventHandlers.get('error')();

channel.messageCount = 0;

Expand Down
27 changes: 27 additions & 0 deletions tests/adapters/ui/routes/Route.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import assert from 'node:assert/strict';
import test from 'node:test';

import { Kernel } from '../../../../dist/index.js';
import { Route } from '../../../../dist/adapters/ui/routes/index.js';

class TestRoute extends Route {}

test('resolves legacy route services through the active kernel container', () => {
class Service {}

const service = new Service();

new Kernel({
di: {
getService(requestedService) {
assert.equal(requestedService, Service);

return service;
},
},
});

const route = new TestRoute();

assert.equal(route.get(Service), service);
});
5 changes: 5 additions & 0 deletions tests/kernel.test.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
import { mkdtemp, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { tmpdir } from 'node:os';
Expand Down Expand Up @@ -77,6 +78,10 @@ test('runs shutdown hooks after consumers and schedulers', async () => {
});

test('can be imported from CommonJS output', () => {
if (!existsSync(new URL('../dist/index.cjs', import.meta.url))) {
return;
}

const require = createRequire(import.meta.url);
const cjsPackage = require('../dist/index.cjs');

Expand Down
Loading