@sincpro/client-framework 0.6.0 → 1.0.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 (132) hide show
  1. package/AGENTS.md +54 -25
  2. package/MANIFESTO.md +59 -39
  3. package/README.md +324 -345
  4. package/dist/client/{memory.d.ts → adapters/memory.d.ts} +1 -1
  5. package/dist/client/{memory.js → adapters/memory.js} +2 -2
  6. package/dist/{adapters → client/adapters}/parquet.d.ts +1 -1
  7. package/dist/client/{rest.d.ts → adapters/rest.d.ts} +1 -1
  8. package/dist/client/{rest.js → adapters/rest.js} +2 -2
  9. package/dist/client/{entity.d.ts → domain/entity.d.ts} +6 -10
  10. package/dist/client/index.d.ts +16 -0
  11. package/dist/client/index.js +16 -0
  12. package/dist/client/services/propagation.d.ts +32 -0
  13. package/dist/{services/execution.js → client/services/propagation.js} +5 -21
  14. package/dist/client/{retry.js → services/retry.js} +1 -1
  15. package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +17 -2
  16. package/dist/codegen/{names.d.ts → domain/names.d.ts} +2 -2
  17. package/dist/codegen/{names.js → domain/names.js} +3 -3
  18. package/dist/codegen/{cli.js → entrypoint/cli.js} +9 -8
  19. package/dist/codegen/index.d.ts +7 -7
  20. package/dist/codegen/index.js +5 -10
  21. package/dist/codegen/{client.d.ts → services/client.d.ts} +3 -1
  22. package/dist/codegen/{client.js → services/client.js} +9 -1
  23. package/dist/codegen/services/domain.d.ts +20 -0
  24. package/dist/codegen/{domain.js → services/domain.js} +82 -25
  25. package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -3
  26. package/dist/codegen/{entities.js → services/entities.js} +18 -4
  27. package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
  28. package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +2 -2
  29. package/dist/codegen/{scaffold.js → services/scaffold.js} +31 -36
  30. package/dist/common/index.d.ts +10 -0
  31. package/dist/common/index.js +10 -0
  32. package/dist/context/adapters/memory.d.ts +6 -0
  33. package/dist/context/adapters/memory.js +34 -0
  34. package/dist/context/adapters/plain-codec.d.ts +3 -0
  35. package/dist/context/adapters/plain-codec.js +16 -0
  36. package/dist/context/{codec.d.ts → domain/codec.d.ts} +0 -1
  37. package/dist/context/{codec.js → domain/codec.js} +0 -14
  38. package/dist/context/domain/execution.d.ts +33 -0
  39. package/dist/context/domain/execution.js +11 -0
  40. package/dist/context/{store.d.ts → domain/store.d.ts} +0 -5
  41. package/dist/context/{store.js → domain/store.js} +1 -34
  42. package/dist/context/{context.d.ts → entrypoint/context.d.ts} +1 -1
  43. package/dist/context/index.d.ts +17 -0
  44. package/dist/context/index.js +15 -0
  45. package/dist/context/infrastructure/process.d.ts +13 -0
  46. package/dist/context/infrastructure/process.js +35 -0
  47. package/dist/core/domain/bus.d.ts +18 -0
  48. package/dist/core/domain/bus.js +18 -0
  49. package/dist/core/domain/dto.d.ts +32 -0
  50. package/dist/core/domain/dto.js +20 -0
  51. package/dist/core/domain/errors.d.ts +18 -0
  52. package/dist/core/domain/errors.js +1 -0
  53. package/dist/core/domain/services.d.ts +37 -0
  54. package/dist/core/domain/services.js +28 -0
  55. package/dist/core/entrypoint/bounded-context.d.ts +70 -0
  56. package/dist/core/entrypoint/bounded-context.js +252 -0
  57. package/dist/core/entrypoint/screens.d.ts +6 -0
  58. package/dist/core/entrypoint/screens.js +136 -0
  59. package/dist/core/index.d.ts +17 -0
  60. package/dist/core/index.js +16 -0
  61. package/dist/core/infrastructure/process.d.ts +56 -0
  62. package/dist/core/infrastructure/process.js +35 -0
  63. package/dist/core/services/binding.d.ts +6 -0
  64. package/dist/core/services/binding.js +13 -0
  65. package/dist/core/services/error-handlers.d.ts +11 -0
  66. package/dist/core/services/error-handlers.js +21 -0
  67. package/dist/core/services/execution.d.ts +35 -0
  68. package/dist/core/services/execution.js +137 -0
  69. package/dist/index.d.ts +16 -38
  70. package/dist/index.js +16 -38
  71. package/dist/model/index.d.ts +10 -0
  72. package/dist/model/index.js +10 -0
  73. package/dist/model/{metas.d.ts → infrastructure/metas.d.ts} +1 -1
  74. package/dist/model/{schema.d.ts → services/schema.d.ts} +1 -1
  75. package/dist/model/{schema.js → services/schema.js} +1 -1
  76. package/dist/observability/adapters/console.d.ts +12 -0
  77. package/dist/observability/adapters/console.js +35 -0
  78. package/dist/observability/adapters/memory.d.ts +41 -0
  79. package/dist/observability/adapters/memory.js +49 -0
  80. package/dist/{adapters → observability/adapters}/opentelemetry.d.ts +1 -1
  81. package/dist/{adapters → observability/adapters}/sentry.d.ts +1 -1
  82. package/dist/observability/domain/errors.d.ts +23 -0
  83. package/dist/observability/domain/errors.js +8 -0
  84. package/dist/observability/domain/logging.d.ts +42 -0
  85. package/dist/observability/domain/logging.js +1 -0
  86. package/dist/observability/domain/tracing.d.ts +49 -0
  87. package/dist/observability/domain/tracing.js +20 -0
  88. package/dist/observability/index.d.ts +16 -0
  89. package/dist/observability/index.js +4 -0
  90. package/dist/observability/infrastructure/registry.d.ts +21 -0
  91. package/dist/observability/infrastructure/registry.js +18 -0
  92. package/dist/observability/services/logging.d.ts +27 -0
  93. package/dist/observability/services/logging.js +105 -0
  94. package/dist/observability/services/outcome.d.ts +12 -0
  95. package/dist/observability/services/outcome.js +20 -0
  96. package/dist/{services/report.js → observability/services/reporting.js} +4 -3
  97. package/dist/testing/index.d.ts +3 -33
  98. package/dist/testing/index.js +5 -37
  99. package/package.json +16 -8
  100. package/dist/codegen/domain.d.ts +0 -15
  101. package/dist/observability/ports.d.ts +0 -65
  102. package/dist/observability/ports.js +0 -21
  103. package/dist/services/bounded-context.d.ts +0 -79
  104. package/dist/services/bounded-context.js +0 -261
  105. package/dist/services/execution.d.ts +0 -63
  106. package/dist/services/feature.d.ts +0 -85
  107. package/dist/services/feature.js +0 -86
  108. package/dist/services/process.d.ts +0 -60
  109. package/dist/services/process.js +0 -57
  110. package/dist/services/screens.d.ts +0 -2
  111. package/dist/services/screens.js +0 -76
  112. /package/dist/{adapters → client/adapters}/parquet.js +0 -0
  113. /package/dist/client/{contract.d.ts → domain/contract.d.ts} +0 -0
  114. /package/dist/client/{contract.js → domain/contract.js} +0 -0
  115. /package/dist/client/{entity.js → domain/entity.js} +0 -0
  116. /package/dist/client/{transport.d.ts → domain/transport.d.ts} +0 -0
  117. /package/dist/client/{transport.js → domain/transport.js} +0 -0
  118. /package/dist/client/{discovery.d.ts → services/discovery.d.ts} +0 -0
  119. /package/dist/client/{discovery.js → services/discovery.js} +0 -0
  120. /package/dist/client/{retry.d.ts → services/retry.d.ts} +0 -0
  121. /package/dist/codegen/{metamodel.js → domain/metamodel.js} +0 -0
  122. /package/dist/codegen/{cli.d.ts → entrypoint/cli.d.ts} +0 -0
  123. /package/dist/codegen/{check.d.ts → services/check.d.ts} +0 -0
  124. /package/dist/codegen/{check.js → services/check.js} +0 -0
  125. /package/dist/codegen/{rest.js → services/rest.js} +0 -0
  126. /package/dist/context/{context.js → entrypoint/context.js} +0 -0
  127. /package/dist/model/{meta.d.ts → domain/meta.d.ts} +0 -0
  128. /package/dist/model/{meta.js → domain/meta.js} +0 -0
  129. /package/dist/model/{metas.js → infrastructure/metas.js} +0 -0
  130. /package/dist/{adapters → observability/adapters}/opentelemetry.js +0 -0
  131. /package/dist/{adapters → observability/adapters}/sentry.js +0 -0
  132. /package/dist/{services/report.d.ts → observability/services/reporting.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { type Draft, type Method, type Transport } from "./transport.js";
1
+ import { type Draft, type Method, type Transport } from "../domain/transport.js";
2
2
  import type { AnyRecord, Criteria, Meta, Problem } from "@sincpro/criteria";
3
3
  /**
4
4
  * A server held in memory: tables of rows with their definition, answered with the same
@@ -1,5 +1,5 @@
1
- import { TransportError, } from "./transport.js";
2
- import { pack, readValues } from "../common/serialization.js";
1
+ import { TransportError, } from "../domain/transport.js";
2
+ import { pack, readValues } from "../../common/serialization.js";
3
3
  import { answer, answerGroups } from "@sincpro/criteria/engine";
4
4
  /**
5
5
  * A deep copy written and read by the wire's rules (`pack`, `readValues`): what a real server
@@ -1,4 +1,4 @@
1
- import type { Decoder } from "../client/transport.js";
1
+ import type { Decoder } from "../domain/transport.js";
2
2
  import type { AnyRecord, Count, Dropped, Envelope } from "@sincpro/criteria";
3
3
  /**
4
4
  * A page sincpro_framework answered as Parquet, read into what JSON would have sent — the
@@ -1,4 +1,4 @@
1
- import { type Decoder, type Transport } from "./transport.js";
1
+ import { type Decoder, type Transport } from "../domain/transport.js";
2
2
  import { type Criteria } from "@sincpro/criteria";
3
3
  /**
4
4
  * The REST wire of sincpro_framework (`FastApiGateway`).
@@ -1,5 +1,5 @@
1
- import { definitionOf, modelAt, } from "./discovery.js";
2
- import { TransportError, } from "./transport.js";
1
+ import { TransportError, } from "../domain/transport.js";
2
+ import { definitionOf, modelAt, } from "../services/discovery.js";
3
3
  import { readProblem } from "@sincpro/criteria";
4
4
  import { RestError, restSource } from "@sincpro/criteria/rest";
5
5
  /** The keys of an answer that are not the record: what is left is the record's own field. */
@@ -49,7 +49,7 @@ export type CommandAnswers = Readonly<Record<string, unknown>>;
49
49
  * save: "PUT /invoices/{id}",
50
50
  * preview: "POST /invoices/preview",
51
51
  * commands: { confirm: "POST /invoices/{id}/confirm", cancel: "POST /invoices/{id}/cancel" },
52
- * history: { route: "/billing-events", type: "Invoice" },
52
+ * history: "/invoices/{id}/events",
53
53
  * });
