@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.
Files changed (176) hide show
  1. package/AGENTS.md +95 -25
  2. package/MANIFESTO.md +103 -40
  3. package/README.md +608 -333
  4. package/dist/circuit_breaker/domain/circuit.d.ts +29 -0
  5. package/dist/circuit_breaker/domain/circuit.js +37 -0
  6. package/dist/circuit_breaker/entrypoint/circuit-breaker.d.ts +56 -0
  7. package/dist/circuit_breaker/entrypoint/circuit-breaker.js +94 -0
  8. package/dist/circuit_breaker/index.d.ts +9 -0
  9. package/dist/circuit_breaker/index.js +9 -0
  10. package/dist/client/adapters/json-rpc.d.ts +40 -0
  11. package/dist/client/adapters/json-rpc.js +97 -0
  12. package/dist/client/{memory.d.ts → adapters/memory.d.ts} +1 -1
  13. package/dist/client/{memory.js → adapters/memory.js} +2 -2
  14. package/dist/{adapters → client/adapters}/parquet.d.ts +1 -1
  15. package/dist/client/adapters/rest-operations.d.ts +19 -0
  16. package/dist/client/adapters/rest-operations.js +66 -0
  17. package/dist/client/{rest.d.ts → adapters/rest.d.ts} +1 -1
  18. package/dist/client/{rest.js → adapters/rest.js} +2 -2
  19. package/dist/client/{entity.d.ts → domain/entity.d.ts} +6 -10
  20. package/dist/client/domain/operation.d.ts +73 -0
  21. package/dist/client/domain/operation.js +19 -0
  22. package/dist/client/index.d.ts +20 -0
  23. package/dist/client/index.js +20 -0
  24. package/dist/client/services/guarded.d.ts +17 -0
  25. package/dist/client/services/guarded.js +19 -0
  26. package/dist/client/services/propagation.d.ts +45 -0
  27. package/dist/{services/execution.js → client/services/propagation.js} +49 -36
  28. package/dist/client/{retry.js → services/retry.js} +1 -1
  29. package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +17 -2
  30. package/dist/codegen/{names.d.ts → domain/names.d.ts} +2 -2
  31. package/dist/codegen/{names.js → domain/names.js} +3 -3
  32. package/dist/codegen/{cli.js → entrypoint/cli.js} +15 -14
  33. package/dist/codegen/index.d.ts +7 -7
  34. package/dist/codegen/index.js +5 -10
  35. package/dist/codegen/services/client.d.ts +15 -0
  36. package/dist/codegen/services/client.js +111 -0
  37. package/dist/codegen/services/domain.d.ts +20 -0
  38. package/dist/codegen/{domain.js → services/domain.js} +82 -25
  39. package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -3
  40. package/dist/codegen/{entities.js → services/entities.js} +18 -4
  41. package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
  42. package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +8 -5
  43. package/dist/codegen/{scaffold.js → services/scaffold.js} +39 -43
  44. package/dist/common/failures.d.ts +83 -0
  45. package/dist/common/failures.js +212 -0
  46. package/dist/common/index.d.ts +11 -0
  47. package/dist/common/index.js +11 -0
  48. package/dist/context/adapters/memory.d.ts +6 -0
  49. package/dist/context/adapters/memory.js +34 -0
  50. package/dist/context/adapters/plain-codec.d.ts +3 -0
  51. package/dist/context/adapters/plain-codec.js +16 -0
  52. package/dist/context/{codec.d.ts → domain/codec.d.ts} +0 -1
  53. package/dist/context/{codec.js → domain/codec.js} +0 -14
  54. package/dist/context/domain/execution.d.ts +45 -0
  55. package/dist/context/domain/execution.js +11 -0
  56. package/dist/context/{store.d.ts → domain/store.d.ts} +0 -5
  57. package/dist/context/{store.js → domain/store.js} +1 -34
  58. package/dist/context/{context.d.ts → entrypoint/context.d.ts} +1 -1
  59. package/dist/context/index.d.ts +17 -0
  60. package/dist/context/index.js +15 -0
  61. package/dist/context/infrastructure/process.d.ts +13 -0
  62. package/dist/context/infrastructure/process.js +35 -0
  63. package/dist/core/domain/bus.d.ts +27 -0
  64. package/dist/core/domain/bus.js +26 -0
  65. package/dist/core/domain/dto.d.ts +62 -0
  66. package/dist/core/domain/dto.js +43 -0
  67. package/dist/core/domain/errors.d.ts +18 -0
  68. package/dist/core/domain/errors.js +1 -0
  69. package/dist/core/domain/pipeline.d.ts +89 -0
  70. package/dist/core/domain/pipeline.js +1 -0
  71. package/dist/core/domain/services.d.ts +47 -0
  72. package/dist/core/domain/services.js +31 -0
  73. package/dist/core/entrypoint/bounded-context.d.ts +139 -0
  74. package/dist/core/entrypoint/bounded-context.js +426 -0
  75. package/dist/core/entrypoint/screens.d.ts +6 -0
  76. package/dist/core/entrypoint/screens.js +136 -0
  77. package/dist/core/index.d.ts +19 -0
  78. package/dist/core/index.js +17 -0
  79. package/dist/core/infrastructure/listeners.d.ts +28 -0
  80. package/dist/core/infrastructure/listeners.js +34 -0
  81. package/dist/core/infrastructure/process.d.ts +56 -0
  82. package/dist/core/infrastructure/process.js +35 -0
  83. package/dist/core/services/binding.d.ts +6 -0
  84. package/dist/core/services/binding.js +16 -0
  85. package/dist/core/services/error-handlers.d.ts +11 -0
  86. package/dist/core/services/error-handlers.js +21 -0
  87. package/dist/core/services/execution.d.ts +66 -0
  88. package/dist/core/services/execution.js +216 -0
  89. package/dist/core/services/interceptors.d.ts +15 -0
  90. package/dist/core/services/interceptors.js +25 -0
  91. package/dist/core/services/listening.d.ts +17 -0
  92. package/dist/core/services/listening.js +25 -0
  93. package/dist/event_driven/adapters/memory-event-store.d.ts +10 -0
  94. package/dist/event_driven/adapters/memory-event-store.js +23 -0
  95. package/dist/event_driven/adapters/stored-queue.d.ts +67 -0
  96. package/dist/event_driven/adapters/stored-queue.js +192 -0
  97. package/dist/event_driven/adapters/sync-queue.d.ts +11 -0
  98. package/dist/event_driven/adapters/sync-queue.js +19 -0
  99. package/dist/event_driven/domain/envelope.d.ts +28 -0
  100. package/dist/event_driven/domain/envelope.js +19 -0
  101. package/dist/event_driven/domain/queue.d.ts +40 -0
  102. package/dist/event_driven/domain/queue.js +1 -0
  103. package/dist/event_driven/domain/store.d.ts +56 -0
  104. package/dist/event_driven/domain/store.js +1 -0
  105. package/dist/event_driven/entrypoint/publisher.d.ts +26 -0
  106. package/dist/event_driven/entrypoint/publisher.js +35 -0
  107. package/dist/event_driven/entrypoint/subscriber.d.ts +34 -0
  108. package/dist/event_driven/entrypoint/subscriber.js +51 -0
  109. package/dist/event_driven/index.d.ts +19 -0
  110. package/dist/event_driven/index.js +17 -0
  111. package/dist/index.d.ts +21 -38
  112. package/dist/index.js +21 -38
  113. package/dist/model/index.d.ts +10 -0
  114. package/dist/model/index.js +10 -0
  115. package/dist/model/{metas.d.ts → infrastructure/metas.d.ts} +1 -1
  116. package/dist/model/{schema.d.ts → services/schema.d.ts} +1 -1
  117. package/dist/model/{schema.js → services/schema.js} +1 -1
  118. package/dist/observability/adapters/console.d.ts +12 -0
  119. package/dist/observability/adapters/console.js +35 -0
  120. package/dist/observability/adapters/memory.d.ts +41 -0
  121. package/dist/observability/adapters/memory.js +49 -0
  122. package/dist/{adapters → observability/adapters}/opentelemetry.d.ts +1 -1
  123. package/dist/{adapters → observability/adapters}/sentry.d.ts +1 -1
  124. package/dist/observability/domain/errors.d.ts +23 -0
  125. package/dist/observability/domain/errors.js +8 -0
  126. package/dist/observability/domain/logging.d.ts +44 -0
  127. package/dist/observability/domain/logging.js +1 -0
  128. package/dist/observability/domain/tracing.d.ts +49 -0
  129. package/dist/observability/domain/tracing.js +20 -0
  130. package/dist/observability/index.d.ts +16 -0
  131. package/dist/observability/index.js +4 -0
  132. package/dist/observability/infrastructure/registry.d.ts +21 -0
  133. package/dist/observability/infrastructure/registry.js +18 -0
  134. package/dist/observability/services/logging.d.ts +27 -0
  135. package/dist/observability/services/logging.js +106 -0
  136. package/dist/observability/services/outcome.d.ts +12 -0
  137. package/dist/observability/services/outcome.js +17 -0
  138. package/dist/{services/report.js → observability/services/reporting.js} +4 -3
  139. package/dist/testing/index.d.ts +3 -33
  140. package/dist/testing/index.js +5 -37
  141. package/package.json +16 -8
  142. package/dist/codegen/client.d.ts +0 -14
  143. package/dist/codegen/client.js +0 -201
  144. package/dist/codegen/domain.d.ts +0 -15
  145. package/dist/observability/ports.d.ts +0 -65
  146. package/dist/observability/ports.js +0 -21
  147. package/dist/services/bounded-context.d.ts +0 -79
  148. package/dist/services/bounded-context.js +0 -261
  149. package/dist/services/execution.d.ts +0 -63
  150. package/dist/services/feature.d.ts +0 -85
  151. package/dist/services/feature.js +0 -86
  152. package/dist/services/process.d.ts +0 -60
  153. package/dist/services/process.js +0 -57
  154. package/dist/services/screens.d.ts +0 -2
  155. package/dist/services/screens.js +0 -76
  156. /package/dist/{adapters → client/adapters}/parquet.js +0 -0
  157. /package/dist/client/{contract.d.ts → domain/contract.d.ts} +0 -0
  158. /package/dist/client/{contract.js → domain/contract.js} +0 -0
  159. /package/dist/client/{entity.js → domain/entity.js} +0 -0
  160. /package/dist/client/{transport.d.ts → domain/transport.d.ts} +0 -0
  161. /package/dist/client/{transport.js → domain/transport.js} +0 -0
  162. /package/dist/client/{discovery.d.ts → services/discovery.d.ts} +0 -0
  163. /package/dist/client/{discovery.js → services/discovery.js} +0 -0
  164. /package/dist/client/{retry.d.ts → services/retry.d.ts} +0 -0
  165. /package/dist/codegen/{metamodel.js → domain/metamodel.js} +0 -0
  166. /package/dist/codegen/{cli.d.ts → entrypoint/cli.d.ts} +0 -0
  167. /package/dist/codegen/{check.d.ts → services/check.d.ts} +0 -0
  168. /package/dist/codegen/{check.js → services/check.js} +0 -0
  169. /package/dist/codegen/{rest.js → services/rest.js} +0 -0
  170. /package/dist/context/{context.js → entrypoint/context.js} +0 -0
  171. /package/dist/model/{meta.d.ts → domain/meta.d.ts} +0 -0
  172. /package/dist/model/{meta.js → domain/meta.js} +0 -0
  173. /package/dist/model/{metas.js → infrastructure/metas.js} +0 -0
  174. /package/dist/{adapters → observability/adapters}/opentelemetry.js +0 -0
  175. /package/dist/{adapters → observability/adapters}/sentry.js +0 -0
  176. /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 "./names.js";
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, and per operation's
5
- * input and answer. Only types — nothing in the file runs.
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
- function scopeOf(metamodel) {
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
- return identifierOf(name);
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
- * The records a to-many holds, when `property` is one — however the contract writes it: the
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"] ?? property;
98
- return held.type === "array" ? held.items : undefined;
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`), with
188
- * `<Name>Answer` kept as a deprecated alias — or as `<Name>Answer` when it has no name.
189
- * 3. The shared definitions those reach — and the ones those reach, until none is new —, but
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 operations, each sorted by name; two of them with one TypeScript name are
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, scope, 0, false)};`,
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 "./metamodel.js";
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}`, and its
9
- * `export` — every record of a criteria as a file — by its path.
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 "../client/contract.js";
2
- import { aliasOf, answerOf, claimed, DOMAIN_MODULE, FRAMEWORK, headerOf, identifierOf, isEntity, localOf, methodOf, pluralOf, sorted, } from "./names.js";
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}`, and its
146
- * `export` — every record of a criteria as a file — by its path.
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, Operation } from "./metamodel.js";
1
+ import type { Metamodel, Operation } from "../domain/metamodel.js";
2
2
  /**
3
3
  * How REST reaches each operation of a contract, as the generated client and entities write it.
4
4
  *
@@ -1,4 +1,4 @@
1
- import type { Metamodel } from "./metamodel.js";
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 the services, by the name each is reached by
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
- /** Where the application's transport is exported (`export const transport`), from the
21
- * bounded context's folder. `../common/infrastructure/transport` by default. */
22
- transport?: string;
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 "./names.js";
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
- "transport",
5
- "features",
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 transport = options.transport ?? at("..", "common", "infrastructure", "transport");
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 { Transport, UseBoundedContext } from "${FRAMEWORK}";`,
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: UseBoundedContext<${Dependencies}>,`,
48
- ` ${client ? "transport" : "_transport"}: Transport,`,
47
+ ` bc: BoundedContext<${Dependencies}>,`,
48
+ ` ${client ? "operations" : "_operations"}: OperationTransport,`,
49
49
  ") {",
