@ingenioz-it/caits 0.6.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingenioz-it/caits",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "CAITS (Clean Architecture In TypeScript): the technical base of a Marko project with a backend and a frontend.",
5
5
  "keywords": [
6
6
  "clean-architecture",
@@ -10,7 +10,13 @@
10
10
  /** Each fact that a rule points at has the number of its line in the file, from 1. */
11
11
  /** @typedef {{ name: string | null; kind: string; line: number }} Export an exported name, its kind, and its line */
12
12
  /** @typedef {{ name: string | null; readonly: boolean; value: string | null; asConst: boolean; line: number }} Property a property of a class */
13
- /** @typedef {{ name: string | null; exported: boolean; extends: string | null; implements: string[]; properties: Property[]; line: number }} Class */
13
+ /** @typedef {{ name: string | null; text: string }} TypeName a type that a signature names: its name when it is a name (`Note`), and its text */
14
+ /**
15
+ * A method of a class, and what it returns: each type of its return type, out of `Promise<…>` and of the unions (`Promise<A | B>` names A
16
+ * and B), or null when it has no return type.
17
+ * @typedef {{ name: string | null; returns: TypeName[] | null; line: number }} Method
18
+ */
19
+ /** @typedef {{ name: string | null; exported: boolean; extends: string | null; implements: string[]; properties: Property[]; methods: Method[]; line: number }} Class */
14
20
  /**
15
21
  * An item of `export default [ … ]`: the names that its chain calls, its text, the name that the first call takes first (`when(Event, …)`
16
22
  * takes Event), the class of each `new X(…)` in it, and its line.
@@ -120,8 +126,17 @@ function exportsOf(node, lineOf) {
120
126
  return [{ name: declaration.id?.name, kind: DECLARATION_KINDS[declaration.type] ?? declaration.type, line }];
121
127
  }
122
128
 
123
- /** @type {(node: Node, exported: boolean, lineOf: (node: Node) => number) => Class} */
124
- const classOf = (node, exported, lineOf) => ({
129
+ /** The types that a return type names: those of a union, and the type of a Promise. @type {(type: Node, text: string) => TypeName[]} */
130
+ const typeNamesOf = (type, text) => {
131
+ if (type.type === "TSUnionType") return type.types.flatMap((/** @type {Node} */ member) => typeNamesOf(member, text));
132
+ const reference = type.type === "TSTypeReference" && type.typeName.type === "Identifier" ? type.typeName.name : null;
133
+ const [promised, ...others] = (type.typeArguments ?? type.typeParameters)?.params ?? [];
134
+ if (reference === "Promise" && promised && others.length === 0) return typeNamesOf(promised, text);
135
+ return [{ name: reference, text: text.slice(type.start, type.end) }];
136
+ };
137
+
138
+ /** @type {(node: Node, exported: boolean, lineOf: (node: Node) => number, text: string) => Class} */
139
+ const classOf = (node, exported, lineOf, text) => ({
125
140
  name: node.id?.name ?? null,
126
141
  exported,
127
142
  extends: nameOf(node.superClass),
@@ -133,6 +148,11 @@ const classOf = (node, exported, lineOf) => ({
133
148
  asConst: member.value?.type === "TSAsExpression" && member.value.typeAnnotation?.typeName?.name === "const",
134
149
  line: lineOf(member)
135
150
  })),
151
+ methods: node.body.body.filter((/** @type {Node} */ member) => (member.type === "MethodDefinition" || member.type === "ClassMethod") && member.kind === "method" && !member.static && !member.computed)
152
+ .map((/** @type {Node} */ member) => {
153
+ const returnType = (member.value ?? member).returnType?.typeAnnotation;
154
+ return { name: keyOf(member), returns: returnType ? typeNamesOf(returnType, text) : null, line: lineOf(member) };
155
+ }),
136
156
  line: lineOf(node)
137
157
  });
138
158
 
@@ -142,7 +162,7 @@ const classOf = (node, exported, lineOf) => ({
142
162
  * - exports: each exported name, with its kind (class, interface, type, const, function, enum, specifier, default, all);
143
163
  * - defaultArray: the items of `export default [ … ]`, with the calls of each item (`bind(X).to(Y)` calls bind, to), the name that the first
144
164
  * call takes first (X), and the classes that the item builds, or null;
145
- * - classes: the classes, with what they extend and implement, and their properties;
165
+ * - classes: the classes, with what they extend and implement, their properties, and the return types of their methods;
146
166
  * - constructed: the class of each `new X(…)`;
147
167
  * - calls: each call, with the value of its arguments that are strings (null for the others);
148
168
  * - objectTypes: the value of each `type: "…"` of an object literal;
@@ -209,7 +229,7 @@ export function factsOf(program, text, comments = []) {
209
229
  });
210
230
  }
211
231
  }
212
- if (node.type === "ClassDeclaration") facts.classes.push(classOf(node, exportedClasses.has(node), lineOf));
232
+ if (node.type === "ClassDeclaration") facts.classes.push(classOf(node, exportedClasses.has(node), lineOf, text));
213
233
  if (node.type === "NewExpression") {
214
234
  facts.constructed.push(nameOf(node.callee));
215
235
  facts.lines.constructed.push(lineOf(node));
@@ -190,16 +190,18 @@ export function portOf(path) {
190
190
  }
191
191
 
192
192
  /**
193
- * An adapter, or its test: {context}/infra/{folder}/{file}. It implements the port named after its folder.
193
+ * An adapter, or its test: {context}/infra/{folder}/{file}. Its port is in the folder of the same name in the domain, domain/port/{folder}/:
194
+ * the module of that folder that is not its contract, whatever its name. The module is null when the domain has no such port.
194
195
  * @param {string} path
195
- * @returns {{ folder: string; test: boolean; port: string; portModule: string } | null}
196
+ * @param {Set<string>} paths
197
+ * @returns {{ folder: string; test: boolean; portModule: string | null } | null}
196
198
  */
197
- export function adapterOf(path) {
199
+ export function adapterOf(path, paths) {
198
200
  const match = /^(src\/(?:backend|frontend)\/[^/]+)\/infra\/([a-z0-9-]+)\/([^/]+)$/.exec(path);
199
201
  if (!match) return null;
200
202
  const [, context, folder, name] = match;
201
- const port = pascal(folder);
202
- return { folder, test: name.endsWith(".test.ts"), port, portModule: `${context}/domain/port/${folder}/${port}` };
203
+ const port = [...paths].sort().map(portOf).find(found => found !== null && !found.contract && found.module.startsWith(`${context}/domain/port/${folder}/`));
204
+ return { folder, test: name.endsWith(".test.ts"), portModule: port?.module ?? null };
203
205
  }
204
206
 
205
207
  /** The contexts whose frontend a file takes: `frontendOf($global, "notes")` takes the frontend of notes. @type {(file: File) => string[]} */
@@ -216,7 +218,7 @@ export const LISTS = { domain: "domain", application: "application", presentatio
216
218
 
217
219
  /**
218
220
  * Whether a list of quality/imports.mjs holds an import of a package: the package, the module or the Node API itself, or a folder of
219
- * modules that holds it (`@ingenioz-it/caits/messaging/*` holds `@ingenioz-it/caits/messaging/Input`).
221
+ * modules that holds it (`@ingenioz-it/caits/messaging/*` holds `@ingenioz-it/caits/messaging/EventPublisher`).
220
222
  * @param {string} specifier
221
223
  * @param {string[]} list
222
224
  */
@@ -3,8 +3,7 @@ import type { Token } from "../Token.js";
3
3
  import { BackendEventBus, type Published } from "../messaging/BackendEventBus.js";
4
4
  import { ContextMap, type Envelope, type Translation } from "../messaging/ContextMap.js";
5
5
  import { EventPublisher } from "../messaging/EventPublisher.js";
6
- import { InProcessMessageBus } from "../messaging/InProcessMessageBus.js";
7
- import type { Input } from "../messaging/Input.js";
6
+ import { InProcessMessageBus, UnknownInputError } from "../messaging/InProcessMessageBus.js";
8
7
  import { MessageBus } from "../messaging/MessageBus.js";
9
8
  import { ReportError, reportToConsole } from "../messaging/ReportError.js";
10
9
  import { bind, Container, type Binding, type Concrete } from "./Container.js";
@@ -40,11 +39,13 @@ export function createBackend(modules: BackendModules, options: BackendOptions =
40
39
  const owners = new Map<unknown, Context>();
41
40
  const listeners = new Set<(event: DomainEvent) => void>();
42
41
 
43
- const ownerOf = (owned: Class): Context => {
42
+ /** The context of an input DTO, or of a controller: the error of an input is that of the message bus, which names the fix. */
43
+ const ownerOf = (owned: Class, refusal: () => Error): Context => {
44
44
  const context = owners.get(owned);
45
- if (!context) throw new Error(`No context owns ${owned.name}`);
45
+ if (!context) throw refusal();
46
46
  return context;
47
47
  };
48
+ const ownerOfInput = (input: object): Context => ownerOf(input.constructor as Class, () => new UnknownInputError(input.constructor.name));
48
49
 
49
50
  /** The container of a request, and its message bus: each input DTO gets a new handler. */
50
51
  const requestOf = ({ container, useCases, shares }: Context, request: RequestContext) => {
@@ -58,8 +59,8 @@ export function createBackend(modules: BackendModules, options: BackendOptions =
58
59
  return { scope, bus };
59
60
  };
60
61
 
61
- const send = async <Output>(input: Input<Output>, request: RequestContext = SYSTEM): Promise<Output> =>
62
- requestOf(ownerOf(input.constructor as Class), request).bus.send(input);
62
+ const send = async (input: object, request: RequestContext = SYSTEM): Promise<unknown> =>
63
+ requestOf(ownerOfInput(input), request).bus.send(input);
63
64
 
64
65
  /**
65
66
  * An event that a handler publishes, in its context: the events of shared that the lines of its map that share give go to the shared
@@ -83,7 +84,7 @@ export function createBackend(modules: BackendModules, options: BackendOptions =
83
84
  event: line.event,
84
85
  inputs: (event: never, envelope: Envelope) => {
85
86
  const inputs = line.inputs(event, envelope);
86
- const foreign = inputs.find(input => ownerOf(input.constructor as Class) !== contexts.get(name));
87
+ const foreign = inputs.find(input => ownerOfInput(input) !== contexts.get(name));
87
88
  if (foreign) throw new Error(`the context map of ${name} gives ${foreign.constructor.name}, an input of another context: a context map gives the inputs of its own context`);
88
89
  return inputs;
89
90
  }
@@ -119,7 +120,8 @@ export function createBackend(modules: BackendModules, options: BackendOptions =
119
120
  send,
120
121
  /** The controller of a context, built for a request. */
121
122
  controller: <C>(controller: abstract new (...args: never[]) => C, request: RequestContext = SYSTEM): C =>
122
- requestOf(ownerOf(controller), request).scope.create(controller as unknown as Concrete<C>),
123
+ requestOf(ownerOf(controller, () => new Error(`No context has the controller ${controller.name}: write it in the folder of its use case, as <use-case>.<name>.controller.ts`)), request)
124
+ .scope.create(controller as unknown as Concrete<C>),
123
125
  /** Publishes an event of shared on the shared bus. */
124
126
  publish: (event: DomainEvent, request: RequestContext = SYSTEM) => { eventBus.publish(event, request); },
125
127
  redeliver: (published: Published) => { eventBus.redeliver(published.event, published.envelope); },
@@ -9,12 +9,14 @@
9
9
  */
10
10
 
11
11
  /**
12
- * The code that a module needs after its own, for the containers:
12
+ * The code that a module needs after its own, for the containers and the message buses:
13
13
  * - after each class that has a constructor, the dependencies of the constructor (DEPENDENCIES in Container.ts): the name of each
14
14
  * parameter, if it is optional, and the token of its type when the type has a value of the same name, declared in the module or
15
15
  * imported from a relative path, from a shortcut of the project (#…, which a package.json or a tsconfig.json maps to a folder) or from
16
16
  * one of the packages given (a port and its token, or a class: the kernel gives EventPublisher, DataDirectory…). The token is read only
17
17
  * when the container needs it, thus a cycle between modules does no harm. The name of the type goes with its token, for the errors;
18
+ * - after each class that has an execute method, the value of the type of its first parameter, found the same way (INPUT in
19
+ * discover.ts): the input DTO of a handler, which the message bus gives it;
18
20
  * - the name of each token declared without one: the name of its constant.
19
21
  * The names are text: the minifier can rename the code.
20
22
  * @param {string} code
@@ -31,6 +33,8 @@ export function declareDependencies(code, filename, parse, packages) {
31
33
  const values = new Set();
32
34
  /** @type {{ name: string; parameters: readonly ESTree.ParamPattern[] }[]} */
33
35
  const classes = [];
36
+ /** @type {{ name: string; input: ESTree.ParamPattern }[]} */
37
+ const executes = [];
34
38
  /** @type {string[]} */
35
39
  const tokens = [];
36
40
 
@@ -57,9 +61,12 @@ export function declareDependencies(code, filename, parse, packages) {
57
61
  values.add(declaration.id.name);
58
62
  const constructor = declaration.body.body.find(member => member.type === "MethodDefinition" && member.kind === "constructor");
59
63
  if (constructor?.type === "MethodDefinition") classes.push({ name: declaration.id.name, parameters: constructor.value.params });
64
+ const execute = declaration.body.body.find(member => member.type === "MethodDefinition" && member.kind === "method" && !member.static && !member.computed
65
+ && member.key.type === "Identifier" && member.key.name === "execute");
66
+ const input = execute?.type === "MethodDefinition" ? execute.value.params[0] : undefined;
67
+ if (input) executes.push({ name: declaration.id.name, input });
60
68
  }
61
69
  }
62
- if (classes.length === 0 && tokens.length === 0) return undefined;
63
70
 
64
71
  /** @type {Map<string, string>} */
65
72
  const namespaces = new Map();
@@ -75,8 +82,7 @@ export function declareDependencies(code, filename, parse, packages) {
75
82
  };
76
83
  /** @type {(parameter: ESTree.ParamPattern, index: number) => string} */
77
84
  const dependencyOf = (parameter, index) => {
78
- const inner = parameter.type === "TSParameterProperty" ? parameter.parameter : parameter;
79
- const pattern = inner.type === "AssignmentPattern" ? inner.left : inner;
85
+ const { inner, pattern } = partsOf(parameter);
80
86
  const name = pattern.type === "Identifier" ? pattern.name : `#${index}`;
81
87
  const typed = tokenOf(pattern.typeAnnotation?.typeAnnotation);
82
88
  const optional = inner.type === "AssignmentPattern" || pattern.optional === true;
@@ -84,9 +90,23 @@ export function declareDependencies(code, filename, parse, packages) {
84
90
  };
85
91
 
86
92
  const declarations = classes.map(({ name, parameters }) => `${name}[Symbol.for("kernel.dependencies")] = [${parameters.map(dependencyOf).join(", ")}];`);
93
+ const inputs = executes.flatMap(({ name, input }) => {
94
+ const typed = tokenOf(partsOf(input).pattern.typeAnnotation?.typeAnnotation);
95
+ return typed ? [`${name}[Symbol.for("kernel.input")] = () => ${typed.token};`] : [];
96
+ });
87
97
  const imports = [...namespaces].map(([source, alias]) => `import * as ${alias} from ${JSON.stringify(source)};`);
88
98
  const names = tokens.map(name => `${name}.name = ${JSON.stringify(name)};`);
89
- return [...imports, ...declarations, ...names].join("\n");
99
+ const lines = [...imports, ...declarations, ...inputs, ...names];
100
+ return lines.length > 0 ? lines.join("\n") : undefined;
101
+ }
102
+
103
+ /**
104
+ * A parameter, without what TypeScript adds around it (`private readonly`), and its pattern, without its default value.
105
+ * @param {ESTree.ParamPattern} parameter
106
+ */
107
+ function partsOf(parameter) {
108
+ const inner = parameter.type === "TSParameterProperty" ? parameter.parameter : parameter;
109
+ return { inner, pattern: inner.type === "AssignmentPattern" ? inner.left : inner };
90
110
  }
91
111
 
92
112
  /** @type {(name: ESTree.ModuleExportName) => string} */
@@ -1,5 +1,4 @@
1
1
  import type { Translation } from "../messaging/ContextMap.js";
2
- import { Input } from "../messaging/Input.js";
3
2
  import type { Binding } from "./Container.js";
4
3
 
5
4
  export type Class = abstract new (...args: never[]) => unknown;
@@ -21,6 +20,22 @@ export function single<T>(found: T[], what: string): T {
21
20
  return found[0];
22
21
  }
23
22
 
23
+ /** Where the plugin of the kernel writes the input DTO of a handler (see declareDependencies.js). */
24
+ export const INPUT = Symbol.for("kernel.input");
25
+
26
+ /**
27
+ * The use case of a handler: the handler, and the input DTO that its execute takes. The plugin of the kernel reads the class from the
28
+ * type of the parameter, as it reads the ports from the types of the constructor.
29
+ */
30
+ export function useCaseOf(path: string, module: Record<string, unknown>): UseCase {
31
+ const handler = single(classesOf(module), `${path} declares one handler`);
32
+ const input = (handler as { [INPUT]?: () => unknown })[INPUT]?.();
33
+ if (typeof input !== "function") {
34
+ throw new Error(`${path}: ${handler.name}.execute names no input DTO. Give its parameter the type of the input DTO, the class that the use case receives.`);
35
+ }
36
+ return { input: input as Class, handler };
37
+ }
38
+
24
39
  /**
25
40
  * The lines of a context map, as its file gives them: the kernel refuses, when it starts, a map that is not a list of lines, rather than
26
41
  * lose what it holds without a word. `isLine` tells a line of the side.
@@ -40,10 +55,10 @@ const isTranslation = ({ shares, event, inputs, events }: Partial<Record<string,
40
55
 
41
56
  /**
42
57
  * Turns the modules found in the backend folder (src/backend by default) into the contexts of the backend: a handler serves the input
43
- * DTO of its use-case folder, and the context map of a context gives the events of shared that it hears and those of its own that it
58
+ * DTO that its execute takes, and the context map of a context gives the events of shared that it hears and those of its own that it
44
59
  * shares.
45
60
  */
46
- export function discover(found: { bindings: Record<string, readonly Binding[]>; dtos: Modules; handlers: Modules; controllers: Modules; maps: Record<string, Translation[]> }): BackendModules {
61
+ export function discover(found: { bindings: Record<string, readonly Binding[]>; handlers: Modules; controllers: Modules; maps: Record<string, Translation[]> }): BackendModules {
47
62
  const contexts: Record<string, ContextModules> = {};
48
63
  const context = (path: string) => contexts[contextOf(path)] ??= { bindings: [], useCases: [], controllers: [], map: [] };
49
64
  let shared: readonly Binding[] = [];
@@ -52,12 +67,7 @@ export function discover(found: { bindings: Record<string, readonly Binding[]>;
52
67
  if (contextOf(path) === "shared") shared = bindings;
53
68
  else context(path).bindings = bindings;
54
69
  }
55
- for (const [path, module] of Object.entries(found.handlers)) {
56
- const dto = path.replace(".handler.ts", ".dto.ts");
57
- const input = single(classesOf(found.dtos[dto] ?? {}).filter(type => type.prototype instanceof Input), `${dto} declares one input DTO`);
58
- const handler = single(classesOf(module), `${path} declares one handler`);
59
- context(path).useCases.push({ input, handler });
60
- }
70
+ for (const [path, module] of Object.entries(found.handlers)) context(path).useCases.push(useCaseOf(path, module));
61
71
  for (const [path, module] of Object.entries(found.controllers)) context(path).controllers.push(...classesOf(module));
62
72
  for (const [path, map] of Object.entries(found.maps)) {
63
73
  if (contextOf(path) === "shared") throw new Error(`the backend context shared hears no other context: it has no context map (${path})`);
@@ -1,8 +1,7 @@
1
- import { Input } from "../messaging/Input.js";
2
1
  import type { Line } from "../messaging/FrontendContextMap.js";
3
2
  import type { StoreDefinition } from "../state/createStore.js";
4
3
  import type { Binding } from "./Container.js";
5
- import { classesOf, linesIn, single, type UseCase } from "./discover.js";
4
+ import { linesIn, useCaseOf, type UseCase } from "./discover.js";
6
5
 
7
6
  /**
8
7
  * A context of the frontend: its bindings, its use cases, its store (none for a context without a use case: its views and its
@@ -20,12 +19,11 @@ const isLine = ({ shares, event, events }: Partial<Record<string, unknown>>) =>
20
19
 
21
20
  /**
22
21
  * Turns the modules found in the frontend folder (src/frontend by default) into the contexts of the frontend: a context has a di.ts,
23
- * a store at most, and its context map if it hears the context shared or shares events with it; a handler serves the input DTO of its
24
- * use-case folder.
22
+ * a store at most, and its context map if it hears the context shared or shares events with it; a handler serves the input DTO that
23
+ * its execute takes.
25
24
  */
26
25
  export function discoverFrontends(found: {
27
26
  bindings: Record<string, readonly Binding[]>;
28
- dtos: Modules;
29
27
  handlers: Modules;
30
28
  stores: Record<string, StoreDefinition<unknown, unknown>>;
31
29
  maps: Record<string, readonly Line[]>;
@@ -46,9 +44,7 @@ export function discoverFrontends(found: {
46
44
  for (const [path, module] of Object.entries(found.handlers)) {
47
45
  const context = contexts[contextOf(path)];
48
46
  if (!context) throw new Error(`${path} belongs to a frontend without a di.ts`);
49
- const dto = path.replace(".handler.ts", ".dto.ts");
50
- const input = single(classesOf(found.dtos[dto] ?? {}).filter(type => type.prototype instanceof Input), `${dto} declares one input DTO`);
51
- context.useCases.push({ input, handler: single(classesOf(module), `${path} declares one handler`) });
47
+ context.useCases.push(useCaseOf(path, module));
52
48
  }
53
49
  for (const [name, context] of Object.entries(contexts)) {
54
50
  if (context.useCases.length > 0 && !context.store) {
@@ -1,6 +1,6 @@
1
1
  /// <reference path="../virtual-modules.d.ts" />
2
- import { bindings, dtos, handlers, controllers, maps } from "virtual:kernel/backend";
2
+ import { bindings, handlers, controllers, maps } from "virtual:kernel/backend";
3
3
  import { discover } from "./discover.js";
4
4
 
5
5
  /** Every context of the backend: its di.ts, its use cases, its controllers and its context map. The plugin of the kernel finds them. */
6
- export const discovered = discover({ bindings, dtos, handlers, controllers, maps });
6
+ export const discovered = discover({ bindings, handlers, controllers, maps });
@@ -1,6 +1,6 @@
1
1
  /// <reference path="../virtual-modules.d.ts" />
2
- import { bindings, dtos, handlers, maps, stores } from "virtual:kernel/frontend";
2
+ import { bindings, handlers, maps, stores } from "virtual:kernel/frontend";
3
3
  import { discoverFrontends } from "./discoverFrontends.js";
4
4
 
5
5
  /** Every context of the frontend: its di.ts, its use cases, its store and its context map. The plugin of the kernel finds them. */
6
- export const discoveredFrontends = discoverFrontends({ bindings, dtos, handlers, stores, maps });
6
+ export const discoveredFrontends = discoverFrontends({ bindings, handlers, stores, maps });
@@ -1,6 +1,5 @@
1
1
  import type { DomainEvent } from "../DomainEvent.js";
2
2
  import type { ContextMap, Envelope } from "./ContextMap.js";
3
- import type { Input } from "./Input.js";
4
3
  import type { ReportError } from "./ReportError.js";
5
4
 
6
5
  export type Published = { readonly event: DomainEvent; readonly envelope: Envelope };
@@ -18,7 +17,7 @@ export class BackendEventBus {
18
17
 
19
18
  constructor(private readonly options: {
20
19
  map: ContextMap;
21
- send: (input: Input<unknown>, envelope: Envelope) => Promise<unknown>;
20
+ send: (input: object, envelope: Envelope) => Promise<unknown>;
22
21
  nextEventId: () => string;
23
22
  reportError: ReportError;
24
23
  }) {}
@@ -48,7 +47,7 @@ export class BackendEventBus {
48
47
 
49
48
  private async deliver(event: DomainEvent, envelope: Envelope): Promise<void> {
50
49
  await nextTurn();
51
- let inputs: Input<unknown>[];
50
+ let inputs: object[];
52
51
  try {
53
52
  inputs = this.options.map.inputsFor(event, envelope);
54
53
  } catch (error) {
@@ -1,5 +1,4 @@
1
1
  import type { DomainEvent } from "../DomainEvent.js";
2
- import type { Input } from "./Input.js";
3
2
 
4
3
  /** What the shared bus adds to an event it carries: an id of its own, and the correlation id of the request that published it. */
5
4
  export type Envelope = { readonly eventId: string; readonly correlationId: string };
@@ -11,14 +10,14 @@ type EventClass<E> = abstract new (...args: never[]) => E;
11
10
  * input DTOs of the context, or an event of the context that it shares, which becomes events of the context shared.
12
11
  */
13
12
  export type Translation =
14
- | { readonly shares: false; readonly event: EventClass<DomainEvent>; readonly inputs: (event: never, envelope: Envelope) => Input<unknown>[] }
13
+ | { readonly shares: false; readonly event: EventClass<DomainEvent>; readonly inputs: (event: never, envelope: Envelope) => object[] }
15
14
  | { readonly shares: true; readonly event: EventClass<DomainEvent>; readonly events: (event: never) => DomainEvent[] };
16
15
 
17
16
  /**
18
17
  * A line that hears the context shared: when this event of shared comes on the shared bus, the handlers of the context execute these
19
18
  * input DTOs. A context map names only its own context and the context shared: two contexts never name each other.
20
19
  */
21
- export function when<E extends DomainEvent>(event: EventClass<E>, inputs: (event: E, envelope: Envelope) => Input<unknown>[]): Translation {
20
+ export function when<E extends DomainEvent>(event: EventClass<E>, inputs: (event: E, envelope: Envelope) => object[]): Translation {
22
21
  return { shares: false, event, inputs };
23
22
  }
24
23
 
@@ -35,7 +34,7 @@ export class ContextMap {
35
34
  constructor(private readonly translations: Translation[]) {}
36
35
 
37
36
  /** The input DTOs that the lines that hear this event of shared give, none when no line hears it. */
38
- inputsFor(event: DomainEvent, envelope: Envelope): Input<unknown>[] {
37
+ inputsFor(event: DomainEvent, envelope: Envelope): object[] {
39
38
  return this.translations.flatMap(translation => !translation.shares && event instanceof translation.event ? translation.inputs(event as never, envelope) : []);
40
39
  }
41
40
 
@@ -1,4 +1,3 @@
1
- import type { Input } from "./Input.js";
2
1
  import type { MessageBus } from "./MessageBus.js";
3
2
  import { reportToConsole, type ReportError } from "./ReportError.js";
4
3
 
@@ -68,7 +67,7 @@ export function problem(status: number, slug: string, title: string, detail?: st
68
67
  }
69
68
 
70
69
  /** Turns a request into an input DTO, sends it to the message bus of the request, and turns the output or the error into a response. */
71
- export abstract class Controller<Dto extends Input<Output>, Output> {
70
+ export abstract class Controller<Dto extends object, Output> {
72
71
  constructor(private bus: MessageBus, private reportError: ReportError = reportToConsole) {}
73
72
 
74
73
  protected abstract toDto(request: Request): Dto | Promise<Dto>;
@@ -92,7 +91,7 @@ export abstract class Controller<Dto extends Input<Output>, Output> {
92
91
  try {
93
92
  let output: Output;
94
93
  try {
95
- output = await this.bus.send(await this.toDto(request));
94
+ output = await this.bus.send(await this.toDto(request)) as Output;
96
95
  } catch (error) {
97
96
  return await this.toErrorResponse(error, request);
98
97
  }
@@ -1,12 +1,10 @@
1
- import type { Input } from "./Input.js";
2
1
  import type { MessageBus } from "./MessageBus.js";
3
2
 
4
- export type OutputOf<I> = I extends Input<infer Output> ? Output : never;
5
- type InputClass<I extends Input<unknown>> = abstract new (...args: never[]) => I;
3
+ type InputClass<I> = abstract new (...args: never[]) => I;
6
4
 
7
5
  export class UnknownInputError extends Error {
8
6
  constructor(readonly inputName: string) {
9
- super(`No handler subscribes to ${inputName}`);
7
+ super(`No handler takes ${inputName}: write one next to its DTO, in <use-case>.handler.ts, with execute(input: ${inputName})`);
10
8
  this.name = "UnknownInputError";
11
9
  }
12
10
  }
@@ -22,14 +20,14 @@ export class DuplicateHandlerError extends Error {
22
20
  export class InProcessMessageBus implements MessageBus {
23
21
  private handlers = new Map<unknown, (input: never) => unknown>();
24
22
 
25
- subscribe<I extends Input<unknown>>(input: InputClass<I>, handler: (input: I) => OutputOf<I> | Promise<OutputOf<I>>): void {
23
+ subscribe<I extends object>(input: InputClass<I>, handler: (input: I) => unknown): void {
26
24
  if (this.handlers.has(input)) throw new DuplicateHandlerError(input.name);
27
25
  this.handlers.set(input, handler);
28
26
  }
29
27
 
30
- async send<Output>(input: Input<Output>): Promise<Output> {
28
+ async send(input: object): Promise<unknown> {
31
29
  const handler = this.handlers.get(input.constructor);
32
30
  if (!handler) throw new UnknownInputError(input.constructor.name);
33
- return (await handler(input as never)) as Output;
31
+ return await handler(input as never);
34
32
  }
35
33
  }
@@ -1,12 +1,11 @@
1
1
  import { Token } from "../Token.js";
2
- import type { Input } from "./Input.js";
3
2
 
4
3
  /**
5
4
  * Where a controller or a store sends an input DTO: the message bus gives it to the handler of its class, and gives back the output of
6
5
  * the handler.
7
6
  */
8
7
  export interface MessageBus {
9
- send<Output>(input: Input<Output>): Promise<Output>;
8
+ send(input: object): Promise<unknown>;
10
9
  }
11
10
 
12
11
  export const MessageBus = new Token<MessageBus>();
@@ -1,5 +1,4 @@
1
1
  import type { EventSubscriber } from "../messaging/EventSubscriber.js";
2
- import type { Input } from "../messaging/Input.js";
3
2
  import type { MessageBus } from "../messaging/MessageBus.js";
4
3
  import { reportToConsole, type ReportError } from "../messaging/ReportError.js";
5
4
 
@@ -9,7 +8,7 @@ export interface StoreDefinition<S, E> {
9
8
  /** The state after the event. */
10
9
  reduce(state: S, event: E): S;
11
10
  /** The input DTO that the event asks the store to send to the message bus, or undefined. It reads the state before the event. */
12
- send(state: S, event: E): Input<unknown> | undefined;
11
+ send(state: S, event: E): object | undefined;
13
12
  }
14
13
 
15
14
  export interface Store<S> {
@@ -2,7 +2,7 @@ import { test as base } from "vitest";
2
2
  import { createTestApplication, type TestApplication } from "./applications.js";
3
3
 
4
4
  /** Everything an application test of the backend imports: the test of a use case, written as a scenario. */
5
- export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, vi } from "vitest";
5
+ export { afterAll, afterEach, assert, beforeAll, beforeEach, describe, expect, vi } from "vitest";
6
6
  export { applicationScenario } from "./applicationScenario.js";
7
7
  export type { TestApplication } from "./applications.js";
8
8
 
@@ -1,19 +1,18 @@
1
1
  import { expect } from "vitest";
2
2
  import type { DomainEvent } from "../../DomainEvent.js";
3
- import type { Input } from "../../messaging/Input.js";
4
3
  import type { TestApplication } from "./applications.js";
5
4
 
6
- type Result<Output> = { output: Output } | { error: unknown };
5
+ type Result = { output: unknown } | { error: unknown };
7
6
 
8
7
  /**
9
8
  * An application test, written as a presentation test: the input DTO, the events the handler publishes, then the output DTO or the error
10
9
  * that the message bus gives. The events are checked only when the test says which ones it expects.
11
10
  */
12
- export class ApplicationScenario<Output> {
11
+ export class ApplicationScenario {
13
12
  private readonly expected: DomainEvent[] = [];
14
13
  private checksEvents = false;
15
14
 
16
- constructor(private readonly app: TestApplication, private readonly dto: Input<Output>) {}
15
+ constructor(private readonly app: TestApplication, private readonly dto: object) {}
17
16
 
18
17
  expectItToPublish(event: DomainEvent): this {
19
18
  this.expected.push(event);
@@ -26,7 +25,7 @@ export class ApplicationScenario<Output> {
26
25
  return this;
27
26
  }
28
27
 
29
- async expectTheOutputToBe(output: Output): Promise<void> {
28
+ async expectTheOutputToBe(output: object): Promise<void> {
30
29
  const from = this.app.published.length;
31
30
  const result = await this.run();
32
31
  if ("error" in result) throw result.error;
@@ -42,7 +41,7 @@ export class ApplicationScenario<Output> {
42
41
  expect(result.error).toStrictEqual(error);
43
42
  }
44
43
 
45
- private async run(): Promise<Result<Output>> {
44
+ private async run(): Promise<Result> {
46
45
  try {
47
46
  return { output: await this.app.send(this.dto) };
48
47
  } catch (error) {
@@ -56,5 +55,5 @@ export class ApplicationScenario<Output> {
56
55
  }
57
56
 
58
57
  export const applicationScenario = (app: TestApplication) => ({
59
- givenTheDto: <Output>(dto: Input<Output>) => new ApplicationScenario(app, dto)
58
+ givenTheDto: (dto: object) => new ApplicationScenario(app, dto)
60
59
  });
@@ -5,7 +5,7 @@ import { createTestBackendApplication, type TestBackendApplication } from "./app
5
5
  * Everything a test of the context map imports. `test` checks what the `ContextMap` gives: the input DTOs for an event of shared
6
6
  * (`inputsFor`), the events of shared for an event of the context (`eventsFor`).
7
7
  */
8
- export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
8
+ export { afterAll, afterEach, assert, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
9
9
  export { ContextMap } from "../../messaging/ContextMap.js";
10
10
  export type { TestBackendApplication } from "./applications.js";
11
11
 
@@ -2,7 +2,7 @@ import { createBackend } from "../../di/createBackend.js";
2
2
  import { discovered } from "../../di/discovered.js";
3
3
 
4
4
  /** Everything an integration test imports: flows of the context map again, with the real infra implementations. */
5
- export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
5
+ export { afterAll, afterEach, assert, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
6
6
 
7
7
  /** The whole backend, built from the di.ts of the contexts only: the real infra implementations, as in production. */
8
8
  export const productionBackend = () => createBackend(discovered);
@@ -1,5 +1,4 @@
1
1
  import { expect } from "vitest";
2
- import type { Input } from "../../messaging/Input.js";
3
2
  import type { TestApplication } from "./applications.js";
4
3
 
5
4
  /**
@@ -10,7 +9,7 @@ import type { TestApplication } from "./applications.js";
10
9
  export class ApplicationScenario implements PromiseLike<void> {
11
10
  private readonly expected: object[] = [];
12
11
 
13
- constructor(private readonly app: TestApplication, private readonly dto: Input<void>) {}
12
+ constructor(private readonly app: TestApplication, private readonly dto: object) {}
14
13
 
15
14
  expectItToPublish(event: object): this {
16
15
  this.expected.push(event);
@@ -32,5 +31,5 @@ export class ApplicationScenario implements PromiseLike<void> {
32
31
  }
33
32
 
34
33
  export const applicationScenario = (app: TestApplication) => ({
35
- givenTheDto: (dto: Input<void>) => new ApplicationScenario(app, dto)
34
+ givenTheDto: (dto: object) => new ApplicationScenario(app, dto)
36
35
  });
@@ -4,7 +4,7 @@ import type { Binding } from "../../di/Container.js";
4
4
  import { createFrontendWithBus } from "../../di/createFrontend.js";
5
5
  import { discoveredFrontends } from "../../di/discoveredFrontends.js";
6
6
  import type { Fetcher } from "../../di/Fetcher.js";
7
- import type { Input } from "../../messaging/Input.js";
7
+ import { UnknownInputError } from "../../messaging/InProcessMessageBus.js";
8
8
  import type { Token } from "../../Token.js";
9
9
 
10
10
  /** The test.di.ts of each context that has one: the bindings that replace, in tests, those that reach outside the browser. */
@@ -37,7 +37,7 @@ export function createTestApplication() {
37
37
 
38
38
  const ownerOf = (input: object): string => {
39
39
  const name = Object.keys(discoveredFrontends).find(context => discoveredFrontends[context].useCases.some(({ input: type }) => type === input.constructor));
40
- if (name === undefined) throw new Error(`No context handles ${input.constructor.name}`);
40
+ if (name === undefined) throw new UnknownInputError(input.constructor.name);
41
41
  return name;
42
42
  };
43
43
 
@@ -50,7 +50,7 @@ export function createTestApplication() {
50
50
 
51
51
  return {
52
52
  /** Sends an input DTO to the message bus of the context that handles it. */
53
- send: <Output>(input: Input<Output>): Promise<Output> => frontend(ownerOf(input)).bus.send(input),
53
+ send: (input: object): Promise<unknown> => frontend(ownerOf(input)).bus.send(input),
54
54
  /** The dependency with this token, built now if no handler needed it before. */
55
55
  get: <T>(token: Token<T>): T => {
56
56
  const found = holders(token);