54
54
  *
55
55
  * Its Commands may be typed too — what each one is sent with, by name (`CommandBodies`), and
@@ -92,11 +92,9 @@ export type Entity<T extends AnyRecord = AnyRecord, C extends CommandBodies = Co
92
92
  * `action`. Declared, a save sends the record whole as the form holds it — not the fields
93
93
  * that changed under `save` — and it is also where the draft is kept. */
94
94
  edit?: WriteRoute;
95
- /** Its business events: the context's events route, and the `entity_type` they carry. */
96
- history?: {
97
- route: string;
98
- type: string;
99
- };
95
+ /** Its business events: where ONE record's events are read (sincpro_framework's
96
+ * `DomainEvents`), `{id}` where its key goes — a page of them, as a list answers. */
97
+ history?: string;
100
98
  /** Every record of a criteria as a file: the route `exportRoute` fills, `downloadRoute` asks. */
101
99
  export?: string;
102
100
  /** Which fields are its description, state, picture… when they are not the conventional
@@ -191,10 +189,8 @@ export type EntityRoutes<C extends CommandBodies = CommandBodies> = CommandRoute
191
189
  draft?: string;
192
190
  /** `"PUT /invoices/{id}/edit"`: the use case that answers sincpro_framework's `Edit`. */
193
191
  edit?: string;
