@sincpro/client-framework 1.0.0 → 1.1.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/AGENTS.md +53 -12
- package/MANIFESTO.md +56 -13
- package/README.md +329 -33
- package/dist/circuit_breaker/domain/circuit.d.ts +29 -0
- package/dist/circuit_breaker/domain/circuit.js +37 -0
- package/dist/circuit_breaker/entrypoint/circuit-breaker.d.ts +56 -0
- package/dist/circuit_breaker/entrypoint/circuit-breaker.js +94 -0
- package/dist/circuit_breaker/index.d.ts +9 -0
- package/dist/circuit_breaker/index.js +9 -0
- package/dist/client/adapters/json-rpc.d.ts +40 -0
- package/dist/client/adapters/json-rpc.js +97 -0
- package/dist/client/adapters/rest-operations.d.ts +19 -0
- package/dist/client/adapters/rest-operations.js +66 -0
- package/dist/client/domain/operation.d.ts +73 -0
- package/dist/client/domain/operation.js +19 -0
- package/dist/client/index.d.ts +5 -1
- package/dist/client/index.js +5 -1
- package/dist/client/services/guarded.d.ts +17 -0
- package/dist/client/services/guarded.js +19 -0
- package/dist/client/services/propagation.d.ts +20 -7
- package/dist/client/services/propagation.js +44 -15
- package/dist/codegen/entrypoint/cli.js +7 -7
- package/dist/codegen/services/client.d.ts +7 -8
- package/dist/codegen/services/client.js +50 -148
- package/dist/codegen/services/scaffold.d.ts +6 -3
- package/dist/codegen/services/scaffold.js +14 -13
- package/dist/common/failures.d.ts +83 -0
- package/dist/common/failures.js +212 -0
- package/dist/common/index.d.ts +1 -0
- package/dist/common/index.js +1 -0
- package/dist/context/domain/execution.d.ts +12 -0
- package/dist/core/domain/bus.d.ts +16 -7
- package/dist/core/domain/bus.js +14 -6
- package/dist/core/domain/dto.d.ts +32 -2
- package/dist/core/domain/dto.js +23 -0
- package/dist/core/domain/pipeline.d.ts +89 -0
- package/dist/core/domain/pipeline.js +1 -0
- package/dist/core/domain/services.d.ts +14 -4
- package/dist/core/domain/services.js +4 -1
- package/dist/core/entrypoint/bounded-context.d.ts +79 -10
- package/dist/core/entrypoint/bounded-context.js +207 -33
- package/dist/core/index.d.ts +4 -2
- package/dist/core/index.js +3 -2
- package/dist/core/infrastructure/listeners.d.ts +28 -0
- package/dist/core/infrastructure/listeners.js +34 -0
- package/dist/core/services/binding.d.ts +2 -2
- package/dist/core/services/binding.js +6 -3
- package/dist/core/services/execution.d.ts +37 -6
- package/dist/core/services/execution.js +86 -7
- package/dist/core/services/interceptors.d.ts +15 -0
- package/dist/core/services/interceptors.js +25 -0
- package/dist/core/services/listening.d.ts +17 -0
- package/dist/core/services/listening.js +25 -0
- package/dist/event_driven/adapters/memory-event-store.d.ts +10 -0
- package/dist/event_driven/adapters/memory-event-store.js +23 -0
- package/dist/event_driven/adapters/stored-queue.d.ts +67 -0
- package/dist/event_driven/adapters/stored-queue.js +192 -0
- package/dist/event_driven/adapters/sync-queue.d.ts +11 -0
- package/dist/event_driven/adapters/sync-queue.js +19 -0
- package/dist/event_driven/domain/envelope.d.ts +28 -0
- package/dist/event_driven/domain/envelope.js +19 -0
- package/dist/event_driven/domain/queue.d.ts +40 -0
- package/dist/event_driven/domain/queue.js +1 -0
- package/dist/event_driven/domain/store.d.ts +56 -0
- package/dist/event_driven/domain/store.js +1 -0
- package/dist/event_driven/entrypoint/publisher.d.ts +26 -0
- package/dist/event_driven/entrypoint/publisher.js +35 -0
- package/dist/event_driven/entrypoint/subscriber.d.ts +34 -0
- package/dist/event_driven/entrypoint/subscriber.js +51 -0
- package/dist/event_driven/index.d.ts +19 -0
- package/dist/event_driven/index.js +17 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +7 -2
- package/dist/observability/domain/logging.d.ts +2 -0
- package/dist/observability/services/logging.js +1 -0
- package/dist/observability/services/outcome.d.ts +6 -6
- package/dist/observability/services/outcome.js +10 -13
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OperationTransport } from "../domain/operation.js";
|
|
1
2
|
import type { Transport } from "../domain/transport.js";
|
|
2
3
|
import { type Execution } from "../../context/domain/execution.js";
|
|
3
4
|
/** Whether a value is a `Transport`: what a run binds to itself even without `BIND`. */
|
|
@@ -14,19 +15,31 @@ export declare function headersOf(execution: Execution): Record<string, string>;
|
|
|
14
15
|
*
|
|
15
16
|
* Context: the transport's own headers (`restTransport({ headers })`) are the session's — the
|
|
16
17
|
* token, the tenant — the same for every call; these are the use case's, different for two use
|
|
17
|
-
* cases running at once, so they travel per call (`CallOptions`).
|
|
18
|
+
* cases running at once, so they travel per call (`CallOptions`). A call that gives no signal of
|
|
19
|
+
* its own is cancelled with the run.
|
|
18
20
|
*/
|
|
19
21
|
export declare function bindTransport(transport: Transport, execution: Execution): Transport;
|
|
22
|
+
/**
|
|
23
|
+
* The operation transport, calling as `execution`: every call carries the run's trace and
|
|
24
|
+
* context as headers and is cancelled with the run — the caller's own headers and signal still
|
|
25
|
+
* win.
|
|
26
|
+
*/
|
|
27
|
+
export declare function bindOperations(transport: OperationTransport, execution: Execution): OperationTransport;
|
|
28
|
+
/** A transport of either kind, bound to `execution`. */
|
|
29
|
+
export declare function bound<W extends Transport | OperationTransport>(wire: W, execution: Execution): W;
|
|
20
30
|
/**
|
|
21
31
|
* A client made over a transport, made again over the transport bound to each execution it is
|
|
22
|
-
* used in. What a generated `clientOf(
|
|
23
|
-
* `this.client.sendIssue(…)` sends that request with its own trace.
|
|
32
|
+
* used in. What a generated `clientOf(operations)` answers, so a Feature that calls
|
|
33
|
+
* `this.client.sendIssue(…)` sends that request with its own trace, context and signal.
|
|
24
34
|
*
|
|
25
|
-
* export function clientOf(
|
|
26
|
-
* return bindable(
|
|
35
|
+
* export function clientOf(operations: OperationTransport) {
|
|
36
|
+
* return bindable(operations, (operations) => ({
|
|
37
|
+
* sendIssue: (input) => operations.call(operations_.CommandSendIssue, input),
|
|
38
|
+
* }));
|
|
27
39
|
* }
|
|
28
40
|
*
|
|
29
41
|
* Context: it takes the function that makes the client, not the client — rebuilding through
|
|
30
|
-
* `clientOf` itself would make TypeScript infer `clientOf` from its own return.
|
|
42
|
+
* `clientOf` itself would make TypeScript infer `clientOf` from its own return. A REST
|
|
43
|
+
* `Transport` binds the same way, for a client written over it by hand.
|
|
31
44
|
*/
|
|
32
|
-
export declare function bindable<T extends object>(
|
|
45
|
+
export declare function bindable<W extends Transport | OperationTransport, T extends object>(wire: W, make: (wire: W) => T): T;
|
|
@@ -35,42 +35,71 @@ export function headersOf(execution) {
|
|
|
35
35
|
*
|
|
36
36
|
* Context: the transport's own headers (`restTransport({ headers })`) are the session's — the
|
|
37
37
|
* token, the tenant — the same for every call; these are the use case's, different for two use
|
|
38
|
-
* cases running at once, so they travel per call (`CallOptions`).
|
|
38
|
+
* cases running at once, so they travel per call (`CallOptions`). A call that gives no signal of
|
|
39
|
+
* its own is cancelled with the run.
|
|
39
40
|
*/
|
|
40
41
|
export function bindTransport(transport, execution) {
|
|
41
42
|
const own = headersOf(execution);
|
|
42
|
-
|
|
43
|
+
const cancels = execution.signal;
|
|
44
|
+
if (Object.keys(own).length === 0 && cancels === undefined)
|
|
43
45
|
return transport;
|
|
44
46
|
const call = (options) => ({ ...options, headers: { ...own, ...options?.headers } });
|
|
45
47
|
const bound = {
|
|
46
|
-
list: (route, criteria, signal, options) => transport.list(route, criteria, signal, call(options)),
|
|
47
|
-
one: (route, signal, options) => transport.one(route, signal, call(options)),
|
|
48
|
-
send: (method, route, body, signal, options) => transport.send(method, route, body, signal, call(options)),
|
|
48
|
+
list: (route, criteria, signal, options) => transport.list(route, criteria, signal ?? cancels, call(options)),
|
|
49
|
+
one: (route, signal, options) => transport.one(route, signal ?? cancels, call(options)),
|
|
50
|
+
send: (method, route, body, signal, options) => transport.send(method, route, body, signal ?? cancels, call(options)),
|
|
49
51
|
};
|
|
50
52
|
const read = transport.read;
|
|
51
53
|
if (read !== undefined)
|
|
52
|
-
bound.read = (route, signal, options) => read.call(transport, route, signal, call(options));
|
|
54
|
+
bound.read = (route, signal, options) => read.call(transport, route, signal ?? cancels, call(options));
|
|
53
55
|
const download = transport.download;
|
|
54
56
|
if (download !== undefined)
|
|
55
|
-
bound.download = (route, signal, options) => download.call(transport, route, signal, call(options));
|
|
57
|
+
bound.download = (route, signal, options) => download.call(transport, route, signal ?? cancels, call(options));
|
|
56
58
|
return bound;
|
|
57
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The operation transport, calling as `execution`: every call carries the run's trace and
|
|
62
|
+
* context as headers and is cancelled with the run — the caller's own headers and signal still
|
|
63
|
+
* win.
|
|
64
|
+
*/
|
|
65
|
+
export function bindOperations(transport, execution) {
|
|
66
|
+
const own = headersOf(execution);
|
|
67
|
+
const cancels = execution.signal;
|
|
68
|
+
if (Object.keys(own).length === 0 && cancels === undefined)
|
|
69
|
+
return transport;
|
|
70
|
+
return {
|
|
71
|
+
call: (operation, input, options) => transport.call(operation, input, {
|
|
72
|
+
...options,
|
|
73
|
+
headers: { ...own, ...options?.headers },
|
|
74
|
+
...((options?.signal ?? cancels) ? { signal: options?.signal ?? cancels } : {}),
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** A transport of either kind, bound to `execution`. */
|
|
79
|
+
export function bound(wire, execution) {
|
|
80
|
+
return (isTransport(wire)
|
|
81
|
+
? bindTransport(wire, execution)
|
|
82
|
+
: bindOperations(wire, execution));
|
|
83
|
+
}
|
|
58
84
|
/**
|
|
59
85
|
* A client made over a transport, made again over the transport bound to each execution it is
|
|
60
|
-
* used in. What a generated `clientOf(
|
|
61
|
-
* `this.client.sendIssue(…)` sends that request with its own trace.
|
|
86
|
+
* used in. What a generated `clientOf(operations)` answers, so a Feature that calls
|
|
87
|
+
* `this.client.sendIssue(…)` sends that request with its own trace, context and signal.
|
|
62
88
|
*
|
|
63
|
-
* export function clientOf(
|
|
64
|
-
* return bindable(
|
|
89
|
+
* export function clientOf(operations: OperationTransport) {
|
|
90
|
+
* return bindable(operations, (operations) => ({
|
|
91
|
+
* sendIssue: (input) => operations.call(operations_.CommandSendIssue, input),
|
|
92
|
+
* }));
|
|
65
93
|
* }
|
|
66
94
|
*
|
|
67
95
|
* Context: it takes the function that makes the client, not the client — rebuilding through
|
|
68
|
-
* `clientOf` itself would make TypeScript infer `clientOf` from its own return.
|
|
96
|
+
* `clientOf` itself would make TypeScript infer `clientOf` from its own return. A REST
|
|
97
|
+
* `Transport` binds the same way, for a client written over it by hand.
|
|
69
98
|
*/
|
|
70
|
-
export function bindable(
|
|
71
|
-
const client = make(
|
|
99
|
+
export function bindable(wire, make) {
|
|
100
|
+
const client = make(wire);
|
|
72
101
|
Object.defineProperty(client, BIND, {
|
|
73
|
-
value: (execution) => bindable(
|
|
102
|
+
value: (execution) => bindable(bound(wire, execution), make),
|
|
74
103
|
enumerable: false,
|
|
75
104
|
});
|
|
76
105
|
return client;
|
|
@@ -14,7 +14,7 @@ import { scaffoldFilesOf } from "../services/scaffold.js";
|
|
|
14
14
|
*
|
|
15
15
|
* - `domain.ts`: the DTOs, only types — and the domain events with `DomainEvent`, their union,
|
|
16
16
|
* when the contract was exported with them (`export-client-contract --events`);
|
|
17
|
-
* - `client.ts`: every operation (and event) as data, and `clientOf(
|
|
17
|
+
* - `client.ts`: every operation (and event) as data, and `clientOf(operations)` over any wire;
|
|
18
18
|
* - `entities.ts`: an `entity<T, C>()` per model a screen can list;
|
|
19
19
|
* - `contract.json`: the snapshot they were written from, for `metasOf`.
|
|
20
20
|
*
|
|
@@ -26,7 +26,7 @@ import { scaffoldFilesOf } from "../services/scaffold.js";
|
|
|
26
26
|
*/
|
|
27
27
|
const USAGE = [
|
|
28
28
|
"usage: sincpro-client generate <contract.json> --out <dir> [--domain <name>] [--base <path>]",
|
|
29
|
-
" sincpro-client scaffold <contract.json> --out <dir> [--
|
|
29
|
+
" sincpro-client scaffold <contract.json> --out <dir> [--operations <module>]",
|
|
30
30
|
" sincpro-client check [--root src] [--tsconfig tsconfig.json]",
|
|
31
31
|
].join("\n");
|
|
32
32
|
/** JSON with every key sorted: the snapshot reads the same however the server ordered it. */
|
|
@@ -53,7 +53,7 @@ function parsed(args) {
|
|
|
53
53
|
flags.set(flag, value);
|
|
54
54
|
}
|
|
55
55
|
const out = flags.get("--out");
|
|
56
|
-
const known = new Set(command === "generate" ? ["--out", "--domain", "--base"] : ["--out", "--
|
|
56
|
+
const known = new Set(command === "generate" ? ["--out", "--domain", "--base"] : ["--out", "--operations"]);
|
|
57
57
|
if (out === undefined || [...flags.keys()].some((one) => !known.has(one)))
|
|
58
58
|
return undefined;
|
|
59
59
|
const domain = flags.get("--domain");
|
|
@@ -66,12 +66,12 @@ function parsed(args) {
|
|
|
66
66
|
...(domain === undefined ? {} : { domain }),
|
|
67
67
|
...(base === undefined ? {} : { base }),
|
|
68
68
|
},
|
|
69
|
-
|
|
69
|
+
operations: flags.get("--operations"),
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
/** The skeleton, each file only when it is not there: what is there is the application's. */
|
|
73
|
-
function scaffold(metamodel, out,
|
|
74
|
-
const files = scaffoldFilesOf(metamodel,
|
|
73
|
+
function scaffold(metamodel, out, operations) {
|
|
74
|
+
const files = scaffoldFilesOf(metamodel, operations === undefined ? {} : { operations });
|
|
75
75
|
const written = [];
|
|
76
76
|
const kept = [];
|
|
77
77
|
for (const [name, source] of Object.entries(files)) {
|
|
@@ -128,7 +128,7 @@ function main(args) {
|
|
|
128
128
|
}
|
|
129
129
|
const metamodel = JSON.parse(readFileSync(asked.source, "utf8"));
|
|
130
130
|
if (asked.command === "scaffold")
|
|
131
|
-
return scaffold(metamodel, asked.out, asked.
|
|
131
|
+
return scaffold(metamodel, asked.out, asked.operations);
|
|
132
132
|
const files = { "domain.ts": domainFileOf(metamodel) };
|
|
133
133
|
if (metamodel.bindings !== undefined) {
|
|
134
134
|
files["client.ts"] = clientFileOf(metamodel, asked.options);
|
|
@@ -3,14 +3,13 @@ import { type GenerateOptions } from "./rest.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* The TypeScript of the client of `metamodel`, as the text of one file next to its `domain.ts`.
|
|
5
5
|
*
|
|
6
|
-
* 1. Every operation, by its local name, as data: identity, role, `query`, entities, targets
|
|
7
|
-
* and the binding of each wire that publishes it.
|
|
8
|
-
* 2.
|
|
9
|
-
* —
|
|
10
|
-
* 3.
|
|
11
|
-
* 4. When the contract carries them, every domain event as data, by its type's name: its wire
|
|
6
|
+
* 1. Every operation, by its local name, as data: identity, role, `query`, entities, targets,
|
|
7
|
+
* whether it answers bytes, and the binding of each wire that publishes it.
|
|
8
|
+
* 2. A method per operation, named by `methodOf`: its input — optional when nothing in it is
|
|
9
|
+
* required — and the call's options (headers, signal, idempotency key), answering its answer.
|
|
10
|
+
* 3. When the contract carries them, every domain event as data, by its type's name: its wire
|
|
12
11
|
* name, and whether the bounded context records it or handles it.
|
|
13
|
-
* Final: `clientOf(transport)`,
|
|
14
|
-
* one name are refused, naming both.
|
|
12
|
+
* Final: `clientOf(transport)` over an `OperationTransport`, bound to each run that uses it; two
|
|
13
|
+
* methods with one name are refused, naming both.
|
|
15
14
|
*/
|
|
16
15
|
export declare function clientFileOf(metamodel: Metamodel, options?: GenerateOptions): string;
|
|
@@ -1,59 +1,31 @@
|
|
|
1
1
|
import { aliasOf, answerOf, claimed, deprecationOf, docOf, DOMAIN_MODULE, FRAMEWORK, headerOf, identifierOf, localOf, methodOf, recordLiteral, sorted, } from "../domain/names.js";
|
|
2
|
-
import {
|
|
2
|
+
import { restOf, } from "./rest.js";
|
|
3
3
|
/**
|
|
4
4
|
* The client of a bounded context as TypeScript: every operation as data, and a method for each
|
|
5
|
-
* one
|
|
6
|
-
*
|
|
5
|
+
* one, called by who it is through an `OperationTransport` — so a script, a test, a Feature or a
|
|
6
|
+
* hook calls the service typed, over whatever wire the application injects.
|
|
7
7
|
*
|
|
8
|
-
* Context:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* Context: the generated client names an operation and its input, never a route, a verb or an
|
|
9
|
+
* RPC method: those are the contract's bindings, in `operations`, and the adapter reads the one of
|
|
10
|
+
* its wire (`restOperations`, `jsonRpcOperations`, a platform's gRPC). So the same client, and the
|
|
11
|
+
* Features that use it, run over REST today and JSON-RPC or gRPC tomorrow without regenerating.
|
|
12
|
+
* Each method takes the operation's input and answers its answer, as the contract types them
|
|
13
|
+
* (`domain.ts`).
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.sort();
|
|
25
|
-
return { path, query };
|
|
15
|
+
const WIRES = [
|
|
16
|
+
["rest", "REST"],
|
|
17
|
+
["rpc", "JSON-RPC"],
|
|
18
|
+
["grpc", "gRPC"],
|
|
19
|
+
["mcp", "MCP"],
|
|
20
|
+
];
|
|
21
|
+
/** The wires that publish an operation, by name. */
|
|
22
|
+
function wiresOf(metamodel, identity) {
|
|
23
|
+
return WIRES.filter(([wire]) => metamodel.bindings?.[wire]?.[identity] !== undefined).map(([, name]) => name);
|
|
26
24
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return { kind: "send" };
|
|
32
|
-
const owner = ownerOf(metamodel, operation);
|
|
33
|
-
if (operation.role === "search" && owner !== undefined && owner in metamodel.models)
|
|
34
|
-
return { kind: "list", entity: owner, ...askedOf(operation, rest, true) };
|
|
35
|
-
// An export answers bytes, never JSON: it is downloaded.
|
|
36
|
-
if (operation.role === "export" && owner !== undefined && owner in metamodel.models)
|
|
37
|
-
return { kind: "download", entity: owner, ...askedOf(operation, rest, true, EXPORTED) };
|
|
38
|
-
const field = owner === undefined ? undefined : operation.targets[owner];
|
|
39
|
-
const record = rest.fields.length === 1 && rest.fields[0] === field;
|
|
40
|
-
if (operation.role === "get" && owner !== undefined && field !== undefined && record)
|
|
41
|
-
return { kind: "one", entity: owner, field };
|
|
42
|
-
// Any other GET — a Query of the context's own, `GET /me` — is read as it answers.
|
|
43
|
-
return { kind: "read", ...askedOf(operation, rest, false) };
|
|
44
|
-
}
|
|
45
|
-
/** A parameter list's TypeScript: the path's keys required, the rest optional, all as text. */
|
|
46
|
-
function paramsOf(asked) {
|
|
47
|
-
const union = (names) => names.map((one) => JSON.stringify(one)).join(" | ");
|
|
48
|
-
if (asked.path.length === 0 && asked.query.length === 0)
|
|
49
|
-
return { signature: "", route: (path) => JSON.stringify(path) };
|
|
50
|
-
const required = asked.path.length === 0 ? "" : `Readonly<Record<${union(asked.path)}, string>>`;
|
|
51
|
-
const optional = asked.query.length === 0 ? "" : `Partial<Record<${union(asked.query)}, string>>`;
|
|
52
|
-
const type = [required, optional].filter((one) => one !== "").join(" & ");
|
|
53
|
-
return {
|
|
54
|
-
signature: `params${asked.path.length === 0 ? "?" : ""}: ${type}`,
|
|
55
|
-
route: (path) => `listRoute(${JSON.stringify(path)}, params)`,
|
|
56
|
-
};
|
|
25
|
+
/** Whether an operation answers bytes as they are produced — an export —, not JSON. */
|
|
26
|
+
function isStreamed(operation) {
|
|
27
|
+
return (operation.output !== null &&
|
|
28
|
+
operation.output["x-sincpro-streamed"] !== undefined);
|
|
57
29
|
}
|
|
58
30
|
/** The data of one operation: what it is, and how each wire reaches it. */
|
|
59
31
|
function dataOf(metamodel, identity, operation, rest) {
|
|
@@ -64,6 +36,8 @@ function dataOf(metamodel, identity, operation, rest) {
|
|
|
64
36
|
` entities: ${JSON.stringify(operation.entities)},`,
|
|
65
37
|
` targets: ${recordLiteral(operation.targets)},`,
|
|
66
38
|
];
|
|
39
|
+
if (isStreamed(operation))
|
|
40
|
+
lines.push(" streamed: true,");
|
|
67
41
|
if (rest !== undefined)
|
|
68
42
|
lines.push(` rest: { method: ${JSON.stringify(rest.method)}, path: ${JSON.stringify(rest.path)} },`);
|
|
69
43
|
const rpc = metamodel.bindings?.rpc?.[identity];
|
|
@@ -77,121 +51,50 @@ function dataOf(metamodel, identity, operation, rest) {
|
|
|
77
51
|
lines.push(` mcp: { tool: ${JSON.stringify(mcp.tool)} },`);
|
|
78
52
|
return ` ${localOf(metamodel, identity)}: {\n${lines.join("\n")}\n },`;
|
|
79
53
|
}
|
|
80
|
-
function methodSourceOf(local, answer, method, call, rest, doc) {
|
|
81
|
-
if (call.kind === "list") {
|
|
82
|
-
const entity = `dto.${identifierOf(call.entity)}`;
|
|
83
|
-
const params = paramsOf(call);
|
|
84
|
-
const signature = params.signature === "" ? "" : `, ${params.signature}`;
|
|
85
|
-
return `${doc} ${method}: (criteria: Criteria<${entity}>${signature}) =>\n transport.list<${entity}>(${params.route(rest.path)}, criteria),`;
|
|
86
|
-
}
|
|
87
|
-
if (call.kind === "download") {
|
|
88
|
-
const entity = `dto.${identifierOf(call.entity)}`;
|
|
89
|
-
const params = paramsOf(call);
|
|
90
|
-
// The path's keys are required, so they come first: a required parameter never follows an
|
|
91
|
-
// optional one.
|
|
92
|
-
const keys = params.signature === "" ? "" : `${params.signature.replace("params?:", "params:")}, `;
|
|
93
|
-
const given = params.signature === "" ? "" : ", params";
|
|
94
|
-
return `${doc} ${method}: (${keys}criteria?: Criteria<${entity}>, format?: ExportFormat) =>\n downloadRoute(transport, exportRoute(${JSON.stringify(rest.path)}, criteria, format${given})),`;
|
|
95
|
-
}
|
|
96
|
-
if (call.kind === "read") {
|
|
97
|
-
const params = paramsOf(call);
|
|
98
|
-
return `${doc} ${method}: (${params.signature}) =>\n readRoute<dto.${answer}>(transport, ${params.route(rest.path)}),`;
|
|
99
|
-
}
|
|
100
|
-
if (call.kind === "one") {
|
|
101
|
-
const path = rest.path.replace(`{${call.field}}`, "${encodeURIComponent(id)}");
|
|
102
|
-
return `${doc} ${method}: (id: string) =>\n transport.one<dto.${identifierOf(call.entity)}>(\`${path}\`),`;
|
|
103
|
-
}
|
|
104
|
-
const id = rest.fields.includes("id")
|
|
105
|
-
? "body.id === undefined ? undefined : String(body.id)"
|
|
106
|
-
: "undefined";
|
|
107
|
-
return [
|
|
108
|
-
`${doc} ${method}: (body: dto.${local}, options?: SendOptions) =>`,
|
|
109
|
-
` transport.send<dto.${answer}>(`,
|
|
110
|
-
` ${JSON.stringify(rest.method)},`,
|
|
111
|
-
` pathFor(routes.${method}, ${id}, body),`,
|
|
112
|
-
` body,`,
|
|
113
|
-
` undefined,`,
|
|
114
|
-
` options,`,
|
|
115
|
-
` ),`,
|
|
116
|
-
].join("\n");
|
|
117
|
-
}
|
|
118
54
|
/**
|
|
119
55
|
* The TypeScript of the client of `metamodel`, as the text of one file next to its `domain.ts`.
|
|
120
56
|
*
|
|
121
|
-
* 1. Every operation, by its local name, as data: identity, role, `query`, entities, targets
|
|
122
|
-
* and the binding of each wire that publishes it.
|
|
123
|
-
* 2.
|
|
124
|
-
* —
|
|
125
|
-
* 3.
|
|
126
|
-
* 4. When the contract carries them, every domain event as data, by its type's name: its wire
|
|
57
|
+
* 1. Every operation, by its local name, as data: identity, role, `query`, entities, targets,
|
|
58
|
+
* whether it answers bytes, and the binding of each wire that publishes it.
|
|
59
|
+
* 2. A method per operation, named by `methodOf`: its input — optional when nothing in it is
|
|
60
|
+
* required — and the call's options (headers, signal, idempotency key), answering its answer.
|
|
61
|
+
* 3. When the contract carries them, every domain event as data, by its type's name: its wire
|
|
127
62
|
* name, and whether the bounded context records it or handles it.
|
|
128
|
-
* Final: `clientOf(transport)`,
|
|
129
|
-
* one name are refused, naming both.
|
|
63
|
+
* Final: `clientOf(transport)` over an `OperationTransport`, bound to each run that uses it; two
|
|
64
|
+
* methods with one name are refused, naming both.
|
|
130
65
|
*/
|
|
131
66
|
export function clientFileOf(metamodel, options = {}) {
|
|
132
67
|
const claim = claimed();
|
|
133
|
-
const operations = sorted(metamodel.operations);
|
|
134
68
|
const data = [];
|
|
135
|
-
const routes = [];
|
|
136
69
|
const methods = [];
|
|
137
|
-
const
|
|
138
|
-
let lists = false;
|
|
139
|
-
let paged = false;
|
|
140
|
-
let reads = false;
|
|
141
|
-
let sends = false;
|
|
142
|
-
let downloads = false;
|
|
143
|
-
for (const [identity, operation] of operations) {
|
|
70
|
+
for (const [identity, operation] of sorted(metamodel.operations)) {
|
|
144
71
|
const local = localOf(metamodel, identity);
|
|
145
|
-
|
|
146
|
-
data.push(dataOf(metamodel, identity, operation, rest));
|
|
147
|
-
const call = callOf(metamodel, operation, rest);
|
|
148
|
-
if (call.kind === "none" || rest === undefined) {
|
|
149
|
-
unreached.push(`\`${local}\`: ${call.kind === "none" ? call.why : ""}.`);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
72
|
+
data.push(dataOf(metamodel, identity, operation, restOf(metamodel, identity, options)));
|
|
152
73
|
const method = methodOf(local);
|
|
153
74
|
claim(method, `operation ${identity}`);
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (call.kind === "send") {
|
|
166
|
-
sends = true;
|
|
167
|
-
routes.push(` ${method}: writeRoute(${JSON.stringify(`${rest.method} ${rest.path}`)}),`);
|
|
168
|
-
}
|
|
75
|
+
const wires = wiresOf(metamodel, identity);
|
|
76
|
+
const doc = docOf([
|
|
77
|
+
operation.description,
|
|
78
|
+
deprecationOf(metamodel, operation),
|
|
79
|
+
wires.length === 0 ? undefined : `Published over ${wires.join(", ")}.`,
|
|
80
|
+
], " ");
|
|
81
|
+
const optional = (operation.input.required ?? []).length === 0;
|
|
82
|
+
methods.push([
|
|
83
|
+
`${doc} ${method}: (input${optional ? "?" : ""}: dto.${local}, options?: OperationOptions) =>`,
|
|
84
|
+
` transport.call<dto.${answerOf(metamodel, identity)}>(operations.${local}, input${optional ? " ?? {}" : ""}, options),`,
|
|
85
|
+
].join("\n"));
|
|
169
86
|
}
|
|
170
|
-
const
|
|
171
|
-
"bindable",
|
|
172
|
-
...(downloads ? ["downloadRoute", "type ExportFormat", "exportRoute"] : []),
|
|
173
|
-
...(lists ? ["listRoute"] : []),
|
|
174
|
-
...(sends ? ["pathFor"] : []),
|
|
175
|
-
...(reads ? ["readRoute"] : []),
|
|
176
|
-
...(sends ? ["type SendOptions"] : []),
|
|
177
|
-
"type Transport",
|
|
178
|
-
...(sends ? ["writeRoute"] : []),
|
|
179
|
-
];
|
|
87
|
+
const events = sorted(metamodel.events ?? {}).map(([name, event]) => ` ${identifierOf(event.title)}: { name: ${JSON.stringify(name)}, recorded: ${event.recorded}, handled: ${event.handled} },`);
|
|
180
88
|
const imports = [
|
|
181
|
-
|
|
182
|
-
`import { ${core.join(", ")} } from "${FRAMEWORK}";`,
|
|
89
|
+
`import { bindable, type OperationOptions, type OperationTransport } from "${FRAMEWORK}";`,
|
|
183
90
|
"",
|
|
184
91
|
`import type * as dto from "${DOMAIN_MODULE}";`,
|
|
185
92
|
].join("\n");
|
|
186
|
-
const events = sorted(metamodel.events ?? {}).map(([name, event]) => ` ${identifierOf(event.title)}: { name: ${JSON.stringify(name)}, recorded: ${event.recorded}, handled: ${event.handled} },`);
|
|
187
93
|
const client = [
|
|
188
94
|
"/**",
|
|
189
|
-
` * «${aliasOf(metamodel)}» over
|
|
190
|
-
" *
|
|
191
|
-
" *
|
|
192
|
-
...(unreached.length === 0
|
|
193
|
-
? []
|
|
194
|
-
: [" *", " * Without a method here:", ...unreached.map((one) => ` * - ${one}`)]),
|
|
95
|
+
` * «${aliasOf(metamodel)}» over any wire: a method per operation, called by who it is. The transport`,
|
|
96
|
+
" * decides how it travels — `restOperations(restTransport(…))`, `jsonRpcOperations(…)`, a",
|
|
97
|
+
" * platform's —; used by a Feature, each call carries the run's trace, context and signal.",
|
|
195
98
|
" */",
|
|
196
99
|
].join("\n");
|
|
197
100
|
return `${[
|
|
@@ -203,7 +106,6 @@ export function clientFileOf(metamodel, options = {}) {
|
|
|
203
106
|
: [
|
|
204
107
|
`/** Every domain event of «${aliasOf(metamodel)}» in the contract, as data: its wire name, and whether it records or handles it. */\nexport const events = {\n${events.join("\n")}\n} as const;`,
|
|
205
108
|
]),
|
|
206
|
-
|
|
207
|
-
`${client}\nexport function clientOf(transport: Transport) {\n return bindable(transport, (transport) => ({\n${methods.join("\n")}\n }));\n}`,
|
|
109
|
+
`${client}\nexport function clientOf(transport: OperationTransport) {\n return bindable(transport, (transport) => ({\n${methods.join("\n")}\n }));\n}`,
|
|
208
110
|
].join("\n\n")}\n`;
|
|
209
111
|
}
|
|
@@ -17,8 +17,11 @@ import type { Metamodel } from "../domain/metamodel.js";
|
|
|
17
17
|
* whole every time.
|
|
18
18
|
*/
|
|
19
19
|
export type ScaffoldOptions = {
|
|
20
|
-
/**
|
|
21
|
-
*
|
|
22
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Where the application exports how it reaches its services (`export const operations`, an
|
|
22
|
+
* `OperationTransport`: REST, JSON-RPC, a platform's), from the bounded context's folder.
|
|
23
|
+
* `../common/infrastructure/operations` by default.
|
|
24
|
+
*/
|
|
25
|
+
operations?: string;
|
|
23
26
|
};
|
|
24
27
|
export declare function scaffoldFilesOf(metamodel: Metamodel, options?: ScaffoldOptions): Record<string, string>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { aliasOf, FRAMEWORK } from "../domain/names.js";
|
|
2
2
|
/** Names the skeleton's `index.ts` cannot give its constant. */
|
|
3
3
|
const TAKEN = new Set([
|
|
4
|
-
"
|
|
4
|
+
"operations",
|
|
5
5
|
"registerServices",
|
|
6
6
|
..."break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static implements interface package private protected public await".split(" "),
|
|
7
7
|
]);
|
|
@@ -26,11 +26,11 @@ export function scaffoldFilesOf(metamodel, options = {}) {
|
|
|
26
26
|
const name = TAKEN.has(bare) ? `${bare}BoundedContext` : bare;
|
|
27
27
|
const Dependencies = `${Name}Dependencies`;
|
|
28
28
|
const client = metamodel.bindings !== undefined;
|
|
29
|
-
const
|
|
29
|
+
const operations = options.operations ?? at("..", "common", "infrastructure", "operations");
|
|
30
30
|
const dependencies = [
|
|
31
31
|
HEADER(`What the services of «${alias}» can count on`),
|
|
32
32
|
"",
|
|
33
|
-
`import type {
|
|
33
|
+
`import type { OperationTransport } from "${FRAMEWORK}";`,
|
|
34
34
|
`import type { BoundedContext } from "${at(FRAMEWORK, "core")}";`,
|
|
35
35
|
...(client ? ["", `import { clientOf } from "${at("..", "generated", "client")}";`] : []),
|
|
36
36
|
"",
|
|
@@ -45,9 +45,9 @@ export function scaffoldFilesOf(metamodel, options = {}) {
|
|
|
45
45
|
"",
|
|
46
46
|
"export function registerDependencies(",
|
|
47
47
|
` bc: BoundedContext<${Dependencies}>,`,
|
|
48
|
-
` ${client ? "
|
|
48
|
+
` ${client ? "operations" : "_operations"}: OperationTransport,`,
|
|
49
49
|
") {",
|
|
50
|
-
...(client ? [` bc.addDependency("client", clientOf(
|
|
50
|
+
...(client ? [` bc.addDependency("client", clientOf(operations));`] : []),
|
|
51
51
|
" return bc;",
|
|
52
52
|
"}",
|
|
53
53
|
"",
|
|
@@ -60,23 +60,24 @@ export function scaffoldFilesOf(metamodel, options = {}) {
|
|
|
60
60
|
" Feature as BaseFeature,",
|
|
61
61
|
" BoundedContext,",
|
|
62
62
|
" type Command,",
|
|
63
|
+
" type DomainEvent,",
|
|
63
64
|
`} from "${at(FRAMEWORK, "core")}";`,
|
|
64
|
-
`import type {
|
|
65
|
+
`import type { OperationTransport } from "${FRAMEWORK}";`,
|
|
65
66
|
"",
|
|
66
67
|
`import { type ${Dependencies}, registerDependencies } from "${at(".", "dependencies")}";`,
|
|
67
68
|
"",
|
|
68
|
-
`/** A Feature of «${alias}
|
|
69
|
-
"export abstract class Feature<C extends Command<R
|
|
69
|
+
`/** A Feature of «${alias}» — of a Command, or a subscriber of an event: its dependencies typed on \`this\`, and \`this.context\`. */`,
|
|
70
|
+
"export abstract class Feature<C extends Command<R> | DomainEvent, R> extends BaseFeature<C, R> {",
|
|
70
71
|
...(client ? [` declare readonly client: ${Dependencies}["client"];`] : []),
|
|
71
72
|
"}",
|
|
72
73
|
"",
|
|
73
74
|
`/** An ApplicationService of «${alias}»: the same, and \`this.featureBus.execute(dto)\`. */`,
|
|
74
|
-
"export abstract class ApplicationService<C extends Command<R
|
|
75
|
+
"export abstract class ApplicationService<C extends Command<R> | DomainEvent, R> extends BaseApplicationService<C, R> {",
|
|
75
76
|
...(client ? [` declare readonly client: ${Dependencies}["client"];`] : []),
|
|
76
77
|
"}",
|
|
77
78
|
"",
|
|
78
|
-
`export function config${Name}BoundedContext(name: string,
|
|
79
|
-
` return registerDependencies(new BoundedContext<${Dependencies}>(name),
|
|
79
|
+
`export function config${Name}BoundedContext(name: string, operations: OperationTransport) {`,
|
|
80
|
+
` return registerDependencies(new BoundedContext<${Dependencies}>(name), operations);`,
|
|
80
81
|
"}",
|
|
81
82
|
"",
|
|
82
83
|
].join("\n");
|
|
@@ -99,13 +100,13 @@ export function scaffoldFilesOf(metamodel, options = {}) {
|
|
|
99
100
|
const index = [
|
|
100
101
|
HEADER(`«${alias}», ready to use`),
|
|
101
102
|
"",
|
|
102
|
-
`import {
|
|
103
|
+
`import { operations } from "${operations}";`,
|
|
103
104
|
"",
|
|
104
105
|
`import { config${Name}BoundedContext } from "${at(".", "infrastructure", "bounded_context")}";`,
|
|
105
106
|
`import { registerServices } from "${at(".", "services")}";`,
|
|
106
107
|
"",
|
|
107
108
|
`/** «${alias}»: \`${name}.execute(new Command(...))\`. */`,
|
|
108
|
-
`export const ${name} = config${Name}BoundedContext(${JSON.stringify(alias)},
|
|
109
|
+
`export const ${name} = config${Name}BoundedContext(${JSON.stringify(alias)}, operations);`,
|
|
109
110
|
`registerServices(${name});`,
|
|
110
111
|
"",
|
|
111
112
|
].join("\n");
|