hexok 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entity.d.ts +6 -2
- package/dist/entity.d.ts.map +1 -1
- package/dist/entity.js.map +1 -1
- package/dist/event-handler.d.ts +4 -0
- package/dist/event-handler.d.ts.map +1 -1
- package/dist/event-handler.js +4 -0
- package/dist/event-handler.js.map +1 -1
- package/dist/use-case.d.ts +3 -3
- package/dist/use-case.js +3 -3
- package/package.json +1 -1
- package/skills/hexok/SKILL.md +5 -4
- package/skills/hexok/references/adapter.md +2 -2
- package/skills/hexok/references/entity.md +23 -1
- package/skills/hexok/references/event-catalog.md +2 -0
- package/skills/hexok/references/event-handler.md +10 -8
- package/skills/hexok/references/mapper.md +3 -3
- package/skills/hexok/references/port.md +8 -6
- package/skills/hexok/references/use-case.md +17 -13
package/dist/entity.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type InferSchema, type ResolvedSchema, type SchemaSource } from './schema.js';
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Recursively readonly view of entity props. `Date` values stay `Date`.
|
|
4
|
+
* Primitives are returned as-is, so a brand (`string & { … }`) is not
|
|
5
|
+
* mapped as an object and dropped.
|
|
6
|
+
*/
|
|
7
|
+
export type DeepReadonly<T> = T extends string | number | boolean | bigint | symbol | null | undefined ? T : T extends Date ? T : T extends readonly (infer U)[] ? ReadonlyArray<DeepReadonly<U>> : T extends object ? {
|
|
4
8
|
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
5
9
|
} : T;
|
|
6
10
|
/** `keyof object` is `never`; widen so an open props type still has string keys. */
|
package/dist/entity.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;AAMrB
|
|
1
|
+
{"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"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"}
|
package/dist/entity.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity.js","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EACL,aAAa,EACb,UAAU,EACV,cAAc,EACd,aAAa,GACd,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAIL,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,MAAM,kBAAkB,GAAa,MAAM,CAAC,MAAM,CAAC,EAAE,CAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"entity.js","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"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"}
|
package/dist/event-handler.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ type EventInstanceOf<Event> = Event extends abstract new (...args: never[]) => i
|
|
|
16
16
|
export declare function EventHandler<const Token extends string, E extends EventConstructor>(token: Token, event: E): (abstract new () => {
|
|
17
17
|
/** Nominal marker. Each `EventHandler(...)` call is a distinct class. */
|
|
18
18
|
readonly "__#private@#brand": true;
|
|
19
|
+
/** Handler name. Same literal as the static token. */
|
|
20
|
+
readonly token: Token;
|
|
21
|
+
/** Event class this handler receives. Same class as the static event. */
|
|
22
|
+
readonly event: E;
|
|
19
23
|
/**
|
|
20
24
|
* Required. Annotate `event` with the event class passed to
|
|
21
25
|
* {@link EventHandler}. Return `void` or `Promise<void>`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-handler.d.ts","sourceRoot":"","sources":["../src/event-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,KAAK,eAAe,CAAC,KAAK,IAAI,KAAK,SAAS,QAAQ,MAClD,GAAG,IAAI,EAAE,KAAK,EAAE,KACb,MAAM,QAAQ,GACf,QAAQ,GACR,KAAK,CAAC;AAEV;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,KAAK,CAAC,KAAK,SAAS,MAAM,EAC1B,CAAC,SAAS,gBAAgB,EAC1B,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAQpB,yEAAyE;kCACvD,IAAI;
|
|
1
|
+
{"version":3,"file":"event-handler.d.ts","sourceRoot":"","sources":["../src/event-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,KAAK,eAAe,CAAC,KAAK,IAAI,KAAK,SAAS,QAAQ,MAClD,GAAG,IAAI,EAAE,KAAK,EAAE,KACb,MAAM,QAAQ,GACf,QAAQ,GACR,KAAK,CAAC;AAEV;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,KAAK,CAAC,KAAK,SAAS,MAAM,EAC1B,CAAC,SAAS,gBAAgB,EAC1B,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAQpB,yEAAyE;kCACvD,IAAI;IACtB,sDAAsD;oBACtC,KAAK;IACrB,yEAAyE;oBACzD,CAAC;IAMjB;;;OAGG;uCAC+B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;;IAnBtD,+EAA+E;oBACxD,KAAK;IAC5B,yCAAyC;oBAClB,CAAC;EAoB3B"}
|
package/dist/event-handler.js
CHANGED
|
@@ -19,6 +19,10 @@ export function EventHandler(token, event) {
|
|
|
19
19
|
static event = event;
|
|
20
20
|
/** Nominal marker. Each `EventHandler(...)` call is a distinct class. */
|
|
21
21
|
#brand = true;
|
|
22
|
+
/** Handler name. Same literal as the static token. */
|
|
23
|
+
token = token;
|
|
24
|
+
/** Event class this handler receives. Same class as the static event. */
|
|
25
|
+
event = event;
|
|
22
26
|
constructor() {
|
|
23
27
|
void this.#brand;
|
|
24
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-handler.js","sourceRoot":"","sources":["../src/event-handler.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAG1B,KAAY,EAAE,KAAQ;IAGtB,MAAe,YAAY;QACzB,+EAA+E;QAC/E,MAAM,CAAU,KAAK,GAAU,KAAK,CAAC;QACrC,yCAAyC;QACzC,MAAM,CAAU,KAAK,GAAM,KAAK,CAAC;QACjC,yEAAyE;QAChE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"event-handler.js","sourceRoot":"","sources":["../src/event-handler.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAG1B,KAAY,EAAE,KAAQ;IAGtB,MAAe,YAAY;QACzB,+EAA+E;QAC/E,MAAM,CAAU,KAAK,GAAU,KAAK,CAAC;QACrC,yCAAyC;QACzC,MAAM,CAAU,KAAK,GAAM,KAAK,CAAC;QACjC,yEAAyE;QAChE,MAAM,GAAG,IAAI,CAAC;QACvB,sDAAsD;QAC7C,KAAK,GAAU,KAAK,CAAC;QAC9B,yEAAyE;QAChE,KAAK,GAAM,KAAK,CAAC;QAE1B;YACE,KAAK,IAAI,CAAC,MAAM,CAAC;QACnB,CAAC;;IASH,OAAO,YAAY,CAAC;AACtB,CAAC"}
|
package/dist/use-case.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* ```ts
|
|
8
8
|
* class CreateUser extends UseCase('user.create') {
|
|
9
|
-
* constructor(private readonly users: UserRepository) { super() }
|
|
9
|
+
* constructor(private readonly deps: { users: UserRepository }) { super() }
|
|
10
10
|
* async execute(input: { email: string }): Promise<{ id: string }> {
|
|
11
11
|
* return { id: input.email }
|
|
12
12
|
* }
|
|
@@ -65,12 +65,12 @@ type Statics<S> = Omit<S, ReservedKey>;
|
|
|
65
65
|
* input: z.object({ query: z.string() }),
|
|
66
66
|
* permission: 'users.read',
|
|
67
67
|
* }) {
|
|
68
|
-
* constructor(private readonly users: UserRepository) { super() }
|
|
68
|
+
* constructor(private readonly deps: { users: UserRepository }) { super() }
|
|
69
69
|
* async execute(
|
|
70
70
|
* ctx: UseCase.Ctx<typeof ApiUseCase>,
|
|
71
71
|
* input: { query: string },
|
|
72
72
|
* ): Promise<User[]> {
|
|
73
|
-
* return this.users.search(input.query)
|
|
73
|
+
* return this.deps.users.search(input.query)
|
|
74
74
|
* }
|
|
75
75
|
* }
|
|
76
76
|
* ```
|
package/dist/use-case.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* ```ts
|
|
8
8
|
* class CreateUser extends UseCase('user.create') {
|
|
9
|
-
* constructor(private readonly users: UserRepository) { super() }
|
|
9
|
+
* constructor(private readonly deps: { users: UserRepository }) { super() }
|
|
10
10
|
* async execute(input: { email: string }): Promise<{ id: string }> {
|
|
11
11
|
* return { id: input.email }
|
|
12
12
|
* }
|
|
@@ -63,12 +63,12 @@ const RESERVED_STATICS = new Set([
|
|
|
63
63
|
* input: z.object({ query: z.string() }),
|
|
64
64
|
* permission: 'users.read',
|
|
65
65
|
* }) {
|
|
66
|
-
* constructor(private readonly users: UserRepository) { super() }
|
|
66
|
+
* constructor(private readonly deps: { users: UserRepository }) { super() }
|
|
67
67
|
* async execute(
|
|
68
68
|
* ctx: UseCase.Ctx<typeof ApiUseCase>,
|
|
69
69
|
* input: { query: string },
|
|
70
70
|
* ): Promise<User[]> {
|
|
71
|
-
* return this.users.search(input.query)
|
|
71
|
+
* return this.deps.users.search(input.query)
|
|
72
72
|
* }
|
|
73
73
|
* }
|
|
74
74
|
* ```
|
package/package.json
CHANGED
package/skills/hexok/SKILL.md
CHANGED
|
@@ -60,7 +60,7 @@ A blog post is an id, a title, a body, and whether it is published. The schema i
|
|
|
60
60
|
|
|
61
61
|
Define each use case as one action, and the ports that action calls, when they are not already there.
|
|
62
62
|
|
|
63
|
-
Saving a post is a port. Publishing an event is a different port. The use case receives both in its constructor and calls them from `execute`. It does not import a driver. Read [references/use-case.md](references/use-case.md) and [references/port.md](references/port.md).
|
|
63
|
+
Saving a post is a port. Publishing an event is a different port. The use case receives both on a `deps` object in its constructor and calls them from `execute` through `this.deps`. Add a second argument, `config`, only when the composition root sets flags on that long-lived instance. It does not import a driver. Read [references/use-case.md](references/use-case.md) and [references/port.md](references/port.md).
|
|
64
64
|
|
|
65
65
|
Use `UseCase(token)` when `execute` only needs the input. Use `UseCase.context<Ctx>()` when every call in the family carries the same extra facts, such as a trace id, the actor, or the caller IP. Share one family across the gateway's use cases.
|
|
66
66
|
|
|
@@ -74,7 +74,7 @@ When the story has no consequence beyond its own write, this step is `none`. Do
|
|
|
74
74
|
|
|
75
75
|
### 3.c. Event handlers
|
|
76
76
|
|
|
77
|
-
When step 3.b named a side effect, implement the handler if it does not exist. The handler takes its own ports in the constructor. `handle` receives the event. Emailing subscribers loads addresses through a subscriber port and sends mail through a mail port. Read [references/event-handler.md](references/event-handler.md).
|
|
77
|
+
When step 3.b named a side effect, implement the handler if it does not exist. The handler takes its own ports on a `deps` object in the constructor. `handle` receives the event. Emailing subscribers loads addresses through a subscriber port and sends mail through a mail port. Read [references/event-handler.md](references/event-handler.md).
|
|
78
78
|
|
|
79
79
|
The handler does not subscribe itself. Subscription belongs to the adapter in step 4.
|
|
80
80
|
|
|
@@ -96,7 +96,7 @@ Use the HTTP library the application already depends on. Express, Hono, and Koa
|
|
|
96
96
|
|
|
97
97
|
The route builds the call context, calls `execute`, and maps failures to the reply. A missing entity is a 404. `CodedError` code `VALIDATION` is a 400 and `data.issues` names the fields. A catalog conflict, such as an already published post, is a 409. The catalog does not contain status codes.
|
|
98
98
|
|
|
99
|
-
Construct adapters, call `start`, and construct use cases in the entry point. A use case instance is long-lived. Context is an argument to `execute`, not a field set per request.
|
|
99
|
+
Construct adapters, call `start`, and construct use cases in the entry point (`new PublishPost({ posts, events })`). A use case instance is long-lived. Context is an argument to `execute`, not a field set per request.
|
|
100
100
|
|
|
101
101
|
### 6. Where handlers run
|
|
102
102
|
|
|
@@ -110,6 +110,7 @@ Do not add a broker only to create a second process. Do not move the handler's w
|
|
|
110
110
|
|
|
111
111
|
## Conventions
|
|
112
112
|
|
|
113
|
+
- A use case, an event handler, and an adapter that takes a driver receive collaborators as one `readonly` object named `deps`, including a class with a single collaborator. Keys are the collaborator names. Write the core port first in the type. Call sites may pass keys in any order. The body uses `this.deps`. Optional instance settings are a second argument, `config?`, also `readonly`. Omit `config` when there are none. New ports go on `deps`. New flags go on `config`. Do not add a third positional argument. A per-request choice stays on `execute`'s input. A value declared for a whole `UseCase.context` family stays on that factory's static spec. Entity payloads, event payloads, error-catalog factories, and port methods stay data arguments.
|
|
113
114
|
- Tokens are factory arguments: `Entity('Post', PostSchema)`, `UseCase('post.publish')`, `Port('PostRepository')`, `Event('post.published', PostSchema)`, `EventCatalog('blog', { postPublished })`, `EventHandler('email.subscribers', PostPublished)`, `Errors('blog', { ... })`. A subclass does not redeclare the token.
|
|
114
115
|
- `execute`, `handle`, port methods, and a mapper's `fromSource` and `toSource` are methods on the class. The compiler reports a missing one. `start`, `stop`, `parse`, and `set` already exist and may be overridden.
|
|
115
116
|
- A use case throws a catalog member when the story refuses. Schema checks throw `CodedError` with code `VALIDATION`. Catch both at the gateway or the worker.
|
|
@@ -119,7 +120,7 @@ Do not add a broker only to create a second process. Do not move the handler's w
|
|
|
119
120
|
- Publish `new PostPublished(payload)`. Compare events with `instanceof` or `event.token`. The catalog map key (`postPublished`) and the event token (`post.published`) are different strings.
|
|
120
121
|
- After a successful driver write, the adapter calls `commit()` on the entity. The mapper does not commit.
|
|
121
122
|
- Tests construct the use case with adapters of the same ports. A test of the story does not need a live broker.
|
|
122
|
-
- Put one concept in one file. A practical layout is `schemas
|
|
123
|
+
- Put one concept in one file. A practical layout is `domain/` (schemas, entities, errors, events), `app/` (ports, use cases, handlers), `infra/` (adapters), and `servers/` (the HTTP entry and a worker entry when step 6 splits a process).
|
|
123
124
|
- Zod, Valibot, and ArkType are passed straight to the primitives. A TypeBox 1 schematic goes through `@hexok/typebox` (`typebox(schema)` or `typeboxDecode(schema)`) before it reaches `Schema`, `Entity`, or `Event`.
|
|
124
125
|
|
|
125
126
|
## When a primitive changes
|
|
@@ -21,7 +21,7 @@ The use case asks a `PostRepository` to save. Something has to actually keep the
|
|
|
21
21
|
|
|
22
22
|
## How to use it
|
|
23
23
|
|
|
24
|
-
An in-memory repository is enough to walk the publish story. The API process can later substitute a database class that extends the same port. The composition root constructs the adapter, starts it, and hands it to `PublishPost`.
|
|
24
|
+
An in-memory repository is enough to walk the publish story. The API process can later substitute a database class that extends the same port. The composition root constructs the adapter, starts it, and hands it to `PublishPost` on `deps`. An adapter that takes a driver or another collaborator uses that same `deps` object.
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
27
|
class InMemoryPosts extends Adapter(PostRepository) {
|
|
@@ -41,7 +41,7 @@ class InMemoryPosts extends Adapter(PostRepository) {
|
|
|
41
41
|
|
|
42
42
|
const posts = new InMemoryPosts();
|
|
43
43
|
await posts.start();
|
|
44
|
-
const publishPost = new PublishPost(posts, events);
|
|
44
|
+
const publishPost = new PublishPost({ posts, events });
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
Store `toProps()` (or a mapped document) and rebuild with `restore` on the way out. Call `commit()` after the write succeeds.
|
|
@@ -46,6 +46,28 @@ Read `props` for the current data. Write only through `set` or a method that cal
|
|
|
46
46
|
|
|
47
47
|
The adapter calls `commit()` after the driver write succeeds. That is what clears `isNew` and the dirty set. The use case does not commit, and a mapper does not commit.
|
|
48
48
|
|
|
49
|
+
## Closing `set`
|
|
50
|
+
|
|
51
|
+
`set` stays public. The instance is an `EntityInstance`, so a private or protected `set` is a type error.
|
|
52
|
+
|
|
53
|
+
When every change belongs on a method, override `set` with a `never` producer and throw. Methods call `super.set`.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
class User extends Entity('User', UserSchema) {
|
|
57
|
+
override set(_producer: never): never {
|
|
58
|
+
throw new Error('User.set is closed');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
changeEmail(email: string): this {
|
|
62
|
+
return super.set((draft) => {
|
|
63
|
+
draft.email = email;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`user.set(...)` fails to typecheck. A value typed as `EntityInstance` can still name `set`. That call throws.
|
|
70
|
+
|
|
49
71
|
## API
|
|
50
72
|
|
|
51
73
|
`Entity(token, schema)` returns the class you extend. `token` is a string literal.
|
|
@@ -71,4 +93,4 @@ The adapter calls `commit()` after the driver write succeeds. That is what clear
|
|
|
71
93
|
|
|
72
94
|
A schema failure throws `CodedError` with code `VALIDATION` and the message `hexok: Post validation failed`. A domain rule throws a member of the error catalog.
|
|
73
95
|
|
|
74
|
-
`DeepReadonly<T>` is the type of `props`, `original`, and `toProps()`. `Date` values stay `Date`. `EntityInstance` is the props-only instance type.
|
|
96
|
+
`DeepReadonly<T>` is the type of `props`, `original`, and `toProps()`. `Date` values stay `Date`. A branded primitive stays that brand. `EntityInstance` is the props-only instance type.
|
|
@@ -52,6 +52,8 @@ A port parameter typed as the catalog class does not check event key or payload,
|
|
|
52
52
|
|
|
53
53
|
`parse` throws `hexok: catalog "blog" expected { key, payload }` when `value` lacks a string `key` or a `payload`. An unknown key throws `hexok: event "<key>" is not in catalog "blog"`. A payload the event schema rejects throws `CodedError` `VALIDATION` from that event's `parse`.
|
|
54
54
|
|
|
55
|
+
`examples/app-template` keeps two catalogs in the app. `DomainEventPublisher` calls `DomainEvents.message` and `@hexok/queue` carries that message. The worker calls `DomainEvents.parse` before `handle`. `ClientEventPublisher` calls `ClientEvents.message` and `@hexok/broadcast` carries that message. The websocket process calls `ClientEvents.parse` and sends the event to subscribed sockets. Neither package owns a catalog.
|
|
56
|
+
|
|
55
57
|
`EventInstance<typeof BlogEvents>` is the union of event instances in the catalog. Each instance carries its token. `EventMessage<typeof BlogEvents>` is the matching `{ key, payload }` union. The second type argument keeps one entry. `key` on a message is the map key. The event token is `BlogEvents.get(message.key).token`, and `event.token` on the instance.
|
|
56
58
|
|
|
57
59
|
Narrow a handler or a switch with `event.token` or `instanceof`. Indexing a handler map by `event.token` does not narrow the payload. A `never` default on the whole event reports an unhandled catalog entry.
|
|
@@ -19,16 +19,18 @@ The use case that publishes a post is finished once the post is saved and the ev
|
|
|
19
19
|
```ts
|
|
20
20
|
class EmailSubscribers extends EventHandler('email.subscribers', PostPublished) {
|
|
21
21
|
constructor(
|
|
22
|
-
private readonly
|
|
23
|
-
|
|
22
|
+
private readonly deps: {
|
|
23
|
+
subscribers: SubscriberDirectory;
|
|
24
|
+
mail: MailSender;
|
|
25
|
+
},
|
|
24
26
|
) {
|
|
25
27
|
super();
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
async handle(event: PostPublished): Promise<void> {
|
|
29
|
-
const addresses = await this.subscribers.list();
|
|
31
|
+
const addresses = await this.deps.subscribers.list();
|
|
30
32
|
for (const address of addresses) {
|
|
31
|
-
await this.mail.send(address, event.payload.title);
|
|
33
|
+
await this.deps.mail.send(address, event.payload.title);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
}
|
|
@@ -36,7 +38,7 @@ class EmailSubscribers extends EventHandler('email.subscribers', PostPublished)
|
|
|
36
38
|
|
|
37
39
|
Annotate `event` with `PostPublished`. An unannotated `handle(event)` is implicit `any`. Keep `handle` a method.
|
|
38
40
|
|
|
39
|
-
A process that shares the API's memory can pass the instance straight to `handle`. A worker in another process
|
|
41
|
+
A process that shares the API's memory can pass the instance straight to `handle`. A worker in another process can parse with `handler.event.parse` when the message is that event's payload, or read a catalog message, run `BlogEvents.parse`, and then call `handle`. Subscription, ack, and retry belong to that adapter. The instance `token` and `event` are the same values as the statics.
|
|
40
42
|
|
|
41
43
|
One handler is bound to one event class. A second event is a second handler.
|
|
42
44
|
|
|
@@ -48,9 +50,9 @@ A static field such as `groupId` is yours to read when you subscribe the handler
|
|
|
48
50
|
|
|
49
51
|
| Member | Role |
|
|
50
52
|
| --- | --- |
|
|
51
|
-
| `EmailSubscribers.token` | The name you passed in. |
|
|
52
|
-
| `EmailSubscribers.event` | The event class this handler receives. |
|
|
53
|
+
| `EmailSubscribers.token` | The name you passed in. The instance has the same `token`. |
|
|
54
|
+
| `EmailSubscribers.event` | The event class this handler receives. The instance has the same `event`. |
|
|
53
55
|
| `handle(event)` | Required. Annotate `event` with that class. Return `void` or `Promise<void>`. |
|
|
54
|
-
| `constructor` |
|
|
56
|
+
| `constructor` | Ports arrive on `deps`. Call `super()`. The composition root passes `new EmailSubscribers({ subscribers, mail })`. |
|
|
55
57
|
|
|
56
58
|
The compiler reports a missing `handle`. The factory records the token and the event class. It takes no consumer-group argument.
|
|
@@ -35,12 +35,12 @@ The domain port stays in domain types. Each adapter has its own stored shape. On
|
|
|
35
35
|
|
|
36
36
|
```ts
|
|
37
37
|
class MongoUsers extends Adapter(UserRepository) {
|
|
38
|
-
constructor(private readonly users: MongoUser) {
|
|
38
|
+
constructor(private readonly deps: { users: MongoUser }) {
|
|
39
39
|
super();
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
override async save(user: User): Promise<void> {
|
|
43
|
-
const doc = this.users.toModel(user);
|
|
43
|
+
const doc = this.deps.users.toModel(user);
|
|
44
44
|
await this.collection.updateOne({ _id: doc._id }, { $set: doc }, { upsert: true });
|
|
45
45
|
user.commit();
|
|
46
46
|
}
|
|
@@ -48,7 +48,7 @@ class MongoUsers extends Adapter(UserRepository) {
|
|
|
48
48
|
override async get(id: string): Promise<User | null> {
|
|
49
49
|
const doc = await this.collection.findOne({ _id: id });
|
|
50
50
|
if (doc === null) return null;
|
|
51
|
-
return this.users.fromModel(doc);
|
|
51
|
+
return this.deps.users.fromModel(doc);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
```
|
|
@@ -24,23 +24,25 @@ abstract class BlogEventPublisher extends Port('BlogEventPublisher') {
|
|
|
24
24
|
|
|
25
25
|
class PublishPost extends UseCase('post.publish') {
|
|
26
26
|
constructor(
|
|
27
|
-
private readonly
|
|
28
|
-
|
|
27
|
+
private readonly deps: {
|
|
28
|
+
posts: PostRepository;
|
|
29
|
+
events: BlogEventPublisher;
|
|
30
|
+
},
|
|
29
31
|
) {
|
|
30
32
|
super();
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
async execute(input: { id: string }): Promise<{ id: string }> {
|
|
34
|
-
const post = await this.posts.get(input.id);
|
|
36
|
+
const post = await this.deps.posts.get(input.id);
|
|
35
37
|
post.publish(new Date());
|
|
36
|
-
await this.posts.save(post);
|
|
37
|
-
await this.events.publish(new PostPublished(post.toProps()));
|
|
38
|
+
await this.deps.posts.save(post);
|
|
39
|
+
await this.deps.events.publish(new PostPublished(post.toProps()));
|
|
38
40
|
return { id: post.props.id };
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
The methods are whatever this seam needs. A repository offers `get` and `save`. A publisher offers `publish`. A mail port offers `send`. A subscription port offers `subscribe`. Keep publishing and subscribing as two ports. `start` and `stop` stay on the adapter. The use case depends on the port class
|
|
45
|
+
The methods are whatever this seam needs. A repository offers `get` and `save`. A publisher offers `publish`. A mail port offers `send`. A subscription port offers `subscribe`. Keep publishing and subscribing as two ports. `start` and `stop` stay on the adapter. The use case depends on the port class. The composition root passes adapters on `deps`: `new PublishPost({ posts, events })`.
|
|
44
46
|
|
|
45
47
|
Search for an existing port before adding one. A second seam is a second `Port(...)` call, even when the method names match.
|
|
46
48
|
|
|
@@ -19,23 +19,25 @@ Publishing a post is a story with a beginning and an end. Load the post, apply t
|
|
|
19
19
|
```ts
|
|
20
20
|
class PublishPost extends UseCase('post.publish') {
|
|
21
21
|
constructor(
|
|
22
|
-
private readonly
|
|
23
|
-
|
|
22
|
+
private readonly deps: {
|
|
23
|
+
posts: PostRepository;
|
|
24
|
+
events: BlogEventPublisher;
|
|
25
|
+
},
|
|
24
26
|
) {
|
|
25
27
|
super();
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
async execute(input: { id: string }): Promise<{ id: string }> {
|
|
29
|
-
const post = await this.posts.get(input.id);
|
|
31
|
+
const post = await this.deps.posts.get(input.id);
|
|
30
32
|
post.publish(new Date());
|
|
31
|
-
await this.posts.save(post);
|
|
32
|
-
await this.events.publish(new PostPublished(post.toProps()));
|
|
33
|
+
await this.deps.posts.save(post);
|
|
34
|
+
await this.deps.events.publish(new PostPublished(post.toProps()));
|
|
33
35
|
return { id: post.props.id };
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
The application constructs `PublishPost` with concrete adapters and calls `execute` from a gateway. One action is one class and one token. A caller who can save a draft and later publish it gets `SavePost` (`post.save`) and `PublishPost` (`post.publish`).
|
|
40
|
+
The application constructs `PublishPost` with concrete adapters (`new PublishPost({ posts, events })`) and calls `execute` from a gateway. One action is one class and one token. Pass every port on `deps`, including a use case with one port. Put the core port first in the type. Optional flags chosen for that long-lived instance are a second argument, `config`. A value the caller sends on one request stays on `execute`'s input. A value declared for the whole context family stays on the factory's static spec. A caller who can save a draft and later publish it gets `SavePost` (`post.save`) and `PublishPost` (`post.publish`).
|
|
39
41
|
|
|
40
42
|
Declare `execute` as a method. A class field, or an assignment after `super()`, is the wrong shape. The class is incomplete until `execute` exists, and the compiler reports the missing method.
|
|
41
43
|
|
|
@@ -49,7 +51,7 @@ Declare `execute` as a method. A class field, or an assignment after `super()`,
|
|
|
49
51
|
| --- | --- |
|
|
50
52
|
| `PublishPost.token` | The name you passed in. |
|
|
51
53
|
| `execute(input)` | Required. Annotate the input and the return type on the subclass. |
|
|
52
|
-
| `constructor` |
|
|
54
|
+
| `constructor` | Ports arrive on `deps`. Optional instance settings arrive on `config`. Call `super()`. |
|
|
53
55
|
|
|
54
56
|
A subclass of plain `UseCase(token)` takes one argument. Adding a context argument there is a type error. Reach for the family factory instead.
|
|
55
57
|
|
|
@@ -68,8 +70,10 @@ const ApiUseCase = UseCase.context<ApiContext>().guard((call) => {
|
|
|
68
70
|
|
|
69
71
|
class PublishPost extends ApiUseCase('post.publish') {
|
|
70
72
|
constructor(
|
|
71
|
-
private readonly
|
|
72
|
-
|
|
73
|
+
private readonly deps: {
|
|
74
|
+
posts: PostRepository;
|
|
75
|
+
events: BlogEventPublisher;
|
|
76
|
+
},
|
|
73
77
|
) {
|
|
74
78
|
super();
|
|
75
79
|
}
|
|
@@ -78,15 +82,15 @@ class PublishPost extends ApiUseCase('post.publish') {
|
|
|
78
82
|
ctx: UseCase.Ctx<typeof ApiUseCase>,
|
|
79
83
|
input: { id: string },
|
|
80
84
|
): Promise<{ id: string }> {
|
|
81
|
-
const post = await this.posts.get(input.id);
|
|
85
|
+
const post = await this.deps.posts.get(input.id);
|
|
82
86
|
post.publish(new Date());
|
|
83
|
-
await this.posts.save(post);
|
|
84
|
-
await this.events.publish(new PostPublished(post.toProps()));
|
|
87
|
+
await this.deps.posts.save(post);
|
|
88
|
+
await this.deps.events.publish(new PostPublished(post.toProps()));
|
|
85
89
|
return { id: post.props.id };
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
await new PublishPost(posts, events).execute({ traceId: 'req-1' }, { id: 'p1' });
|
|
93
|
+
await new PublishPost({ posts, events }).execute({ traceId: 'req-1' }, { id: 'p1' });
|
|
90
94
|
```
|
|
91
95
|
|
|
92
96
|
A member that needs a static contract, such as a permission string or an input schema the application will read, declares that contract as the second type argument. The schema is data the guard can read. Validating the input stays in the guard or in `execute`.
|