194
- history?: {
195
- route: string;
196
- type: string;
197
- };
192
+ /** `"/invoices/{id}/events"`: a record's events (`DomainEvents[Invoice, …]`), oldest first. */
193
+ history?: string;
198
194
  /** `"/invoices/export"`: every record of a criteria as a file (`Export[Invoice, Streamed]`),
199
195
  * asked through `exportRoute` and `downloadRoute`. */
200
196
  export?: string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The seam with a server: the `Transport` port and its implementations, entities with typed
3
+ * commands, the service's client contract, and how a run's trace and context reach the wire.
4
+ *
5
+ * domain/ the port and the server's words: `Transport`, `TransportError`, `entity()`, the
6
+ * client contract
7
+ * adapters/ REST, memory, and Parquet (optional entry `./parquet`)
8
+ * services/ discovery, retry keys, propagation (`traceparent`, `sincpro-context`)
9
+ */
10
+ export { type MemoryOptions, type MemoryTable, memoryTransport, } from "./adapters/memory.js";
11
+ export { type RestOptions, restTransport, urlOf, type VersionDrift, } from "./adapters/rest.js";
12
+ export { type ClientContract, entityKeyOf, kindOf, metasOf, versionsOf, } from "./domain/contract.js";
13
+ export { bodyFor, type CommandAnswers, type CommandBodies, type CommandRoute, type Entity, entity, type EntityRef, type EntityRoutes, type ExportFormat, exportRoute, listRoute, NEW_PARAM, pathFor, PICK_PARAM, QUERY_PARAM, SCOPE_PARAM, scopeOfRef, startingValuesOf, type SummaryFields, type SummaryPart, TRAIL_PARAM, WITH_PARAM, type WriteRoute, writeRoute, } from "./domain/entity.js";
14
+ export { type CallOptions, type Decoder, type Download, downloadRoute, type Draft, type Method, type One, readRoute, type SendOptions, type Transport, TransportError, } from "./domain/transport.js";
15
+ export { bindable, bindTransport, headersOf, } from "./services/propagation.js";
16
+ export { type RetryKeys, retryKeys, unanswered, } from "./services/retry.js";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The seam with a server: the `Transport` port and its implementations, entities with typed
3
+ * commands, the service's client contract, and how a run's trace and context reach the wire.
4
+ *
5
+ * domain/ the port and the server's words: `Transport`, `TransportError`, `entity()`, the
6
+ * client contract
7
+ * adapters/ REST, memory, and Parquet (optional entry `./parquet`)
8
+ * services/ discovery, retry keys, propagation (`traceparent`, `sincpro-context`)
9
+ */
10
+ export { memoryTransport, } from "./adapters/memory.js";
11
+ export { restTransport, urlOf, } from "./adapters/rest.js";
12
+ export { entityKeyOf, kindOf, metasOf, versionsOf, } from "./domain/contract.js";
13
+ export { bodyFor, entity, exportRoute, listRoute, NEW_PARAM, pathFor, PICK_PARAM, QUERY_PARAM, SCOPE_PARAM, scopeOfRef, startingValuesOf, TRAIL_PARAM, WITH_PARAM, writeRoute, } from "./domain/entity.js";
14
+ export { downloadRoute, readRoute, TransportError, } from "./domain/transport.js";
15
+ export { bindable, bindTransport, headersOf, } from "./services/propagation.js";
16
+ export { retryKeys, unanswered, } from "./services/retry.js";
@@ -0,0 +1,32 @@
1
+ import type { Transport } from "../domain/transport.js";
2
+ import { type Execution } from "../../context/domain/execution.js";
3
+ /** Whether a value is a `Transport`: what a run binds to itself even without `BIND`. */
4
+ export declare function isTransport(value: unknown): value is Transport;
5
+ /**
6
+ * What every request of an execution carries: its trace (`traceparent`, W3C) and its context
7
+ * (`sincpro-context`, the header sincpro_framework's entrances read, written with `plainCodec`).
8
+ * Nothing when there is no trace and no context that travels.
9
+ */
10
+ export declare function headersOf(execution: Execution): Record<string, string>;
11
+ /**
12
+ * The transport, writing the execution's headers on every call — the caller's own headers of a
13
+ * call still win.
14
+ *
15
+ * Context: the transport's own headers (`restTransport({ headers })`) are the session's — the
16
+ * 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
+ */
19
+ export declare function bindTransport(transport: Transport, execution: Execution): Transport;
20
+ /**
21
+ * A client made over a transport, made again over the transport bound to each execution it is
22
+ * used in. What a generated `clientOf(transport)` answers, so a Feature that calls
23
+ * `this.client.sendIssue(…)` sends that request with its own trace.
24
+ *
25
+ * export function clientOf(transport: Transport) {
26
+ * return bindable(transport, (transport) => ({ sendIssue: (body) => transport.send(…) }));
27
+ * }
28
+ *
29
+ * 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.
31
+ */
32
+ export declare function bindable<T extends object>(transport: Transport, make: (transport: Transport) => T): T;
@@ -1,16 +1,8 @@
1
- import { CONTEXT_HEADER, plainCodec, travels } from "../context/codec.js";
2
- /**
3
- * A dependency that has something to say about the execution it is used in: given that
4
- * execution, it answers itself bound to it. The transport and every generated client are;
5
- * any other dependency of a bounded context is handed as it is.
6
- */
7
- export const BIND = Symbol.for("sincpro.client-framework.bind");
8
- function isBindable(value) {
9
- return (value !== null &&
10
- (typeof value === "object" || typeof value === "function") &&
11
- typeof value[BIND] === "function");
12
- }
13
- function isTransport(value) {
1
+ import { plainCodec } from "../../context/adapters/plain-codec.js";
2
+ import { CONTEXT_HEADER, travels } from "../../context/domain/codec.js";
3
+ import { BIND } from "../../context/domain/execution.js";
4
+ /** Whether a value is a `Transport`: what a run binds to itself even without `BIND`. */
5
+ export function isTransport(value) {
14
6
  if (value === null || typeof value !== "object")
15
7
  return false;
16
8
  const one = value;
@@ -83,11 +75,3 @@ export function bindable(transport, make) {
83
75
  });
84
76
  return client;
85
77
  }