50
- ...(client ? [` bc.addDependency("client", clientOf(transport));`] : []),
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
- " type Transport,",
62
- " UseBoundedContext,",
63
- `} from "${FRAMEWORK}";`,
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}»: its dependencies typed on \`this\`, and \`this.context\`. */`,
69
- `export class Feature extends BaseFeature<${Dependencies}> {}`,
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.feature.<Name>\` for each Feature. */`,
72
- "export class ApplicationService extends BaseApplicationService<",
73
- ` ${Dependencies},`,
74
- ` ${Name}Features`,
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, transport: Transport) {`,
78
- ` return registerDependencies(new UseBoundedContext<${Dependencies}>(name), transport);`,
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
- ` * Each service by the name it is reached by — \`${name}.Issues.close(…)\`. A Feature groups the`,
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
- `export type ${Name}Features = typeof features;`,
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 { transport } from "${transport}";`,
103
+ `import { operations } from "${operations}";`,
106
104
  "",
107
105
  `import { config${Name}BoundedContext } from "${at(".", "infrastructure", "bounded_context")}";`,
108
- `import { applicationServices, features } from "${at(".", "services")}";`,
106
+ `import { registerServices } from "${at(".", "services")}";`,
109
107
  "",
110
- `/** «${alias}»: \`${name}.<Service>.<method>(…)\` — from a screen, a test, another bounded context. */`,
111
- `export const ${name} = config${Name}BoundedContext(${JSON.stringify(alias)}, transport).register({`,
112
- " features,",
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;