@sincpro/client-framework 0.6.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 +95 -25
- package/MANIFESTO.md +103 -40
- package/README.md +608 -333
- 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/{memory.d.ts → adapters/memory.d.ts} +1 -1
- package/dist/client/{memory.js → adapters/memory.js} +2 -2
- package/dist/{adapters → client/adapters}/parquet.d.ts +1 -1
- package/dist/client/adapters/rest-operations.d.ts +19 -0
- package/dist/client/adapters/rest-operations.js +66 -0
- package/dist/client/{rest.d.ts → adapters/rest.d.ts} +1 -1
- package/dist/client/{rest.js → adapters/rest.js} +2 -2
- package/dist/client/{entity.d.ts → domain/entity.d.ts} +6 -10
- package/dist/client/domain/operation.d.ts +73 -0
- package/dist/client/domain/operation.js +19 -0
- package/dist/client/index.d.ts +20 -0
- package/dist/client/index.js +20 -0
- package/dist/client/services/guarded.d.ts +17 -0
- package/dist/client/services/guarded.js +19 -0
- package/dist/client/services/propagation.d.ts +45 -0
- package/dist/{services/execution.js → client/services/propagation.js} +49 -36
- package/dist/client/{retry.js → services/retry.js} +1 -1
- package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +17 -2
- package/dist/codegen/{names.d.ts → domain/names.d.ts} +2 -2
- package/dist/codegen/{names.js → domain/names.js} +3 -3
- package/dist/codegen/{cli.js → entrypoint/cli.js} +15 -14
- package/dist/codegen/index.d.ts +7 -7
- package/dist/codegen/index.js +5 -10
- package/dist/codegen/services/client.d.ts +15 -0
- package/dist/codegen/services/client.js +111 -0
- package/dist/codegen/services/domain.d.ts +20 -0
- package/dist/codegen/{domain.js → services/domain.js} +82 -25
- package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -3
- package/dist/codegen/{entities.js → services/entities.js} +18 -4
- package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
- package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +8 -5
- package/dist/codegen/{scaffold.js → services/scaffold.js} +39 -43
- package/dist/common/failures.d.ts +83 -0
- package/dist/common/failures.js +212 -0
- package/dist/common/index.d.ts +11 -0
- package/dist/common/index.js +11 -0
- package/dist/context/adapters/memory.d.ts +6 -0
- package/dist/context/adapters/memory.js +34 -0
- package/dist/context/adapters/plain-codec.d.ts +3 -0
- package/dist/context/adapters/plain-codec.js +16 -0
- package/dist/context/{codec.d.ts → domain/codec.d.ts} +0 -1
- package/dist/context/{codec.js → domain/codec.js} +0 -14
- package/dist/context/domain/execution.d.ts +45 -0
- package/dist/context/domain/execution.js +11 -0
- package/dist/context/{store.d.ts → domain/store.d.ts} +0 -5
- package/dist/context/{store.js → domain/store.js} +1 -34
- package/dist/context/{context.d.ts → entrypoint/context.d.ts} +1 -1
- package/dist/context/index.d.ts +17 -0
- package/dist/context/index.js +15 -0
- package/dist/context/infrastructure/process.d.ts +13 -0
- package/dist/context/infrastructure/process.js +35 -0
- package/dist/core/domain/bus.d.ts +27 -0
- package/dist/core/domain/bus.js +26 -0
- package/dist/core/domain/dto.d.ts +62 -0
- package/dist/core/domain/dto.js +43 -0
- package/dist/core/domain/errors.d.ts +18 -0
- package/dist/core/domain/errors.js +1 -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 +47 -0
- package/dist/core/domain/services.js +31 -0
- package/dist/core/entrypoint/bounded-context.d.ts +139 -0
- package/dist/core/entrypoint/bounded-context.js +426 -0
- package/dist/core/entrypoint/screens.d.ts +6 -0
- package/dist/core/entrypoint/screens.js +136 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +17 -0
- package/dist/core/infrastructure/listeners.d.ts +28 -0
- package/dist/core/infrastructure/listeners.js +34 -0
- package/dist/core/infrastructure/process.d.ts +56 -0
- package/dist/core/infrastructure/process.js +35 -0
- package/dist/core/services/binding.d.ts +6 -0
- package/dist/core/services/binding.js +16 -0
- package/dist/core/services/error-handlers.d.ts +11 -0
- package/dist/core/services/error-handlers.js +21 -0
- package/dist/core/services/execution.d.ts +66 -0
- package/dist/core/services/execution.js +216 -0
- 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 +21 -38
- package/dist/index.js +21 -38
- package/dist/model/index.d.ts +10 -0
- package/dist/model/index.js +10 -0
- package/dist/model/{metas.d.ts → infrastructure/metas.d.ts} +1 -1
- package/dist/model/{schema.d.ts → services/schema.d.ts} +1 -1
- package/dist/model/{schema.js → services/schema.js} +1 -1
- package/dist/observability/adapters/console.d.ts +12 -0
- package/dist/observability/adapters/console.js +35 -0
- package/dist/observability/adapters/memory.d.ts +41 -0
- package/dist/observability/adapters/memory.js +49 -0
- package/dist/{adapters → observability/adapters}/opentelemetry.d.ts +1 -1
- package/dist/{adapters → observability/adapters}/sentry.d.ts +1 -1
- package/dist/observability/domain/errors.d.ts +23 -0
- package/dist/observability/domain/errors.js +8 -0
- package/dist/observability/domain/logging.d.ts +44 -0
- package/dist/observability/domain/logging.js +1 -0
- package/dist/observability/domain/tracing.d.ts +49 -0
- package/dist/observability/domain/tracing.js +20 -0
- package/dist/observability/index.d.ts +16 -0
- package/dist/observability/index.js +4 -0
- package/dist/observability/infrastructure/registry.d.ts +21 -0
- package/dist/observability/infrastructure/registry.js +18 -0
- package/dist/observability/services/logging.d.ts +27 -0
- package/dist/observability/services/logging.js +106 -0
- package/dist/observability/services/outcome.d.ts +12 -0
- package/dist/observability/services/outcome.js +17 -0
- package/dist/{services/report.js → observability/services/reporting.js} +4 -3
- package/dist/testing/index.d.ts +3 -33
- package/dist/testing/index.js +5 -37
- package/package.json +16 -8
- package/dist/codegen/client.d.ts +0 -14
- package/dist/codegen/client.js +0 -201
- package/dist/codegen/domain.d.ts +0 -15
- package/dist/observability/ports.d.ts +0 -65
- package/dist/observability/ports.js +0 -21
- package/dist/services/bounded-context.d.ts +0 -79
- package/dist/services/bounded-context.js +0 -261
- package/dist/services/execution.d.ts +0 -63
- package/dist/services/feature.d.ts +0 -85
- package/dist/services/feature.js +0 -86
- package/dist/services/process.d.ts +0 -60
- package/dist/services/process.js +0 -57
- package/dist/services/screens.d.ts +0 -2
- package/dist/services/screens.js +0 -76
- /package/dist/{adapters → client/adapters}/parquet.js +0 -0
- /package/dist/client/{contract.d.ts → domain/contract.d.ts} +0 -0
- /package/dist/client/{contract.js → domain/contract.js} +0 -0
- /package/dist/client/{entity.js → domain/entity.js} +0 -0
- /package/dist/client/{transport.d.ts → domain/transport.d.ts} +0 -0
- /package/dist/client/{transport.js → domain/transport.js} +0 -0
- /package/dist/client/{discovery.d.ts → services/discovery.d.ts} +0 -0
- /package/dist/client/{discovery.js → services/discovery.js} +0 -0
- /package/dist/client/{retry.d.ts → services/retry.d.ts} +0 -0
- /package/dist/codegen/{metamodel.js → domain/metamodel.js} +0 -0
- /package/dist/codegen/{cli.d.ts → entrypoint/cli.d.ts} +0 -0
- /package/dist/codegen/{check.d.ts → services/check.d.ts} +0 -0
- /package/dist/codegen/{check.js → services/check.js} +0 -0
- /package/dist/codegen/{rest.js → services/rest.js} +0 -0
- /package/dist/context/{context.js → entrypoint/context.js} +0 -0
- /package/dist/model/{meta.d.ts → domain/meta.d.ts} +0 -0
- /package/dist/model/{meta.js → domain/meta.js} +0 -0
- /package/dist/model/{metas.js → infrastructure/metas.js} +0 -0
- /package/dist/{adapters → observability/adapters}/opentelemetry.js +0 -0
- /package/dist/{adapters → observability/adapters}/sentry.js +0 -0
- /package/dist/{services/report.d.ts → observability/services/reporting.d.ts} +0 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { answerOf, claimed, contextOf, deprecationOf, docOf, headerOf, identifierOf, keyOf, localOf, sorted, } from "
|
|
1
|
+
import { aliasOf, answerOf, claimed, contextOf, deprecationOf, docOf, headerOf, identifierOf, keyOf, localOf, sorted, } from "../domain/names.js";
|
|
2
2
|
/**
|
|
3
3
|
* The DTOs of a bounded context as TypeScript, out of the contract sincpro_framework exports:
|
|
4
|
-
* a type per model, per shared definition a model or an operation reaches,
|
|
5
|
-
* input and answer. Only types — nothing in
|
|
4
|
+
* a type per model, per shared definition a model or an operation reaches, per operation's
|
|
5
|
+
* input and answer, and per domain event when the contract carries them. Only types — nothing in
|
|
6
|
+
* the file runs.
|
|
7
|
+
*
|
|
8
|
+
* Context: types, never classes. The wire is JSON, and a type says exactly what it holds — a
|
|
9
|
+
* decimal as text, a page as `{items, count, cursor}` —, where a class would claim more unless
|
|
10
|
+
* every field had a reader. And the local bus routes by class: a generated class extending
|
|
11
|
+
* `Command` could be registered and executed locally, the confusion between a remote operation
|
|
12
|
+
* and a local use case this package keeps apart. Events read best as a union narrowed by `name`.
|
|
6
13
|
*
|
|
7
14
|
* Context: the file is a snapshot for whoever writes code — a person, or an agent reading the
|
|
8
15
|
* repository —, never the truth at runtime: the transport keeps reading the live definition and
|
|
@@ -46,11 +53,17 @@ function keyNote(schema) {
|
|
|
46
53
|
return undefined;
|
|
47
54
|
return `The key of a \`${said.relation}\`.`;
|
|
48
55
|
}
|
|
49
|
-
|
|
56
|
+
/**
|
|
57
|
+
* What each reference of a rendering names. `input`: the rendering is what a caller SENDS — an
|
|
58
|
+
* operation's input —, so a model there is its `<Name>Input`, whose fields follow the contract's
|
|
59
|
+
* `required`, not the record a read answers whole.
|
|
60
|
+
*/
|
|
61
|
+
function scopeOf(metamodel, shared, input = false) {
|
|
50
62
|
const definitions = metamodel.$defs ?? {};
|
|
51
63
|
const scope = {
|
|
52
|
-
reached: new Set(),
|
|
53
|
-
imports: new Set(),
|
|
64
|
+
reached: shared?.reached ?? new Set(),
|
|
65
|
+
imports: shared?.imports ?? new Set(),
|
|
66
|
+
inputs: shared?.inputs ?? new Set(),
|
|
54
67
|
refer: (ref) => {
|
|
55
68
|
const model = ref.startsWith(MODELS);
|
|
56
69
|
if (!model && !ref.startsWith(DEFINITIONS))
|
|
@@ -60,7 +73,10 @@ function scopeOf(metamodel) {
|
|
|
60
73
|
if (model || name in metamodel.models) {
|
|
61
74
|
if (!(name in metamodel.models))
|
|
62
75
|
throw new Error(`"${ref}" names no model of ${contextOf(metamodel)}`);
|
|
63
|
-
|
|
76
|
+
if (!input)
|
|
77
|
+
return identifierOf(name);
|
|
78
|
+
scope.inputs.add(name);
|
|
79
|
+
return `${identifierOf(name)}Input`;
|
|
64
80
|
}
|
|
65
81
|
const imported = IMPORTED[name];
|
|
66
82
|
if (imported !== undefined) {
|
|
@@ -86,16 +102,13 @@ function optional(name, property, schema, record) {
|
|
|
86
102
|
return EXPANDED.has(property["x-sincpro"]?.type ?? "");
|
|
87
103
|
return !(schema.required ?? []).includes(name);
|
|
88
104
|
}
|
|
89
|
-
/**
|
|
90
|
-
*
|
|
91
|
-
* collection a reading answers (`{items, count, cursor}`, sincpro_framework 3.33 on) or, before
|
|
92
|
-
* that, a bare array, which said less than the wire sent.
|
|
93
|
-
*/
|
|
105
|
+
/** The records a to-many holds, when `property` is one: the `items` of the collection a reading
|
|
106
|
+
* answers (`{items, count, cursor}`). */
|
|
94
107
|
function heldRecords(property) {
|
|
95
108
|
if (!TO_MANY.has(property["x-sincpro"]?.type ?? ""))
|
|
96
109
|
return undefined;
|
|
97
|
-
const held = property.properties?.["items"]
|
|
98
|
-
return held
|
|
110
|
+
const held = property.properties?.["items"];
|
|
111
|
+
return held?.type === "array" ? held.items : undefined;
|
|
99
112
|
}
|
|
100
113
|
/**
|
|
101
114
|
* A property's TypeScript. A to-many inside a record is an `EntityCollection` of its records —
|
|
@@ -179,21 +192,37 @@ function operationDoc(metamodel, local, operation) {
|
|
|
179
192
|
].filter((one) => one !== undefined);
|
|
180
193
|
return (docOf([operation.description, `${facts.join(" · ")}.`, deprecationOf(metamodel, operation)], "") || `/** ${local} */\n`);
|
|
181
194
|
}
|
|
195
|
+
/** What the server says of an event: its words, then its wire name and who records or hears it. */
|
|
196
|
+
function eventDoc(metamodel, name, event) {
|
|
197
|
+
const alias = aliasOf(metamodel);
|
|
198
|
+
const facts = [
|
|
199
|
+
`\`${name}\` on the wire`,
|
|
200
|
+
event.recorded ? `recorded by «${alias}»` : undefined,
|
|
201
|
+
event.handled ? `handled by «${alias}»` : undefined,
|
|
202
|
+
].filter((one) => one !== undefined);
|
|
203
|
+
return docOf([event.description, `${facts.join(" · ")}.`], "");
|
|
204
|
+
}
|
|
182
205
|
/**
|
|
183
206
|
* The TypeScript of every DTO of `metamodel`, as the text of one file.
|
|
184
207
|
*
|
|
185
208
|
* 1. The models, by name: an entity's contract, a related model by its own name.
|
|
186
209
|
* 2. Each operation, by its local name (its identity without `<context>.`): its input as
|
|
187
|
-
* `<Name>` and its answer by the backend's own name for it (`answerOf`: `ResponseX`)
|
|
188
|
-
* `<Name>Answer`
|
|
189
|
-
* 3.
|
|
210
|
+
* `<Name>` and its answer by the backend's own name for it (`answerOf`: `ResponseX`) — or as
|
|
211
|
+
* `<Name>Answer` when it has no name.
|
|
212
|
+
* 3. A model an input carries — and the ones it carries, until none is new — as `<Name>Input`:
|
|
213
|
+
* what a caller sends follows the contract's `required`; the server fills the rest.
|
|
214
|
+
* 4. Each domain event, when the contract carries them, by its class's name (`title`): a record
|
|
215
|
+
* as it is received — every field present, `name` its wire name as a literal —, and
|
|
216
|
+
* `DomainEvent`, the union of them all, which a `switch` on `name` narrows.
|
|
217
|
+
* 5. The shared definitions those reach — and the ones those reach, until none is new —, but
|
|
190
218
|
* never one a package of the stack already types (`Criteria`).
|
|
191
219
|
* Final: a header that says where it came from and not to edit it, the imports, then models,
|
|
192
|
-
* definitions and
|
|
193
|
-
* refused, naming both.
|
|
220
|
+
* definitions, operations and events, each sorted by name; two of them with one TypeScript
|
|
221
|
+
* name are refused, naming both.
|
|
194
222
|
*/
|
|
195
223
|
export function domainFileOf(metamodel) {
|
|
196
224
|
const scope = scopeOf(metamodel);
|
|
225
|
+
const sent = scopeOf(metamodel, scope, true);
|
|
197
226
|
const definitions = metamodel.$defs ?? {};
|
|
198
227
|
const claim = claimed();
|
|
199
228
|
const models = sorted(metamodel.models).map(([name, schema]) => {
|
|
@@ -210,19 +239,47 @@ export function domainFileOf(metamodel) {
|
|
|
210
239
|
? "unknown"
|
|
211
240
|
: (streamed(operation.output, scope) ?? typeOf(operation.output, scope, 0, false));
|
|
212
241
|
const written = [
|
|
213
|
-
`${operationDoc(metamodel, local, operation)}export type ${local} = ${typeOf(operation.input,
|
|
242
|
+
`${operationDoc(metamodel, local, operation)}export type ${local} = ${typeOf(operation.input, sent, 0, false)};`,
|
|
214
243
|
];
|
|
215
244
|
if (!answered.has(named)) {
|
|
216
245
|
answered.add(named);
|
|
217
246
|
claim(named, `the answer of ${identity}`);
|
|
218
247
|
written.push(`/** What \`${local}\` answers. */\nexport type ${named} = ${answer};`);
|
|
219
248
|
}
|
|
220
|
-
if (named !== `${local}Answer`) {
|
|
221
|
-
claim(`${local}Answer`, `the old name of the answer of ${identity}`);
|
|
222
|
-
written.push(`/** @deprecated The backend calls it \`${named}\`: use that name. */\nexport type ${local}Answer = ${named};`);
|
|
223
|
-
}
|
|
224
249
|
return written;
|
|
225
250
|
});
|
|
251
|
+
// An event is a fact received whole: every field is there, as in a record a read answers. An
|
|
252
|
+
// event whose class is also a model — a context's base event, when a Feature hears it — is
|
|
253
|
+
// that model's type, narrowed to its own name: one class, one type.
|
|
254
|
+
const members = [];
|
|
255
|
+
const events = sorted(metamodel.events ?? {}).flatMap(([name, event]) => {
|
|
256
|
+
const type = identifierOf(event.title);
|
|
257
|
+
if (event.title in metamodel.models) {
|
|
258
|
+
members.push(`(${type} & { name: ${JSON.stringify(name)} })`);
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
claim(type, `event ${name}`);
|
|
262
|
+
members.push(type);
|
|
263
|
+
return [
|
|
264
|
+
`${eventDoc(metamodel, name, event)}export type ${type} = ${typeOf(event.schema, scope, 0, true)};`,
|
|
265
|
+
];
|
|
266
|
+
});
|
|
267
|
+
if (members.length > 0) {
|
|
268
|
+
claim("DomainEvent", "the union of the domain events");
|
|
269
|
+
events.push(`${docOf([`Every domain event of «${aliasOf(metamodel)}» in the contract: a \`switch\` on \`name\` tells which.`], "")}export type DomainEvent =\n${members.map((one) => ` | ${one}`).join("\n")};`);
|
|
270
|
+
}
|
|
271
|
+
const inputs = new Map();
|
|
272
|
+
for (let pending = [...scope.inputs]; pending.length > 0;) {
|
|
273
|
+
for (const name of pending) {
|
|
274
|
+
const schema = metamodel.models[name];
|
|
275
|
+
inputs.set(name, `${docOf([`What a caller sends of a \`${name}\`: what the contract requires; the server fills the rest.`], "")}export type ${identifierOf(name)}Input = ${typeOf(schema, sent, 0, false)};`);
|
|
276
|
+
}
|
|
277
|
+
pending = [...scope.inputs].filter((name) => !inputs.has(name));
|
|
278
|
+
}
|
|
279
|
+
const sentModels = [...inputs.keys()].sort().map((name) => {
|
|
280
|
+
claim(`${identifierOf(name)}Input`, `what a caller sends of model ${name}`);
|
|
281
|
+
return inputs.get(name);
|
|
282
|
+
});
|
|
226
283
|
const written = new Map();
|
|
227
284
|
for (let pending = [...scope.reached]; pending.length > 0;) {
|
|
228
285
|
for (const name of pending) {
|
|
@@ -244,5 +301,5 @@ export function domainFileOf(metamodel) {
|
|
|
244
301
|
const imports = [...byPackage.keys()]
|
|
245
302
|
.sort()
|
|
246
303
|
.map((from) => `import type { ${byPackage.get(from).join(", ")} } from "${from}";`);
|
|
247
|
-
return `${[headerOf("The DTOs", metamodel), ...(imports.length === 0 ? [] : [imports.join("\n")]), ...models, ...shared, ...operations].join("\n\n")}\n`;
|
|
304
|
+
return `${[headerOf("The DTOs", metamodel), ...(imports.length === 0 ? [] : [imports.join("\n")]), ...models, ...sentModels, ...shared, ...operations, ...events].join("\n\n")}\n`;
|
|
248
305
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import type { Metamodel } from "
|
|
1
|
+
import type { Metamodel } from "../domain/metamodel.js";
|
|
2
2
|
import { type GenerateOptions } from "./rest.js";
|
|
3
3
|
/**
|
|
4
4
|
* The TypeScript of the entities of `metamodel`, as the text of one file next to its `domain.ts`.
|
|
5
5
|
*
|
|
6
6
|
* 1. Per model, by name: the one `search` a transport reads for it (`own` when there are
|
|
7
7
|
* several) — none, and the model gets no entity, said in the file's own doc.
|
|
8
|
-
* 2. Its `get`, `edit` and `preview` the same way, the record's field written `{id}
|
|
9
|
-
* `
|
|
8
|
+
* 2. Its `get`, `edit` and `preview` the same way, the record's field written `{id}`; its
|
|
9
|
+
* `events` — a record's `DomainEvents` — as `history`, the same way; and its `export` — every
|
|
10
|
+
* record of a criteria as a file — by its path.
|
|
10
11
|
* 3. Every write of role `command` that concerns it: with `target` when its body names the
|
|
11
12
|
* record, as it is when it creates one; its body typed without that field.
|
|
12
13
|
* Final: an `entity<T, C>()` per model listed, keyed `<domain>:<kind>`, and `entities`, all of
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { entityKeyOf } from "
|
|
2
|
-
import { aliasOf, answerOf, claimed, DOMAIN_MODULE, FRAMEWORK, headerOf, identifierOf, isEntity, localOf, methodOf, pluralOf, sorted, } from "
|
|
1
|
+
import { entityKeyOf } from "../../client/domain/contract.js";
|
|
2
|
+
import { aliasOf, answerOf, claimed, DOMAIN_MODULE, FRAMEWORK, headerOf, identifierOf, isEntity, localOf, methodOf, pluralOf, sorted, } from "../domain/names.js";
|
|
3
3
|
import { ownerOf, restOf, WRITES, } from "./rest.js";
|
|
4
4
|
/** The one operation of `role` for `entity` a transport can call, by `own` when there are many. */
|
|
5
5
|
function oneOf(candidates, role) {
|
|
@@ -90,6 +90,19 @@ function entitySourceOf(metamodel, model, domain, options, claim) {
|
|
|
90
90
|
const route = `${write.found.rest.method} ${recordPath(write.found.rest, field)}`;
|
|
91
91
|
routes.push(` ${role}: ${JSON.stringify(route)},`);
|
|
92
92
|
}
|
|
93
|
+
// A record's events (sincpro_framework's `DomainEvents`): its history, read by the record's key.
|
|
94
|
+
const events = oneOf(reads("events", isGet), "events");
|
|
95
|
+
if (events !== undefined) {
|
|
96
|
+
if ("missing" in events)
|
|
97
|
+
missing.push(`history: ${events.missing}`);
|
|
98
|
+
else {
|
|
99
|
+
const field = events.found.operation.targets[model];
|
|
100
|
+
if (field !== undefined && events.found.rest.fields.length === 1)
|
|
101
|
+
routes.push(` history: ${JSON.stringify(recordPath(events.found.rest, field))},`);
|
|
102
|
+
else
|
|
103
|
+
missing.push(`history: \`${events.found.local}\` is at ${events.found.rest.path}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
93
106
|
const exported = oneOf(reads("export", isGet), "export");
|
|
94
107
|
if (exported !== undefined) {
|
|
95
108
|
if ("missing" in exported)
|
|
@@ -142,8 +155,9 @@ function entitySourceOf(metamodel, model, domain, options, claim) {
|
|
|
142
155
|
*
|
|
143
156
|
* 1. Per model, by name: the one `search` a transport reads for it (`own` when there are
|
|
144
157
|
* several) — none, and the model gets no entity, said in the file's own doc.
|
|
145
|
-
* 2. Its `get`, `edit` and `preview` the same way, the record's field written `{id}
|
|
146
|
-
* `
|
|
158
|
+
* 2. Its `get`, `edit` and `preview` the same way, the record's field written `{id}`; its
|
|
159
|
+
* `events` — a record's `DomainEvents` — as `history`, the same way; and its `export` — every
|
|
160
|
+
* record of a criteria as a file — by its path.
|
|
147
161
|
* 3. Every write of role `command` that concerns it: with `target` when its body names the
|
|
148
162
|
* record, as it is when it creates one; its body typed without that field.
|
|
149
163
|
* Final: an `entity<T, C>()` per model listed, keyed `<domain>:<kind>`, and `entities`, all of
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Metamodel } from "
|
|
1
|
+
import type { Metamodel } from "../domain/metamodel.js";
|
|
2
2
|
/**
|
|
3
3
|
* A bounded context's skeleton, written ONCE beside what is generated — the files that are the
|
|
4
4
|
* application's from then on:
|
|
@@ -7,7 +7,7 @@ import type { Metamodel } from "./metamodel.js";
|
|
|
7
7
|
* generated/ `sincpro-client generate` — regenerated, never edited
|
|
8
8
|
* infrastructure/dependencies.ts what the services can count on: the generated client
|
|
9
9
|
* infrastructure/bounded_context.ts its Feature and ApplicationService, and how it is made
|
|
10
|
-
* services/index.ts
|
|
10
|
+
* services/index.ts registers command classes and their services
|
|
11
11
|
* index.ts `export const project` — ready to use
|
|
12
12
|
*
|
|
13
13
|
* Context: the layout sincpro_framework gives a service (`dependencies.py`, `framework.py`,
|
|
@@ -17,8 +17,11 @@ import type { Metamodel } from "./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,9 +1,8 @@
|
|
|
1
|
-
import { aliasOf, FRAMEWORK } from "
|
|
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
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"applicationServices",
|
|
4
|
+
"operations",
|
|
5
|
+
"registerServices",
|
|
7
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(" "),
|
|
8
7
|
]);
|
|
9
8
|
/** `sales-orders` → `SalesOrders`. */
|
|
@@ -27,11 +26,12 @@ export function scaffoldFilesOf(metamodel, options = {}) {
|
|
|
27
26
|
const name = TAKEN.has(bare) ? `${bare}BoundedContext` : bare;
|
|
28
27
|
const Dependencies = `${Name}Dependencies`;
|
|
29
28
|
const client = metamodel.bindings !== undefined;
|
|
30
|
-
const
|
|
29
|
+
const operations = options.operations ?? at("..", "common", "infrastructure", "operations");
|
|
31
30
|
const dependencies = [
|
|
32
31
|
HEADER(`What the services of «${alias}» can count on`),
|
|
33
32
|
"",
|
|
34
|
-
`import type {
|
|
33
|
+
`import type { OperationTransport } from "${FRAMEWORK}";`,
|
|
34
|
+
`import type { BoundedContext } from "${at(FRAMEWORK, "core")}";`,
|
|
35
35
|
...(client ? ["", `import { clientOf } from "${at("..", "generated", "client")}";`] : []),
|
|
36
36
|
"",
|
|
37
37
|
`/** What every service of «${alias}» finds on \`this\`${client ? ": `this.client`, the service's operations" : ""}. */`,
|
|
@@ -44,10 +44,10 @@ export function scaffoldFilesOf(metamodel, options = {}) {
|
|
|
44
44
|
"}",
|
|
45
45
|
"",
|
|
46
46
|
"export function registerDependencies(",
|
|
47
|
-
` bc:
|
|
48
|
-
` ${client ? "
|
|
47
|
+
` bc: BoundedContext<${Dependencies}>,`,
|
|
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
|
"",
|
|
@@ -58,60 +58,56 @@ export function scaffoldFilesOf(metamodel, options = {}) {
|
|
|
58
58
|
"import {",
|
|
59
59
|
" ApplicationService as BaseApplicationService,",
|
|
60
60
|
" Feature as BaseFeature,",
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
61
|
+
" BoundedContext,",
|
|
62
|
+
" type Command,",
|
|
63
|
+
" type DomainEvent,",
|
|
64
|
+
`} from "${at(FRAMEWORK, "core")}";`,
|
|
65
|
+
`import type { OperationTransport } from "${FRAMEWORK}";`,
|
|
64
66
|
"",
|
|
65
|
-
`import type { ${Name}Features } from "${at("..", "services")}";`,
|
|
66
67
|
`import { type ${Dependencies}, registerDependencies } from "${at(".", "dependencies")}";`,
|
|
67
68
|
"",
|
|
68
|
-
`/** A Feature of «${alias}
|
|
69
|
-
|
|
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> {",
|
|
71
|
+
...(client ? [` declare readonly client: ${Dependencies}["client"];`] : []),
|
|
72
|
+
"}",
|
|
70
73
|
"",
|
|
71
|
-
`/** An ApplicationService of «${alias}»: the same, and \`this.
|
|
72
|
-
"export class ApplicationService extends BaseApplicationService<",
|
|
73
|
-
` ${Dependencies}
|
|
74
|
-
|
|
75
|
-
"> {}",
|
|
74
|
+
`/** An ApplicationService of «${alias}»: the same, and \`this.featureBus.execute(dto)\`. */`,
|
|
75
|
+
"export abstract class ApplicationService<C extends Command<R> | DomainEvent, R> extends BaseApplicationService<C, R> {",
|
|
76
|
+
...(client ? [` declare readonly client: ${Dependencies}["client"];`] : []),
|
|
77
|
+
"}",
|
|
76
78
|
"",
|
|
77
|
-
`export function config${Name}BoundedContext(name: string,
|
|
78
|
-
` return registerDependencies(new
|
|
79
|
+
`export function config${Name}BoundedContext(name: string, operations: OperationTransport) {`,
|
|
80
|
+
` return registerDependencies(new BoundedContext<${Dependencies}>(name), operations);`,
|
|
79
81
|
"}",
|
|
80
82
|
"",
|
|
81
83
|
].join("\n");
|
|
82
84
|
const services = [
|
|
83
85
|
HEADER(`What «${alias}» offers`),
|
|
84
86
|
"",
|
|
85
|
-
"
|
|
86
|
-
`
|
|
87
|
-
" * operations of one part of the domain; an ApplicationService composes the Features:",
|
|
88
|
-
" *",
|
|
89
|
-
" * // services/issues.ts",
|
|
90
|
-
` * export class Issues extends Feature {`,
|
|
91
|
-
" * close(input: dto.CommandCloseIssue) {",
|
|
92
|
-
" * return this.client.closeIssue(input);",
|
|
93
|
-
" * }",
|
|
94
|
-
" * }",
|
|
95
|
-
" */",
|
|
96
|
-
"export const features = {};",
|
|
97
|
-
"export const applicationServices = {};",
|
|
87
|
+
`import type { BoundedContext } from "${at(FRAMEWORK, "core")}";`,
|
|
88
|
+
`import type { ${Dependencies} } from "${at("..", "infrastructure", "dependencies")}";`,
|
|
98
89
|
"",
|
|
99
|
-
|
|
90
|
+
"/** Register local Command classes with their Features and ApplicationServices.",
|
|
91
|
+
" * Generated DTO types are remote contracts, not runtime command constructors: import",
|
|
92
|
+
` * them under their own name — \`import type * as remote from "${at("..", "generated", "domain")}"\` —`,
|
|
93
|
+
" * and let a local Command implement one (`implements remote.CommandCancelIssue`).",
|
|
94
|
+
" * Register with context.feature(CommandClass)(FeatureClass) or",
|
|
95
|
+
" * context.appService(CommandClass)(ApplicationServiceClass).",
|
|
96
|
+
" */",
|
|
97
|
+
`export function registerServices(_context: BoundedContext<${Dependencies}>): void {}`,
|
|
100
98
|
"",
|
|
101
99
|
].join("\n");
|
|
102
100
|
const index = [
|
|
103
101
|
HEADER(`«${alias}», ready to use`),
|
|
104
102
|
"",
|
|
105
|
-
`import {
|
|
103
|
+
`import { operations } from "${operations}";`,
|
|
106
104
|
"",
|
|
107
105
|
`import { config${Name}BoundedContext } from "${at(".", "infrastructure", "bounded_context")}";`,
|
|
108
|
-
`import {
|
|
106
|
+
`import { registerServices } from "${at(".", "services")}";`,
|
|
109
107
|
"",
|
|
110
|
-
`/** «${alias}»: \`${name}
|
|
111
|
-
`export const ${name} = config${Name}BoundedContext(${JSON.stringify(alias)},
|
|
112
|
-
|
|
113
|
-
" applicationServices,",
|
|
114
|
-
"});",
|
|
108
|
+
`/** «${alias}»: \`${name}.execute(new Command(...))\`. */`,
|
|
109
|
+
`export const ${name} = config${Name}BoundedContext(${JSON.stringify(alias)}, operations);`,
|
|
110
|
+
`registerServices(${name});`,
|
|
115
111
|
"",
|
|
116
112
|
].join("\n");
|
|
117
113
|
return {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { FailureKind, NextStep, Problem } from "@sincpro/criteria";
|
|
2
|
+
/**
|
|
3
|
+
* What kind of failure an error is, whatever raised it — the backend's `common/failures`, read
|
|
4
|
+
* on a client: a refusal the server explained, a request nobody answered, a run a screen
|
|
5
|
+
* cancelled, a local domain's own refusal, another backend's error a platform translated.
|
|
6
|
+
*
|
|
7
|
+
* failureOf(new TransportError("/pay", 409, problem)) -> { kind: "conflict", nextStep: "refresh_and_retry", … }
|
|
8
|
+
* failureOf(new TransportError("/pay", null, null)) -> { kind: "unknown_outcome", fault: true, … }
|
|
9
|
+
* failureOf(new CreditExhausted()) -> what the class declares (`failureKind`)
|
|
10
|
+
* failureOf(new TypeError("x")) -> { kind: "internal", fault: true, … }
|
|
11
|
+
*
|
|
12
|
+
* Context: every client decides the same things about a failure — show it or report it, retry it
|
|
13
|
+
* or set it aside, ask to sign in again —, and each app was deciding them its own way. The words
|
|
14
|
+
* are the wire's (`FailureKind`, `NextStep`, `Problem` from @sincpro/criteria), the rules the
|
|
15
|
+
* backend's: a retryable kind is retried after `retryAfter`; a permanent one fails the same way on
|
|
16
|
+
* every attempt and is set aside; a fault is the system's problem and is reported. An error says
|
|
17
|
+
* its own kind (`failureKind`, as the backend's errors do), so nothing here knows who raised it:
|
|
18
|
+
* a platform's adapter translates its backend's errors into errors that declare one.
|
|
19
|
+
*/
|
|
20
|
+
export type Failure = {
|
|
21
|
+
readonly kind: FailureKind;
|
|
22
|
+
/** UPPER_SNAKE and stable: what a client switches on. */
|
|
23
|
+
readonly reason: string;
|
|
24
|
+
/** What the caller does next. */
|
|
25
|
+
readonly nextStep: NextStep;
|
|
26
|
+
/** Whether sending the same thing again — as it is, or after a fresh read — may succeed. */
|
|
27
|
+
readonly retryable: boolean;
|
|
28
|
+
/** Seconds to wait before retrying; none when retrying does not help. */
|
|
29
|
+
readonly retryAfter: number | undefined;
|
|
30
|
+
/** It fails the same way on every attempt: a queue sets it aside, never retries it. */
|
|
31
|
+
readonly permanent: boolean;
|
|
32
|
+
/** The system's problem — a bug, a server down, no answer —, not an answer to the caller. */
|
|
33
|
+
readonly fault: boolean;
|
|
34
|
+
/** Whoever asked stopped waiting: not a failure of anyone. */
|
|
35
|
+
readonly aborted: boolean;
|
|
36
|
+
/** What the server said, when it said it. */
|
|
37
|
+
readonly problem: Problem | undefined;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* What an error can say about itself — on the instance or as a static of its class:
|
|
41
|
+
*
|
|
42
|
+
* class CreditExhausted extends Error {
|
|
43
|
+
* static readonly failureKind = "exhausted";
|
|
44
|
+
* readonly retryAfter = 30;
|
|
45
|
+
* }
|
|
46
|
+
*/
|
|
47
|
+
export type DeclaredFailure = {
|
|
48
|
+
readonly failureKind?: FailureKind;
|
|
49
|
+
readonly reason?: string;
|
|
50
|
+
readonly retryAfter?: number;
|
|
51
|
+
};
|
|
52
|
+
/** The kinds that fail the same way on every attempt — set aside at the first failure. */
|
|
53
|
+
export declare const PERMANENT_KINDS: ReadonlySet<FailureKind>;
|
|
54
|
+
/**
|
|
55
|
+
* The kind each wire's own code stands for, when an answer carries no `Problem` — the backend's
|
|
56
|
+
* tables, read back, so a transport of any wire (REST, JSON-RPC, gRPC, another backend's) builds
|
|
57
|
+
* errors that declare the same kinds:
|
|
58
|
+
*
|
|
59
|
+
* class RpcError extends Error {
|
|
60
|
+
* constructor(readonly code: number, message: string) { super(message); }
|
|
61
|
+
* get failureKind() { return kindOfJsonRpcCode(this.code); }
|
|
62
|
+
* }
|
|
63
|
+
*/
|
|
64
|
+
export declare function kindOfHttpStatus(status: number): FailureKind;
|
|
65
|
+
/** The kind a gRPC status code stands for — the backend's gRPC table, read back. */
|
|
66
|
+
export declare function kindOfGrpcCode(code: number): FailureKind;
|
|
67
|
+
/** The kind a JSON-RPC error code stands for — the backend's catalogue, read back. */
|
|
68
|
+
export declare function kindOfJsonRpcCode(code: number): FailureKind;
|
|
69
|
+
/**
|
|
70
|
+
* The failure `error` is.
|
|
71
|
+
*
|
|
72
|
+
* 1. Whoever asked stopped waiting (`AbortError`): aborted — a write may have run, so its kind is
|
|
73
|
+
* `unknown_outcome`, but it is nobody's failure.
|
|
74
|
+
* 2. What the error declares (`failureKind`, `reason`, `retryAfter`), on itself or its class.
|
|
75
|
+
* 3. What the server said (`problem`, a `Problem` — the same over REST, JSON-RPC and gRPC): its
|
|
76
|
+
* kind, reason, next step and wait.
|
|
77
|
+
* 4. A REST answer (`TransportError`) with a status and no problem: the kind its HTTP status
|
|
78
|
+
* stands for. Another wire's transport declares its kind instead (`kindOfGrpcCode`,
|
|
79
|
+
* `kindOfJsonRpcCode`).
|
|
80
|
+
* 5. A request that got no answer (`status: null`): `unknown_outcome` — it may have run.
|
|
81
|
+
* Final: anything else is `internal`, a fault.
|
|
82
|
+
*/
|
|
83
|
+
export declare function failureOf(error: unknown): Failure;
|