86
- /** A dependency as the execution should see it: bound when it can be, as it is otherwise. */
87
- export function bindDependency(value, execution) {
88
- if (isBindable(value))
89
- return value[BIND](execution);
90
- if (isTransport(value))
91
- return bindTransport(value, execution);
92
- return value;
93
- }
@@ -1,4 +1,4 @@
1
- import { TransportError } from "./transport.js";
1
+ import { TransportError } from "../domain/transport.js";
2
2
  /** Whether a failure leaves it unknown if the Command ran — so a retry must reuse its key. */
3
3
  export function unanswered(error) {
4
4
  if (!(error instanceof TransportError))
@@ -53,6 +53,21 @@ export type Operation = {
53
53
  /** The operation that replaces it, by identity. */
54
54
  replacement?: string | null;
55
55
  };
56
+ /**
57
+ * A domain event of the bounded context — in a contract exported with its events
58
+ * (`export-client-contract --events`), keyed by its wire name (`DomainEvent.name`).
59
+ */
60
+ export type EventDescription = {
61
+ /** Its class's name on the server: the type it is written as. */
62
+ title: string;
63
+ description?: string | null;
64
+ /** One of the bounded context's own events: its aggregates record it. */
65
+ recorded: boolean;
66
+ /** A Feature of the bounded context is registered for it: it subscribes. */
67
+ handled: boolean;
68
+ /** Every field it carries — the envelope and what happened —, `name` a constant. */
69
+ schema: Schema;
70
+ };
56
71
  /** How REST reaches an operation: its method and its whole path, `{field}` where a field goes. */
57
72
  export type RestBinding = {
58
73
  method: string;
@@ -80,14 +95,14 @@ export type Metamodel = {
80
95
  /** The name the gateways publish it under (`FastApiGateway({"project": …})`): what a client
81
96
  * names the context by. */
82
97
  alias?: string;
83
- /** The bounded context's name, as sincpro_framework 3.32 wrote it before `bounded_context`. */
84
- context?: string;
85
98
  version: string;
86
99
  /** Every shape the contract names: an entity (it has an `identity`) or a value it holds. */
87
100
  models: Readonly<Record<string, Schema>>;
88
101
  /** By the Command's identity, `"<bounded_context>.<Class>"`. */
89
102
  operations: Readonly<Record<string, Operation>>;
90
103
  $defs?: Readonly<Record<string, Schema>>;
104
+ /** Its domain events, by wire name — only when the contract was exported with them. */
105
+ events?: Readonly<Record<string, EventDescription>>;
91
106
  /** By wire, then by the Command's identity. */
92
107
  bindings?: Bindings;
93
108
  };
@@ -13,7 +13,7 @@ export declare function keyOf(name: string): string;
13
13
  export declare function sorted<V>(record: Readonly<Record<string, V>>): [string, V][];
14
14
  /** `{ Issue: "issue_id" }`: a record of texts as TypeScript writes it, keys sorted. */
15
15
  export declare function recordLiteral(record: Readonly<Record<string, string>>): string;
16
- /** The bounded context's own name: `bounded_context`, or `context` in a 3.32 contract. */
16
+ /** The bounded context's own name: what every operation's identity starts with. */
17
17
  export declare function contextOf(metamodel: Metamodel): string;
18
18
  /** What a client names the context by: its `alias`, else its own name. */
19
19
  export declare function aliasOf(metamodel: Metamodel): string;
@@ -30,7 +30,7 @@ export declare function localOf(metamodel: Metamodel, identity: string): string;
30
30
  *
31
31
  * Context: the ubiquitous language. The backend writes `CommandCancelIssue` → `ResponseCancelIssue`,
32
32
  * and a client that calls it `CommandCancelIssueAnswer` makes a person learn two words for one
33
- * thing. The `<Name>Answer` it used to write stays as a deprecated alias of the backend's name.
33
+ * thing.
34
34
  */
35
35
  export declare function answerOf(metamodel: Metamodel, identity: string): string;
36
36
  /** The method an operation is called by: `CommandSendIssue` → `sendIssue`. */
@@ -32,9 +32,9 @@ export function recordLiteral(record) {
32
32
  const entries = sorted(record).map(([key, value]) => `${keyOf(key)}: ${JSON.stringify(value)}`);
33
33
  return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
34
34
  }
35
- /** The bounded context's own name: `bounded_context`, or `context` in a 3.32 contract. */
35
+ /** The bounded context's own name: what every operation's identity starts with. */
36
36
  export function contextOf(metamodel) {
37
- const name = metamodel.bounded_context ?? metamodel.context;
37
+ const name = metamodel.bounded_context;
38
38
  if (name === undefined)
39
39
  throw new Error("the contract names no bounded context");
40
40
  return name;
@@ -100,7 +100,7 @@ function answersOf(metamodel) {
100
100
  *
101
101
  * Context: the ubiquitous language. The backend writes `CommandCancelIssue` → `ResponseCancelIssue`,
102
102
  * and a client that calls it `CommandCancelIssueAnswer` makes a person learn two words for one
103
- * thing. The `<Name>Answer` it used to write stays as a deprecated alias of the backend's name.
103
+ * thing.
104
104
  */
105
105
  export function answerOf(metamodel, identity) {
106
106
  let known = ANSWERS.get(metamodel);
@@ -1,19 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import { CheckError, checkImports } from "./check.js";
5
- import { clientFileOf } from "./client.js";
6
- import { domainFileOf } from "./domain.js";
7
- import { entitiesFileOf } from "./entities.js";
8
- import { aliasOf } from "./names.js";
9
- import { scaffoldFilesOf } from "./scaffold.js";
4
+ import { aliasOf } from "../domain/names.js";
5
+ import { CheckError, checkImports } from "../services/check.js";
6
+ import { clientFileOf } from "../services/client.js";
7
+ import { domainFileOf } from "../services/domain.js";
8
+ import { entitiesFileOf } from "../services/entities.js";
9
+ import { scaffoldFilesOf } from "../services/scaffold.js";
10
10
  /**
11
11
  * `sincpro-client generate <contract.json> --out <dir> [--domain <name>] [--base <path>]` — what an
12
12
  * application's `make client` runs, once per bounded context, over the client contract
13
13
  * sincpro_framework exported (`export-client-contract`):
14
14
  *
15
- * - `domain.ts`: the DTOs, only types;
16
- * - `client.ts`: every operation as data, and `clientOf(transport)`;
15
+ * - `domain.ts`: the DTOs, only types — and the domain events with `DomainEvent`, their union,
16
+ * when the contract was exported with them (`export-client-contract --events`);
17
+ * - `client.ts`: every operation (and event) as data, and `clientOf(transport)`;
17
18
  * - `entities.ts`: an `entity<T, C>()` per model a screen can list;
18
19
  * - `contract.json`: the snapshot they were written from, for `metasOf`.
19
20
  *
@@ -3,10 +3,10 @@
3
3
  * sincpro_framework exports (PRD_01 phase B). A build step that runs in Node — nothing in
4
4
  * the runtime imports it (`make doctor`), and the package's root entry does not re-export it. `sincpro-client generate` (`cli.ts`) is the same, from a terminal.
5
5
  */
6
- export { CheckError, checkImports, type Place, placeOf, type Violation, violationOf, } from "./check.js";
7
- export { clientFileOf } from "./client.js";
8
- export { domainFileOf } from "./domain.js";
9
- export { entitiesFileOf } from "./entities.js";
10
- export type { Bindings, Metamodel, Operation, RestBinding, Schema, } from "./metamodel.js";
11
- export type { GenerateOptions } from "./rest.js";
12
- export { scaffoldFilesOf, type ScaffoldOptions, } from "./scaffold.js";
6
+ export type { Bindings, Metamodel, Operation, RestBinding, Schema, } from "./domain/metamodel.js";
7
+ export { CheckError, checkImports, type Place, placeOf, type Violation, violationOf, } from "./services/check.js";
8
+ export { clientFileOf } from "./services/client.js";
9
+ export { domainFileOf } from "./services/domain.js";
10
+ export { entitiesFileOf } from "./services/entities.js";
11
+ export type { GenerateOptions } from "./services/rest.js";
12
+ export { scaffoldFilesOf, type ScaffoldOptions, } from "./services/scaffold.js";
@@ -1,10 +1,5 @@
1
- /**
2
- * The generator: a bounded context's DTOs, client and entities out of the client contract
3
- * sincpro_framework exports (PRD_01 phase B). A build step that runs in Node — nothing in
4
- * the runtime imports it (`make doctor`), and the package's root entry does not re-export it. `sincpro-client generate` (`cli.ts`) is the same, from a terminal.
5
- */
6
- export { CheckError, checkImports, placeOf, violationOf, } from "./check.js";
7
- export { clientFileOf } from "./client.js";
8
- export { domainFileOf } from "./domain.js";
9
- export { entitiesFileOf } from "./entities.js";
10
- export { scaffoldFilesOf, } from "./scaffold.js";
1
+ export { CheckError, checkImports, placeOf, violationOf, } from "./services/check.js";
2
+ export { clientFileOf } from "./services/client.js";
3
+ export { domainFileOf } from "./services/domain.js";
4
+ export { entitiesFileOf } from "./services/entities.js";
5
+ export { scaffoldFilesOf, } from "./services/scaffold.js";
@@ -1,4 +1,4 @@
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 client of `metamodel`, as the text of one file next to its `domain.ts`.
@@ -8,6 +8,8 @@ import { type GenerateOptions } from "./rest.js";
8
8
  * 2. Per operation REST reaches, how a transport calls it (`callOf`): a page, a record, a write
9
9
  * — or none, said in the client's own doc.
10
10
  * 3. The routes of the writes, read once (`writeRoute`).
11
+ * 4. When the contract carries them, every domain event as data, by its type's name: its wire
12
+ * name, and whether the bounded context records it or handles it.
11
13
  * Final: `clientOf(transport)`, a method per operation reached, named by `methodOf`; two with
12
14
  * one name are refused, naming both.
13
15
  */
@@ -1,4 +1,4 @@
1
- import { aliasOf, answerOf, claimed, deprecationOf, docOf, DOMAIN_MODULE, FRAMEWORK, headerOf, identifierOf, localOf, methodOf, recordLiteral, sorted, } from "./names.js";
1
+ import { aliasOf, answerOf, claimed, deprecationOf, docOf, DOMAIN_MODULE, FRAMEWORK, headerOf, identifierOf, localOf, methodOf, recordLiteral, sorted, } from "../domain/names.js";
2
2
  import { ownerOf, restOf, WRITES, } from "./rest.js";
3
3
  /**
4
4
  * The client of a bounded context as TypeScript: every operation as data, and a method for each
@@ -123,6 +123,8 @@ function methodSourceOf(local, answer, method, call, rest, doc) {
123
123
  * 2. Per operation REST reaches, how a transport calls it (`callOf`): a page, a record, a write
124
124
  * — or none, said in the client's own doc.
125
125
  * 3. The routes of the writes, read once (`writeRoute`).
126
+ * 4. When the contract carries them, every domain event as data, by its type's name: its wire
127
+ * name, and whether the bounded context records it or handles it.
126
128
  * Final: `clientOf(transport)`, a method per operation reached, named by `methodOf`; two with
127
129
  * one name are refused, naming both.
128
130
  */
@@ -181,6 +183,7 @@ export function clientFileOf(metamodel, options = {}) {
181
183
  "",
182
184
  `import type * as dto from "${DOMAIN_MODULE}";`,
183
185
  ].join("\n");
186
+ const events = sorted(metamodel.events ?? {}).map(([name, event]) => ` ${identifierOf(event.title)}: { name: ${JSON.stringify(name)}, recorded: ${event.recorded}, handled: ${event.handled} },`);
184
187
  const client = [
185
188
  "/**",
186
189
  ` * «${aliasOf(metamodel)}» over a transport: a method for each operation REST reaches as a`,
@@ -195,6 +198,11 @@ export function clientFileOf(metamodel, options = {}) {
195
198
  headerOf("The client", metamodel),
196
199
  imports,
197
200
  `/** Every operation of «${aliasOf(metamodel)}», as data: what it is, and how each wire reaches it. */\nexport const operations = {\n${data.join("\n")}\n} as const;`,
201
+ ...(events.length === 0
202
+ ? []
203
+ : [
204
+ `/** 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
+ ]),
198
206
  ...(routes.length === 0 ? [] : [`const routes = {\n${routes.join("\n")}\n};`]),
199
207
  `${client}\nexport function clientOf(transport: Transport) {\n return bindable(transport, (transport) => ({\n${methods.join("\n")}\n }));\n}`,
200
208
  ].join("\n\n")}\n`;
@@ -0,0 +1,20 @@
1
+ import type { Metamodel } from "../domain/metamodel.js";
2
+ /**
3
+ * The TypeScript of every DTO of `metamodel`, as the text of one file.
4
+ *
5
+ * 1. The models, by name: an entity's contract, a related model by its own name.
6
+ * 2. Each operation, by its local name (its identity without `<context>.`): its input as
7
+ * `<Name>` and its answer by the backend's own name for it (`answerOf`: `ResponseX`) — or as
8
+ * `<Name>Answer` when it has no name.
9
+ * 3. A model an input carries — and the ones it carries, until none is new — as `<Name>Input`:
10
+ * what a caller sends follows the contract's `required`; the server fills the rest.
11
+ * 4. Each domain event, when the contract carries them, by its class's name (`title`): a record
12
+ * as it is received — every field present, `name` its wire name as a literal —, and
13
+ * `DomainEvent`, the union of them all, which a `switch` on `name` narrows.
14
+ * 5. The shared definitions those reach — and the ones those reach, until none is new —, but
15
+ * never one a package of the stack already types (`Criteria`).
16
+ * Final: a header that says where it came from and not to edit it, the imports, then models,
17
+ * definitions, operations and events, each sorted by name; two of them with one TypeScript
18
+ * name are refused, naming both.
19
+ */
20
+ export declare function domainFileOf(metamodel: Metamodel): string;
@@ -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