@sincpro/client-framework 0.5.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.
- package/AGENTS.md +54 -25
- package/MANIFESTO.md +59 -39
- package/README.md +329 -328
- package/dist/client/{memory.d.ts → adapters/memory.d.ts} +1 -1
- package/dist/client/{memory.js → adapters/memory.js} +7 -5
- package/dist/client/adapters/parquet.d.ts +50 -0
- package/dist/client/adapters/parquet.js +151 -0
- package/dist/client/{rest.d.ts → adapters/rest.d.ts} +7 -1
- package/dist/client/{rest.js → adapters/rest.js} +48 -3
- package/dist/client/{entity.d.ts → domain/entity.d.ts} +22 -11
- package/dist/client/{entity.js → domain/entity.js} +16 -1
- package/dist/client/{transport.d.ts → domain/transport.d.ts} +37 -1
- package/dist/client/{transport.js → domain/transport.js} +6 -0
- package/dist/client/index.d.ts +16 -0
- package/dist/client/index.js +16 -0
- package/dist/client/services/propagation.d.ts +32 -0
- package/dist/{services/execution.js → client/services/propagation.js} +8 -21
- package/dist/client/{retry.js → services/retry.js} +1 -1
- package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +21 -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} +9 -8
- package/dist/codegen/index.d.ts +7 -7
- package/dist/codegen/index.js +5 -10
- package/dist/codegen/{client.d.ts → services/client.d.ts} +3 -1
- package/dist/codegen/{client.js → services/client.js} +34 -5
- package/dist/codegen/services/domain.d.ts +20 -0
- package/dist/codegen/{domain.js → services/domain.js} +121 -23
- package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -2
- package/dist/codegen/{entities.js → services/entities.js} +34 -4
- package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
- package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +2 -2
- package/dist/codegen/{scaffold.js → services/scaffold.js} +31 -36
- package/dist/common/collection.d.ts +110 -0
- package/dist/common/collection.js +169 -0
- package/dist/common/index.d.ts +10 -0
- package/dist/common/index.js +10 -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 +33 -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 +18 -0
- package/dist/core/domain/bus.js +18 -0
- package/dist/core/domain/dto.d.ts +32 -0
- package/dist/core/domain/dto.js +20 -0
- package/dist/core/domain/errors.d.ts +18 -0
- package/dist/core/domain/errors.js +1 -0
- package/dist/core/domain/services.d.ts +37 -0
- package/dist/core/domain/services.js +28 -0
- package/dist/core/entrypoint/bounded-context.d.ts +70 -0
- package/dist/core/entrypoint/bounded-context.js +252 -0
- package/dist/core/entrypoint/screens.d.ts +6 -0
- package/dist/core/entrypoint/screens.js +136 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +16 -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 +13 -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 +35 -0
- package/dist/core/services/execution.js +137 -0
- package/dist/index.d.ts +16 -36
- package/dist/index.js +16 -36
- package/dist/model/{meta.d.ts → domain/meta.d.ts} +1 -0
- 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 +42 -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 +105 -0
- package/dist/observability/services/outcome.d.ts +12 -0
- package/dist/observability/services/outcome.js +20 -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 +25 -8
- 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 -67
- /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/{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.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
|
@@ -26,7 +26,11 @@ export type Schema = {
|
|
|
26
26
|
type?: string;
|
|
27
27
|
relation?: string | null;
|
|
28
28
|
identity?: string;
|
|
29
|
+
/** A to-many whose records are other aggregates: listed and searched, never edited as lines. */
|
|
30
|
+
reference?: boolean;
|
|
29
31
|
};
|
|
32
|
+
/** An answer that is bytes as they are produced (an export): the media types it may be. */
|
|
33
|
+
"x-sincpro-streamed"?: readonly string[];
|
|
30
34
|
};
|
|
31
35
|
export type Operation = {
|
|
32
36
|
/** What it is for its entity: `search`, `get`, `edit`, `command`… */
|
|
@@ -49,6 +53,21 @@ export type Operation = {
|
|
|
49
53
|
/** The operation that replaces it, by identity. */
|
|
50
54
|
replacement?: string | null;
|
|
51
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
|
+
};
|
|
52
71
|
/** How REST reaches an operation: its method and its whole path, `{field}` where a field goes. */
|
|
53
72
|
export type RestBinding = {
|
|
54
73
|
method: string;
|
|
@@ -76,14 +95,14 @@ export type Metamodel = {
|
|
|
76
95
|
/** The name the gateways publish it under (`FastApiGateway({"project": …})`): what a client
|
|
77
96
|
* names the context by. */
|
|
78
97
|
alias?: string;
|
|
79
|
-
/** The bounded context's name, as sincpro_framework 3.32 wrote it before `bounded_context`. */
|
|
80
|
-
context?: string;
|
|
81
98
|
version: string;
|
|
82
99
|
/** Every shape the contract names: an entity (it has an `identity`) or a value it holds. */
|
|
83
100
|
models: Readonly<Record<string, Schema>>;
|
|
84
101
|
/** By the Command's identity, `"<bounded_context>.<Class>"`. */
|
|
85
102
|
operations: Readonly<Record<string, Operation>>;
|
|
86
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>>;
|
|
87
106
|
/** By wire, then by the Command's identity. */
|
|
88
107
|
bindings?: Bindings;
|
|
89
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:
|
|
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.
|
|
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:
|
|
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
|
|
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.
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { scaffoldFilesOf } from "
|
|
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
|
-
*
|
|
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
|
*
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -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 {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
export
|
|
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";
|
package/dist/codegen/index.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 "
|
|
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 "
|
|
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
|
|
@@ -7,17 +7,20 @@ import { ownerOf, restOf, WRITES, } from "./rest.js";
|
|
|
7
7
|
*
|
|
8
8
|
* Context: a method only writes down the route the contract published and hands the call to the
|
|
9
9
|
* transport, which keeps the rules — the `Idempotency-Key`, the `Problem`, the model's version.
|
|
10
|
-
* A GET is a page (`list`), one record (`one`),
|
|
11
|
-
* a
|
|
10
|
+
* A GET is a page (`list`), one record (`one`), a file (`export`: `downloadRoute`, its bytes as
|
|
11
|
+
* they arrive), or — a Query of the context's own, `GET /me` — a read of its answer as it came
|
|
12
|
+
* (`readRoute`, over a transport that has `read`). A GET's
|
|
12
13
|
* keys in its path are required parameters; every other field is an optional one.
|
|
13
14
|
*/
|
|
14
15
|
/** A search's fields a page asks with, not the screen: the criteria, and its cursor inside it. */
|
|
15
16
|
const PAGED = new Set(["criteria", "cursor"]);
|
|
17
|
+
/** An export's fields its method takes apart: the criteria and the format. */
|
|
18
|
+
const EXPORTED = new Set(["criteria", "cursor", "format"]);
|
|
16
19
|
/** A GET's parameters: the path's keys, and the rest of its input but what a page asks with. */
|
|
17
|
-
function askedOf(operation, rest, paged) {
|
|
20
|
+
function askedOf(operation, rest, paged, apart = PAGED) {
|
|
18
21
|
const path = [...rest.fields].sort();
|
|
19
22
|
const query = Object.keys(operation.input.properties ?? {})
|
|
20
|
-
.filter((one) => !path.includes(one) && !(paged &&
|
|
23
|
+
.filter((one) => !path.includes(one) && !(paged && apart.has(one)))
|
|
21
24
|
.sort();
|
|
22
25
|
return { path, query };
|
|
23
26
|
}
|
|
@@ -29,6 +32,9 @@ function callOf(metamodel, operation, rest) {
|
|
|
29
32
|
const owner = ownerOf(metamodel, operation);
|
|
30
33
|
if (operation.role === "search" && owner !== undefined && owner in metamodel.models)
|
|
31
34
|
return { kind: "list", entity: owner, ...askedOf(operation, rest, true) };
|
|
35
|
+
// An export answers bytes, never JSON: it is downloaded.
|
|
36
|
+
if (operation.role === "export" && owner !== undefined && owner in metamodel.models)
|
|
37
|
+
return { kind: "download", entity: owner, ...askedOf(operation, rest, true, EXPORTED) };
|
|
32
38
|
const field = owner === undefined ? undefined : operation.targets[owner];
|
|
33
39
|
const record = rest.fields.length === 1 && rest.fields[0] === field;
|
|
34
40
|
if (operation.role === "get" && owner !== undefined && field !== undefined && record)
|
|
@@ -78,6 +84,15 @@ function methodSourceOf(local, answer, method, call, rest, doc) {
|
|
|
78
84
|
const signature = params.signature === "" ? "" : `, ${params.signature}`;
|
|
79
85
|
return `${doc} ${method}: (criteria: Criteria<${entity}>${signature}) =>\n transport.list<${entity}>(${params.route(rest.path)}, criteria),`;
|
|
80
86
|
}
|
|
87
|
+
if (call.kind === "download") {
|
|
88
|
+
const entity = `dto.${identifierOf(call.entity)}`;
|
|
89
|
+
const params = paramsOf(call);
|
|
90
|
+
// The path's keys are required, so they come first: a required parameter never follows an
|
|
91
|
+
// optional one.
|
|
92
|
+
const keys = params.signature === "" ? "" : `${params.signature.replace("params?:", "params:")}, `;
|
|
93
|
+
const given = params.signature === "" ? "" : ", params";
|
|
94
|
+
return `${doc} ${method}: (${keys}criteria?: Criteria<${entity}>, format?: ExportFormat) =>\n downloadRoute(transport, exportRoute(${JSON.stringify(rest.path)}, criteria, format${given})),`;
|
|
95
|
+
}
|
|
81
96
|
if (call.kind === "read") {
|
|
82
97
|
const params = paramsOf(call);
|
|
83
98
|
return `${doc} ${method}: (${params.signature}) =>\n readRoute<dto.${answer}>(transport, ${params.route(rest.path)}),`;
|
|
@@ -108,6 +123,8 @@ function methodSourceOf(local, answer, method, call, rest, doc) {
|
|
|
108
123
|
* 2. Per operation REST reaches, how a transport calls it (`callOf`): a page, a record, a write
|
|
109
124
|
* — or none, said in the client's own doc.
|
|
110
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.
|
|
111
128
|
* Final: `clientOf(transport)`, a method per operation reached, named by `methodOf`; two with
|
|
112
129
|
* one name are refused, naming both.
|
|
113
130
|
*/
|
|
@@ -122,6 +139,7 @@ export function clientFileOf(metamodel, options = {}) {
|
|
|
122
139
|
let paged = false;
|
|
123
140
|
let reads = false;
|
|
124
141
|
let sends = false;
|
|
142
|
+
let downloads = false;
|
|
125
143
|
for (const [identity, operation] of operations) {
|
|
126
144
|
const local = localOf(metamodel, identity);
|
|
127
145
|
const rest = restOf(metamodel, identity, options);
|
|
@@ -135,6 +153,10 @@ export function clientFileOf(metamodel, options = {}) {
|
|
|
135
153
|
claim(method, `operation ${identity}`);
|
|
136
154
|
const doc = docOf([operation.description, deprecationOf(metamodel, operation)], " ");
|
|
137
155
|
methods.push(methodSourceOf(local, answerOf(metamodel, identity), method, call, rest, doc));
|
|
156
|
+
if (call.kind === "download") {
|
|
157
|
+
downloads = true;
|
|
158
|
+
paged = true;
|
|
159
|
+
}
|
|
138
160
|
if (call.kind === "list" || call.kind === "read") {
|
|
139
161
|
paged ||= call.kind === "list";
|
|
140
162
|
reads ||= call.kind === "read";
|
|
@@ -147,6 +169,7 @@ export function clientFileOf(metamodel, options = {}) {
|
|
|
147
169
|
}
|
|
148
170
|
const core = [
|
|
149
171
|
"bindable",
|
|
172
|
+
...(downloads ? ["downloadRoute", "type ExportFormat", "exportRoute"] : []),
|
|
150
173
|
...(lists ? ["listRoute"] : []),
|
|
151
174
|
...(sends ? ["pathFor"] : []),
|
|
152
175
|
...(reads ? ["readRoute"] : []),
|
|
@@ -160,6 +183,7 @@ export function clientFileOf(metamodel, options = {}) {
|
|
|
160
183
|
"",
|
|
161
184
|
`import type * as dto from "${DOMAIN_MODULE}";`,
|
|
162
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} },`);
|
|
163
187
|
const client = [
|
|
164
188
|
"/**",
|
|
165
189
|
` * «${aliasOf(metamodel)}» over a transport: a method for each operation REST reaches as a`,
|
|
@@ -174,6 +198,11 @@ export function clientFileOf(metamodel, options = {}) {
|
|
|
174
198
|
headerOf("The client", metamodel),
|
|
175
199
|
imports,
|
|
176
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
|
+
]),
|
|
177
206
|
...(routes.length === 0 ? [] : [`const routes = {\n${routes.join("\n")}\n};`]),
|
|
178
207
|
`${client}\nexport function clientOf(transport: Transport) {\n return bindable(transport, (transport) => ({\n${methods.join("\n")}\n }));\n}`,
|
|
179
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 "
|
|
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
|
|
@@ -20,7 +27,11 @@ const DEFINITIONS = "#/$defs/";
|
|
|
20
27
|
/** A shared definition a package of the stack already types: imported, never written again. */
|
|
21
28
|
const IMPORTED = {
|
|
22
29
|
Criteria: { name: "Criteria", from: "@sincpro/criteria" },
|
|
30
|
+
Download: { name: "Download", from: "@sincpro/client-framework" },
|
|
31
|
+
EntityCollection: { name: "EntityCollection", from: "@sincpro/client-framework" },
|
|
23
32
|
};
|
|
33
|
+
/** A relation that holds several records: a reading answers it as a page of them. */
|
|
34
|
+
const TO_MANY = new Set(["one2many", "many2many"]);
|
|
24
35
|
/** The field types whose value is the record itself, not a key that names it. */
|
|
25
36
|
const RELATIONAL = new Set(["many2one", "one2many", "many2many", "embedded"]);
|
|
26
37
|
/** A related record: in an answer only when the read expands it. */
|
|
@@ -42,11 +53,17 @@ function keyNote(schema) {
|
|
|
42
53
|
return undefined;
|
|
43
54
|
return `The key of a \`${said.relation}\`.`;
|
|
44
55
|
}
|
|
45
|
-
|
|
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) {
|
|
46
62
|
const definitions = metamodel.$defs ?? {};
|
|
47
63
|
const scope = {
|
|
48
|
-
reached: new Set(),
|
|
49
|
-
imports: new Set(),
|
|
64
|
+
reached: shared?.reached ?? new Set(),
|
|
65
|
+
imports: shared?.imports ?? new Set(),
|
|
66
|
+
inputs: shared?.inputs ?? new Set(),
|
|
50
67
|
refer: (ref) => {
|
|
51
68
|
const model = ref.startsWith(MODELS);
|
|
52
69
|
if (!model && !ref.startsWith(DEFINITIONS))
|
|
@@ -56,7 +73,10 @@ function scopeOf(metamodel) {
|
|
|
56
73
|
if (model || name in metamodel.models) {
|
|
57
74
|
if (!(name in metamodel.models))
|
|
58
75
|
throw new Error(`"${ref}" names no model of ${contextOf(metamodel)}`);
|
|
59
|
-
|
|
76
|
+
if (!input)
|
|
77
|
+
return identifierOf(name);
|
|
78
|
+
scope.inputs.add(name);
|
|
79
|
+
return `${identifierOf(name)}Input`;
|
|
60
80
|
}
|
|
61
81
|
const imported = IMPORTED[name];
|
|
62
82
|
if (imported !== undefined) {
|
|
@@ -82,6 +102,26 @@ function optional(name, property, schema, record) {
|
|
|
82
102
|
return EXPANDED.has(property["x-sincpro"]?.type ?? "");
|
|
83
103
|
return !(schema.required ?? []).includes(name);
|
|
84
104
|
}
|
|
105
|
+
/** The records a to-many holds, when `property` is one: the `items` of the collection a reading
|
|
106
|
+
* answers (`{items, count, cursor}`). */
|
|
107
|
+
function heldRecords(property) {
|
|
108
|
+
if (!TO_MANY.has(property["x-sincpro"]?.type ?? ""))
|
|
109
|
+
return undefined;
|
|
110
|
+
const held = property.properties?.["items"];
|
|
111
|
+
return held?.type === "array" ? held.items : undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A property's TypeScript. A to-many inside a record is an `EntityCollection` of its records —
|
|
115
|
+
* a page with its count and cursor, as the read answers it — never an array, which would say the
|
|
116
|
+
* page is all there is.
|
|
117
|
+
*/
|
|
118
|
+
function propertyTypeOf(property, scope, depth, record) {
|
|
119
|
+
const records = record ? heldRecords(property) : undefined;
|
|
120
|
+
if (records === undefined)
|
|
121
|
+
return typeOf(property, scope, depth, record);
|
|
122
|
+
scope.imports.add("EntityCollection");
|
|
123
|
+
return `EntityCollection<${typeOf(records, scope, depth, record)}>`;
|
|
124
|
+
}
|
|
85
125
|
function objectOf(schema, scope, depth, record) {
|
|
86
126
|
const properties = sorted(schema.properties ?? {});
|
|
87
127
|
if (properties.length === 0) {
|
|
@@ -95,7 +135,7 @@ function objectOf(schema, scope, depth, record) {
|
|
|
95
135
|
const lines = properties.map(([name, property]) => {
|
|
96
136
|
const doc = docOf([property.description, keyNote(property)], pad);
|
|
97
137
|
const mark = optional(name, property, schema, record) ? "?" : "";
|
|
98
|
-
return `${doc}${pad}${keyOf(name)}${mark}: ${
|
|
138
|
+
return `${doc}${pad}${keyOf(name)}${mark}: ${propertyTypeOf(property, scope, depth + 1, record)};`;
|
|
99
139
|
});
|
|
100
140
|
return `{\n${lines.join("\n")}\n${" ".repeat(depth)}}`;
|
|
101
141
|
}
|
|
@@ -133,6 +173,13 @@ function typeOf(schema, scope, depth, record) {
|
|
|
133
173
|
: objectOf(schema, scope, depth, record);
|
|
134
174
|
}
|
|
135
175
|
}
|
|
176
|
+
/** What a streamed answer is on the client — an export's bytes (`Download`) —, else nothing. */
|
|
177
|
+
function streamed(output, scope) {
|
|
178
|
+
if (output["x-sincpro-streamed"] === undefined)
|
|
179
|
+
return undefined;
|
|
180
|
+
scope.imports.add("Download");
|
|
181
|
+
return "Download";
|
|
182
|
+
}
|
|
136
183
|
/** What the server says of an operation: its words, then what it is, for whom, on which field. */
|
|
137
184
|
function operationDoc(metamodel, local, operation) {
|
|
138
185
|
const runsOn = sorted(operation.targets).map(([entity, field]) => `\`${entity}\` in \`${field}\``);
|
|
@@ -145,21 +192,37 @@ function operationDoc(metamodel, local, operation) {
|
|
|
145
192
|
].filter((one) => one !== undefined);
|
|
146
193
|
return (docOf([operation.description, `${facts.join(" · ")}.`, deprecationOf(metamodel, operation)], "") || `/** ${local} */\n`);
|
|
147
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
|
+
}
|
|
148
205
|
/**
|
|
149
206
|
* The TypeScript of every DTO of `metamodel`, as the text of one file.
|
|
150
207
|
*
|
|
151
208
|
* 1. The models, by name: an entity's contract, a related model by its own name.
|
|
152
209
|
* 2. Each operation, by its local name (its identity without `<context>.`): its input as
|
|
153
|
-
* `<Name>` and its answer by the backend's own name for it (`answerOf`: `ResponseX`)
|
|
154
|
-
* `<Name>Answer`
|
|
155
|
-
* 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
|
|
156
218
|
* never one a package of the stack already types (`Criteria`).
|
|
157
219
|
* Final: a header that says where it came from and not to edit it, the imports, then models,
|
|
158
|
-
* definitions and
|
|
159
|
-
* refused, naming both.
|
|
220
|
+
* definitions, operations and events, each sorted by name; two of them with one TypeScript
|
|
221
|
+
* name are refused, naming both.
|
|
160
222
|
*/
|
|
161
223
|
export function domainFileOf(metamodel) {
|
|
162
224
|
const scope = scopeOf(metamodel);
|
|
225
|
+
const sent = scopeOf(metamodel, scope, true);
|
|
163
226
|
const definitions = metamodel.$defs ?? {};
|
|
164
227
|
const claim = claimed();
|
|
165
228
|
const models = sorted(metamodel.models).map(([name, schema]) => {
|
|
@@ -172,21 +235,51 @@ export function domainFileOf(metamodel) {
|
|
|
172
235
|
const local = localOf(metamodel, identity);
|
|
173
236
|
const named = answerOf(metamodel, identity);
|
|
174
237
|
claim(local, `operation ${identity}`);
|
|
175
|
-
const answer = operation.output === null
|
|
238
|
+
const answer = operation.output === null
|
|
239
|
+
? "unknown"
|
|
240
|
+
: (streamed(operation.output, scope) ?? typeOf(operation.output, scope, 0, false));
|
|
176
241
|
const written = [
|
|
177
|
-
`${operationDoc(metamodel, local, operation)}export type ${local} = ${typeOf(operation.input,
|
|
242
|
+
`${operationDoc(metamodel, local, operation)}export type ${local} = ${typeOf(operation.input, sent, 0, false)};`,
|
|
178
243
|
];
|
|
179
244
|
if (!answered.has(named)) {
|
|
180
245
|
answered.add(named);
|
|
181
246
|
claim(named, `the answer of ${identity}`);
|
|
182
247
|
written.push(`/** What \`${local}\` answers. */\nexport type ${named} = ${answer};`);
|
|
183
248
|
}
|
|
184
|
-
if (named !== `${local}Answer`) {
|
|
185
|
-
claim(`${local}Answer`, `the old name of the answer of ${identity}`);
|
|
186
|
-
written.push(`/** @deprecated The backend calls it \`${named}\`: use that name. */\nexport type ${local}Answer = ${named};`);
|
|
187
|
-
}
|
|
188
249
|
return written;
|
|
189
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
|
+
});
|
|
190
283
|
const written = new Map();
|
|
191
284
|
for (let pending = [...scope.reached]; pending.length > 0;) {
|
|
192
285
|
for (const name of pending) {
|
|
@@ -199,9 +292,14 @@ export function domainFileOf(metamodel) {
|
|
|
199
292
|
claim(identifierOf(name), `definition ${name}`);
|
|
200
293
|
return written.get(name);
|
|
201
294
|
});
|
|
202
|
-
|
|
295
|
+
// One line per package, its names sorted.
|
|
296
|
+
const byPackage = new Map();
|
|
297
|
+
for (const name of [...scope.imports].sort()) {
|
|
203
298
|
const imported = IMPORTED[name];
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
299
|
+
byPackage.set(imported.from, [...(byPackage.get(imported.from) ?? []), imported.name]);
|
|
300
|
+
}
|
|
301
|
+
const imports = [...byPackage.keys()]
|
|
302
|
+
.sort()
|
|
303
|
+
.map((from) => `import type { ${byPackage.get(from).join(", ")} } from "${from}";`);
|
|
304
|
+
return `${[headerOf("The DTOs", metamodel), ...(imports.length === 0 ? [] : [imports.join("\n")]), ...models, ...sentModels, ...shared, ...operations, ...events].join("\n\n")}\n`;
|
|
207
305
|
}
|
|
@@ -1,11 +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}
|
|
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.
|
|
9
11
|
* 3. Every write of role `command` that concerns it: with `target` when its body names the
|
|
10
12
|
* record, as it is when it creates one; its body typed without that field.
|
|
11
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) {
|
|
@@ -61,10 +61,18 @@ function entitySourceOf(metamodel, model, domain, options, claim) {
|
|
|
61
61
|
const missing = [];
|
|
62
62
|
const routes = [` list: ${JSON.stringify(list.found.rest.path)},`];
|
|
63
63
|
const one = oneOf(reads("get", isGet), "get");
|
|
64
|
+
const edit = oneOf(reads("edit", isWrite), "edit");
|
|
64
65
|
if (one !== undefined && "found" in one) {
|
|
65
66
|
const field = one.found.operation.targets[model];
|
|
67
|
+
// sincpro_framework's `Get` answers the caller's own draft only when asked (`draft`), and an
|
|
68
|
+
// entity edited through `Edit` keeps its drafts there: its record is read with them, or a
|
|
69
|
+
// draft kept on one visit never comes back on the next.
|
|
70
|
+
const drafts = edit !== undefined &&
|
|
71
|
+
"found" in edit &&
|
|
72
|
+
one.found.operation.input.properties?.["draft"]?.type === "boolean";
|
|
73
|
+
const path = `${recordPath(one.found.rest, field)}${drafts ? "?draft=true" : ""}`;
|
|
66
74
|
if (field !== undefined && one.found.rest.fields.length === 1)
|
|
67
|
-
routes.push(` one: ${JSON.stringify(
|
|
75
|
+
routes.push(` one: ${JSON.stringify(path)},`);
|
|
68
76
|
else
|
|
69
77
|
missing.push(`one: \`${one.found.local}\` is at ${one.found.rest.path}`);
|
|
70
78
|
}
|
|
@@ -82,6 +90,26 @@ function entitySourceOf(metamodel, model, domain, options, claim) {
|
|
|
82
90
|
const route = `${write.found.rest.method} ${recordPath(write.found.rest, field)}`;
|
|
83
91
|
routes.push(` ${role}: ${JSON.stringify(route)},`);
|
|
84
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
|
+
}
|
|
106
|
+
const exported = oneOf(reads("export", isGet), "export");
|
|
107
|
+
if (exported !== undefined) {
|
|
108
|
+
if ("missing" in exported)
|
|
109
|
+
missing.push(`export: ${exported.missing}`);
|
|
110
|
+
else
|
|
111
|
+
routes.push(` export: ${JSON.stringify(exported.found.rest.path)},`);
|
|
112
|
+
}
|
|
85
113
|
const commands = reached.filter((one) => one.operation.role === "command" &&
|
|
86
114
|
isWrite(one.rest.method) &&
|
|
87
115
|
one.operation.entities.includes(model));
|
|
@@ -127,7 +155,9 @@ function entitySourceOf(metamodel, model, domain, options, claim) {
|
|
|
127
155
|
*
|
|
128
156
|
* 1. Per model, by name: the one `search` a transport reads for it (`own` when there are
|
|
129
157
|
* several) — none, and the model gets no entity, said in the file's own doc.
|
|
130
|
-
* 2. Its `get`, `edit` and `preview` the same way, the record's field written `{id}
|
|
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.
|
|
131
161
|
* 3. Every write of role `command` that concerns it: with `target` when its body names the
|
|
132
162
|
* record, as it is when it creates one; its body typed without that field.
|
|
133
163
|
* Final: an `entity<T, C>()` per model listed, keyed `<domain>:<kind>`, and `entities`, all of
|