@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Metamodel } from "
|
|
1
|
+
import type { Metamodel } from "../domain/metamodel.js";
|
|
2
2
|
/**
|
|
3
3
|
* A bounded context's skeleton, written ONCE beside what is generated — the files that are the
|
|
4
4
|
* application's from then on:
|
|
@@ -7,7 +7,7 @@ import type { Metamodel } from "./metamodel.js";
|
|
|
7
7
|
* generated/ `sincpro-client generate` — regenerated, never edited
|
|
8
8
|
* infrastructure/dependencies.ts what the services can count on: the generated client
|
|
9
9
|
* infrastructure/bounded_context.ts its Feature and ApplicationService, and how it is made
|
|
10
|
-
* services/index.ts
|
|
10
|
+
* services/index.ts registers command classes and their services
|
|
11
11
|
* index.ts `export const project` — ready to use
|
|
12
12
|
*
|
|
13
13
|
* Context: the layout sincpro_framework gives a service (`dependencies.py`, `framework.py`,
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { aliasOf, FRAMEWORK } from "
|
|
1
|
+
import { aliasOf, FRAMEWORK } from "../domain/names.js";
|
|
2
2
|
/** Names the skeleton's `index.ts` cannot give its constant. */
|
|
3
3
|
const TAKEN = new Set([
|
|
4
4
|
"transport",
|
|
5
|
-
"
|
|
6
|
-
"applicationServices",
|
|
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`. */
|
|
@@ -31,7 +30,8 @@ export function scaffoldFilesOf(metamodel, options = {}) {
|
|
|
31
30
|
const dependencies = [
|
|
32
31
|
HEADER(`What the services of «${alias}» can count on`),
|
|
33
32
|
"",
|
|
34
|
-
`import type { Transport
|
|
33
|
+
`import type { Transport } 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,7 +44,7 @@ export function scaffoldFilesOf(metamodel, options = {}) {
|
|
|
44
44
|
"}",
|
|
45
45
|
"",
|
|
46
46
|
"export function registerDependencies(",
|
|
47
|
-
` bc:
|
|
47
|
+
` bc: BoundedContext<${Dependencies}>,`,
|
|
48
48
|
` ${client ? "transport" : "_transport"}: Transport,`,
|
|
49
49
|
") {",
|
|
50
50
|
...(client ? [` bc.addDependency("client", clientOf(transport));`] : []),
|
|
@@ -58,45 +58,42 @@ export function scaffoldFilesOf(metamodel, options = {}) {
|
|
|
58
58
|
"import {",
|
|
59
59
|
" ApplicationService as BaseApplicationService,",
|
|
60
60
|
" Feature as BaseFeature,",
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
`} from "${FRAMEWORK}";`,
|
|
61
|
+
" BoundedContext,",
|
|
62
|
+
" type Command,",
|
|
63
|
+
`} from "${at(FRAMEWORK, "core")}";`,
|
|
64
|
+
`import type { Transport } from "${FRAMEWORK}";`,
|
|
64
65
|
"",
|
|
65
|
-
`import type { ${Name}Features } from "${at("..", "services")}";`,
|
|
66
66
|
`import { type ${Dependencies}, registerDependencies } from "${at(".", "dependencies")}";`,
|
|
67
67
|
"",
|
|
68
68
|
`/** A Feature of «${alias}»: its dependencies typed on \`this\`, and \`this.context\`. */`,
|
|
69
|
-
|
|
69
|
+
"export abstract class Feature<C extends Command<R>, R> extends BaseFeature<C, R> {",
|
|
70
|
+
...(client ? [` declare readonly client: ${Dependencies}["client"];`] : []),
|
|
71
|
+
"}",
|
|
70
72
|
"",
|
|
71
|
-
`/** An ApplicationService of «${alias}»: the same, and \`this.
|
|
72
|
-
"export class ApplicationService extends BaseApplicationService<",
|
|
73
|
-
` ${Dependencies}
|
|
74
|
-
|
|
75
|
-
"> {}",
|
|
73
|
+
`/** An ApplicationService of «${alias}»: the same, and \`this.featureBus.execute(dto)\`. */`,
|
|
74
|
+
"export abstract class ApplicationService<C extends Command<R>, R> extends BaseApplicationService<C, R> {",
|
|
75
|
+
...(client ? [` declare readonly client: ${Dependencies}["client"];`] : []),
|
|
76
|
+
"}",
|
|
76
77
|
"",
|
|
77
78
|
`export function config${Name}BoundedContext(name: string, transport: Transport) {`,
|
|
78
|
-
` return registerDependencies(new
|
|
79
|
+
` return registerDependencies(new BoundedContext<${Dependencies}>(name), transport);`,
|
|
79
80
|
"}",
|
|
80
81
|
"",
|
|
81
82
|
].join("\n");
|
|
82
83
|
const services = [
|
|
83
84
|
HEADER(`What «${alias}» offers`),
|
|
84
85
|
"",
|
|
85
|
-
"
|
|
86
|
-
`
|
|
87
|
-
" * operations of one part of the domain; an ApplicationService composes the Features:",
|
|
88
|
-
" *",
|
|
89
|
-
" * // services/issues.ts",
|
|
90
|
-
` * export class Issues extends Feature {`,
|
|
91
|
-
" * close(input: dto.CommandCloseIssue) {",
|
|
92
|
-
" * return this.client.closeIssue(input);",
|
|
93
|
-
" * }",
|
|
94
|
-
" * }",
|
|
95
|
-
" */",
|
|
96
|
-
"export const features = {};",
|
|
97
|
-
"export const applicationServices = {};",
|
|
86
|
+
`import type { BoundedContext } from "${at(FRAMEWORK, "core")}";`,
|
|
87
|
+
`import type { ${Dependencies} } from "${at("..", "infrastructure", "dependencies")}";`,
|
|
98
88
|
"",
|
|
99
|
-
|
|
89
|
+
"/** Register local Command classes with their Features and ApplicationServices.",
|
|
90
|
+
" * Generated DTO types are remote contracts, not runtime command constructors: import",
|
|
91
|
+
` * them under their own name — \`import type * as remote from "${at("..", "generated", "domain")}"\` —`,
|
|
92
|
+
" * and let a local Command implement one (`implements remote.CommandCancelIssue`).",
|
|
93
|
+
" * Register with context.feature(CommandClass)(FeatureClass) or",
|
|
94
|
+
" * context.appService(CommandClass)(ApplicationServiceClass).",
|
|
95
|
+
" */",
|
|
96
|
+
`export function registerServices(_context: BoundedContext<${Dependencies}>): void {}`,
|
|
100
97
|
"",
|
|
101
98
|
].join("\n");
|
|
102
99
|
const index = [
|
|
@@ -105,13 +102,11 @@ export function scaffoldFilesOf(metamodel, options = {}) {
|
|
|
105
102
|
`import { transport } from "${transport}";`,
|
|
106
103
|
"",
|
|
107
104
|
`import { config${Name}BoundedContext } from "${at(".", "infrastructure", "bounded_context")}";`,
|
|
108
|
-
`import {
|
|
105
|
+
`import { registerServices } from "${at(".", "services")}";`,
|
|
109
106
|
"",
|
|
110
|
-
`/** «${alias}»: \`${name}
|
|
111
|
-
`export const ${name} = config${Name}BoundedContext(${JSON.stringify(alias)}, transport)
|
|
112
|
-
|
|
113
|
-
" applicationServices,",
|
|
114
|
-
"});",
|
|
107
|
+
`/** «${alias}»: \`${name}.execute(new Command(...))\`. */`,
|
|
108
|
+
`export const ${name} = config${Name}BoundedContext(${JSON.stringify(alias)}, transport);`,
|
|
109
|
+
`registerServices(${name});`,
|
|
115
110
|
"",
|
|
116
111
|
].join("\n");
|
|
117
112
|
return {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { AnyRecord, Count, Criteria } from "@sincpro/criteria";
|
|
2
|
+
/**
|
|
3
|
+
* A to-many as a reading answers it — sincpro_framework's `EntityCollection`: the records, how
|
|
4
|
+
* many there are, and the cursor to go on inside this parent. Never a bare list, which would
|
|
5
|
+
* say the page is all there is.
|
|
6
|
+
*
|
|
7
|
+
* customer.invoices -> { items: [i1, …, i40], count: { value: 1000, exact: false }, cursor: "eyJ…" }
|
|
8
|
+
*
|
|
9
|
+
* Context: a customer may have a million invoices, and the read brings forty of them with
|
|
10
|
+
* «at least a thousand» — the backend counts up to a cap (`RELATION_COUNT_CAP`) because an exact
|
|
11
|
+
* total reads every child. A screen that counted `items` showed forty and called it the total;
|
|
12
|
+
* one that iterated an array never knew there was more. The same type on every platform, so the
|
|
13
|
+
* web, the phone and a script read a relation the same way.
|
|
14
|
+
*
|
|
15
|
+
* `count` is `null` when the read asked for none (`CountMode.NONE`): the cursor alone says there
|
|
16
|
+
* is more. A collection is written back only whole — the backend refuses a page of it, because
|
|
17
|
+
* written back it would remove what it did not bring.
|
|
18
|
+
*/
|
|
19
|
+
export type EntityCollection<T> = {
|
|
20
|
+
readonly items: readonly T[];
|
|
21
|
+
readonly count?: Count | null;
|
|
22
|
+
readonly cursor?: string | null;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A collection with every part said — what `collectionOf` answers —, edited the way a string or
|
|
26
|
+
* a date is: `append`, `extend`, `remove` and `replace` answer a new collection and never change
|
|
27
|
+
* this one, as sincpro_framework's `EntityCollection` does.
|
|
28
|
+
*
|
|
29
|
+
* const lines = collectionOf(invoice.lines);
|
|
30
|
+
* const next = lines.append(newLine).remove(oldLine.id); // lines is as it was
|
|
31
|
+
*
|
|
32
|
+
* An edit keeps what the collection says: a page stays a page (its cursor; a floor stays a
|
|
33
|
+
* floor), an exact count moves with what was added or removed. `remove` and `replace` of a record
|
|
34
|
+
* it does not hold throw, as Python's `list.remove` does. Written as JSON it is the wire's shape:
|
|
35
|
+
* `{ items, count, cursor }`.
|
|
36
|
+
*/
|
|
37
|
+
export declare class HeldCollection<T> implements EntityCollection<T> {
|
|
38
|
+
#private;
|
|
39
|
+
readonly items: readonly T[];
|
|
40
|
+
readonly count: Count | null;
|
|
41
|
+
readonly cursor: string | null;
|
|
42
|
+
constructor(items: readonly T[], count?: Count | null, cursor?: string | null, identity?: string);
|
|
43
|
+
/** How many records are held — this page, never the total (`count` is that). */
|
|
44
|
+
get length(): number;
|
|
45
|
+
/** Whether it holds less than there is (`isPartial`). */
|
|
46
|
+
get partial(): boolean;
|
|
47
|
+
[Symbol.iterator](): Iterator<T>;
|
|
48
|
+
/** A new collection with `records` at the end. */
|
|
49
|
+
append(...records: T[]): HeldCollection<T>;
|
|
50
|
+
/** A new collection with every one of `records` at the end, in order. */
|
|
51
|
+
extend(records: Iterable<T>): HeldCollection<T>;
|
|
52
|
+
/** A new collection without these records — each named by itself or by its identity. */
|
|
53
|
+
remove(...records: unknown[]): HeldCollection<T>;
|
|
54
|
+
/** A new collection where each of `records` takes the place of the held one with its identity. */
|
|
55
|
+
replace(...records: T[]): HeldCollection<T>;
|
|
56
|
+
toJSON(): EntityCollection<T>;
|
|
57
|
+
}
|
|
58
|
+
/** Whether `value` is a collection as a reading answers one (`{items, …}`). */
|
|
59
|
+
export declare function isCollection(value: unknown): value is EntityCollection<unknown>;
|
|
60
|
+
/**
|
|
61
|
+
* Whatever a relation arrived as, as a collection with every part said.
|
|
62
|
+
*
|
|
63
|
+
* collectionOf({ items: [a, b], count: { value: 30, exact: true }, cursor: "x" })
|
|
64
|
+
* -> the same, `cursor` and `count` said
|
|
65
|
+
* collectionOf([a, b]) -> { items: [a, b], count: { value: 2, exact: true }, cursor: null }
|
|
66
|
+
* collectionOf(undefined) -> { items: [], count: { value: 0, exact: true }, cursor: null }
|
|
67
|
+
*
|
|
68
|
+
* An array — a domain written by hand, a record built in memory — holds all there is. `identity`
|
|
69
|
+
* is the field `remove` and `replace` match records by (the model's `identity`, `"id"`).
|
|
70
|
+
*/
|
|
71
|
+
export declare function collectionOf<T>(value: EntityCollection<T> | readonly T[] | null | undefined, identity?: string): HeldCollection<T>;
|
|
72
|
+
/**
|
|
73
|
+
* Whether the collection holds less than there is — sincpro_framework's `is_partial`, the same
|
|
74
|
+
* rule: a cursor says there is more; no count claims nothing; a floor, or a count above what it
|
|
75
|
+
* holds, says there is more.
|
|
76
|
+
*
|
|
77
|
+
* isPartial({ items: [a], cursor: "x" }) -> true
|
|
78
|
+
* isPartial({ items: [a], count: { value: 1000, exact: false } }) -> true
|
|
79
|
+
* isPartial({ items: [a, b], count: { value: 2, exact: true } }) -> false
|
|
80
|
+
* isPartial({ items: [a] }) -> false
|
|
81
|
+
*/
|
|
82
|
+
export declare function isPartial(collection: EntityCollection<unknown>): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* How many there are, as well as the collection knows: its count, or — when the read asked for
|
|
85
|
+
* none — what it holds, exact only when nothing says there is more.
|
|
86
|
+
*
|
|
87
|
+
* totalOf({ items: [a, b], count: { value: 1000, exact: false } }) -> { value: 1000, exact: false }
|
|
88
|
+
* totalOf({ items: [a, b], cursor: "x" }) -> { value: 2, exact: false }
|
|
89
|
+
*/
|
|
90
|
+
export declare function totalOf(collection: EntityCollection<unknown>): Count;
|
|
91
|
+
/** How a relation ties two records, as a model's definition says it (`FieldMeta`). */
|
|
92
|
+
export type RelationKeys = {
|
|
93
|
+
identified_by?: string | null;
|
|
94
|
+
parent_field?: string | null;
|
|
95
|
+
related_field?: string | null;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* The question «the related records of this one», over the related entity — sincpro_framework's
|
|
99
|
+
* `References.criteria()`: what pages a relation past what its record brought, searches it and
|
|
100
|
+
* orders it, a list like any other. `criteria` narrows it; it never widens it.
|
|
101
|
+
*
|
|
102
|
+
* relatedCriteria(meta.fields.invoices, customer, "id")
|
|
103
|
+
* -> { selection: { field: "customer_id", operator: "=", value: customer.id } }
|
|
104
|
+
* relatedCriteria(field, customer, "id", { order: [{ field: "issued_on", descending: true }] })
|
|
105
|
+
*
|
|
106
|
+
* The two fields the definition names (`parent_field`, `related_field`) are read when it names
|
|
107
|
+
* them — a relation by a business key —; `identified_by` against the record's own key otherwise.
|
|
108
|
+
* A relation whose definition says neither cannot be asked, and says so.
|
|
109
|
+
*/
|
|
110
|
+
export declare function relatedCriteria<T extends AnyRecord>(keys: RelationKeys, record: AnyRecord, identity?: string, criteria?: Criteria<T>): Criteria<T>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A collection with every part said — what `collectionOf` answers —, edited the way a string or
|
|
3
|
+
* a date is: `append`, `extend`, `remove` and `replace` answer a new collection and never change
|
|
4
|
+
* this one, as sincpro_framework's `EntityCollection` does.
|
|
5
|
+
*
|
|
6
|
+
* const lines = collectionOf(invoice.lines);
|
|
7
|
+
* const next = lines.append(newLine).remove(oldLine.id); // lines is as it was
|
|
8
|
+
*
|
|
9
|
+
* An edit keeps what the collection says: a page stays a page (its cursor; a floor stays a
|
|
10
|
+
* floor), an exact count moves with what was added or removed. `remove` and `replace` of a record
|
|
11
|
+
* it does not hold throw, as Python's `list.remove` does. Written as JSON it is the wire's shape:
|
|
12
|
+
* `{ items, count, cursor }`.
|
|
13
|
+
*/
|
|
14
|
+
export class HeldCollection {
|
|
15
|
+
items;
|
|
16
|
+
count;
|
|
17
|
+
cursor;
|
|
18
|
+
/** The field each record's identity is in — what `remove` and `replace` match by. */
|
|
19
|
+
#identity;
|
|
20
|
+
constructor(items, count = null, cursor = null, identity = "id") {
|
|
21
|
+
this.items = Object.freeze([...items]);
|
|
22
|
+
this.count = count;
|
|
23
|
+
this.cursor = cursor;
|
|
24
|
+
this.#identity = identity;
|
|
25
|
+
Object.freeze(this);
|
|
26
|
+
}
|
|
27
|
+
/** How many records are held — this page, never the total (`count` is that). */
|
|
28
|
+
get length() {
|
|
29
|
+
return this.items.length;
|
|
30
|
+
}
|
|
31
|
+
/** Whether it holds less than there is (`isPartial`). */
|
|
32
|
+
get partial() {
|
|
33
|
+
return isPartial(this);
|
|
34
|
+
}
|
|
35
|
+
[Symbol.iterator]() {
|
|
36
|
+
return this.items[Symbol.iterator]();
|
|
37
|
+
}
|
|
38
|
+
/** A new collection with `records` at the end. */
|
|
39
|
+
append(...records) {
|
|
40
|
+
return this.#edited([...this.items, ...records], records.length);
|
|
41
|
+
}
|
|
42
|
+
/** A new collection with every one of `records` at the end, in order. */
|
|
43
|
+
extend(records) {
|
|
44
|
+
return this.append(...records);
|
|
45
|
+
}
|
|
46
|
+
/** A new collection without these records — each named by itself or by its identity. */
|
|
47
|
+
remove(...records) {
|
|
48
|
+
const unwanted = new Set(records.map((one) => this.#keyOf(one)));
|
|
49
|
+
const held = new Set(this.items.map((one) => this.#keyOf(one)));
|
|
50
|
+
const missing = [...unwanted].filter((key) => !held.has(key));
|
|
51
|
+
if (missing.length > 0)
|
|
52
|
+
throw new Error(`not in the collection: ${missing.join(", ")}`);
|
|
53
|
+
return this.#edited(this.items.filter((one) => !unwanted.has(this.#keyOf(one))), -unwanted.size);
|
|
54
|
+
}
|
|
55
|
+
/** A new collection where each of `records` takes the place of the held one with its identity. */
|
|
56
|
+
replace(...records) {
|
|
57
|
+
const byKey = new Map(records.map((one) => [this.#keyOf(one), one]));
|
|
58
|
+
const held = new Set(this.items.map((one) => this.#keyOf(one)));
|
|
59
|
+
const missing = [...byKey.keys()].filter((key) => !held.has(key));
|
|
60
|
+
if (missing.length > 0)
|
|
61
|
+
throw new Error(`not in the collection: ${missing.join(", ")} — append adds a record`);
|
|
62
|
+
return this.#edited(this.items.map((one) => byKey.get(this.#keyOf(one)) ?? one), 0);
|
|
63
|
+
}
|
|
64
|
+
toJSON() {
|
|
65
|
+
return { items: this.items, count: this.count, cursor: this.cursor };
|
|
66
|
+
}
|
|
67
|
+
#keyOf(record) {
|
|
68
|
+
if (typeof record !== "object" || record === null)
|
|
69
|
+
return record;
|
|
70
|
+
const key = record[this.#identity];
|
|
71
|
+
// A record not stored yet has no identity: it is itself, never every other new one too.
|
|
72
|
+
return key === undefined || key === null || key === "" ? record : key;
|
|
73
|
+
}
|
|
74
|
+
#edited(items, moved) {
|
|
75
|
+
const count = this.count !== null && this.count.exact
|
|
76
|
+
? { value: Math.max(this.count.value + moved, 0), exact: true }
|
|
77
|
+
: this.count;
|
|
78
|
+
return new HeldCollection(items, count, this.cursor, this.#identity);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Whether `value` is a collection as a reading answers one (`{items, …}`). */
|
|
82
|
+
export function isCollection(value) {
|
|
83
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
84
|
+
return false;
|
|
85
|
+
if (!("items" in value))
|
|
86
|
+
return false;
|
|
87
|
+
const items = value.items;
|
|
88
|
+
// A page whose records did not come (`items: null`) is still a page: its count says so.
|
|
89
|
+
return Array.isArray(items) || items === null || items === undefined;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Whatever a relation arrived as, as a collection with every part said.
|
|
93
|
+
*
|
|
94
|
+
* collectionOf({ items: [a, b], count: { value: 30, exact: true }, cursor: "x" })
|
|
95
|
+
* -> the same, `cursor` and `count` said
|
|
96
|
+
* collectionOf([a, b]) -> { items: [a, b], count: { value: 2, exact: true }, cursor: null }
|
|
97
|
+
* collectionOf(undefined) -> { items: [], count: { value: 0, exact: true }, cursor: null }
|
|
98
|
+
*
|
|
99
|
+
* An array — a domain written by hand, a record built in memory — holds all there is. `identity`
|
|
100
|
+
* is the field `remove` and `replace` match records by (the model's `identity`, `"id"`).
|
|
101
|
+
*/
|
|
102
|
+
export function collectionOf(value, identity = "id") {
|
|
103
|
+
if (value instanceof HeldCollection && identity === "id")
|
|
104
|
+
return value;
|
|
105
|
+
if (Array.isArray(value))
|
|
106
|
+
return new HeldCollection(value, { value: value.length, exact: true }, null, identity);
|
|
107
|
+
if (isCollection(value)) {
|
|
108
|
+
const page = value;
|
|
109
|
+
return new HeldCollection(page.items ?? [], page.count ?? null, page.cursor ?? null, identity);
|
|
110
|
+
}
|
|
111
|
+
return new HeldCollection([], { value: 0, exact: true }, null, identity);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Whether the collection holds less than there is — sincpro_framework's `is_partial`, the same
|
|
115
|
+
* rule: a cursor says there is more; no count claims nothing; a floor, or a count above what it
|
|
116
|
+
* holds, says there is more.
|
|
117
|
+
*
|
|
118
|
+
* isPartial({ items: [a], cursor: "x" }) -> true
|
|
119
|
+
* isPartial({ items: [a], count: { value: 1000, exact: false } }) -> true
|
|
120
|
+
* isPartial({ items: [a, b], count: { value: 2, exact: true } }) -> false
|
|
121
|
+
* isPartial({ items: [a] }) -> false
|
|
122
|
+
*/
|
|
123
|
+
export function isPartial(collection) {
|
|
124
|
+
if (collection.cursor != null)
|
|
125
|
+
return true;
|
|
126
|
+
const count = collection.count;
|
|
127
|
+
if (count == null)
|
|
128
|
+
return false;
|
|
129
|
+
return !count.exact || count.value > (collection.items ?? []).length;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* How many there are, as well as the collection knows: its count, or — when the read asked for
|
|
133
|
+
* none — what it holds, exact only when nothing says there is more.
|
|
134
|
+
*
|
|
135
|
+
* totalOf({ items: [a, b], count: { value: 1000, exact: false } }) -> { value: 1000, exact: false }
|
|
136
|
+
* totalOf({ items: [a, b], cursor: "x" }) -> { value: 2, exact: false }
|
|
137
|
+
*/
|
|
138
|
+
export function totalOf(collection) {
|
|
139
|
+
return (collection.count ?? {
|
|
140
|
+
value: (collection.items ?? []).length,
|
|
141
|
+
exact: collection.cursor == null,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The question «the related records of this one», over the related entity — sincpro_framework's
|
|
146
|
+
* `References.criteria()`: what pages a relation past what its record brought, searches it and
|
|
147
|
+
* orders it, a list like any other. `criteria` narrows it; it never widens it.
|
|
148
|
+
*
|
|
149
|
+
* relatedCriteria(meta.fields.invoices, customer, "id")
|
|
150
|
+
* -> { selection: { field: "customer_id", operator: "=", value: customer.id } }
|
|
151
|
+
* relatedCriteria(field, customer, "id", { order: [{ field: "issued_on", descending: true }] })
|
|
152
|
+
*
|
|
153
|
+
* The two fields the definition names (`parent_field`, `related_field`) are read when it names
|
|
154
|
+
* them — a relation by a business key —; `identified_by` against the record's own key otherwise.
|
|
155
|
+
* A relation whose definition says neither cannot be asked, and says so.
|
|
156
|
+
*/
|
|
157
|
+
export function relatedCriteria(keys, record, identity = "id", criteria = {}) {
|
|
158
|
+
const there = keys.related_field ?? keys.identified_by;
|
|
159
|
+
const here = keys.related_field != null ? (keys.parent_field ?? identity) : identity;
|
|
160
|
+
if (there == null || there === "")
|
|
161
|
+
throw new Error("this relation's definition names no key to ask it by (identified_by, related_field)");
|
|
162
|
+
const mine = { field: there, operator: "=", value: record[here] };
|
|
163
|
+
return {
|
|
164
|
+
...criteria,
|
|
165
|
+
selection: (criteria.selection === undefined
|
|
166
|
+
? mine
|
|
167
|
+
: { all: [criteria.selection, mine] }),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values of the wire, as the backend writes them: ids, a day and an instant, `Decimal`,
|
|
3
|
+
* `Money`, a to-many as a page of its records, and how any value is written and read.
|
|
4
|
+
*/
|
|
5
|
+
export { collectionOf, type EntityCollection, type HeldCollection, isCollection, isPartial, relatedCriteria, type RelationKeys, totalOf, } from "./collection.js";
|
|
6
|
+
export { Decimal, type DecimalLike } from "./decimal.js";
|
|
7
|
+
export { isEntityId, momentOf, newEntityId } from "./ids.js";
|
|
8
|
+
export { Money } from "./money.js";
|
|
9
|
+
export { BYTES, type JsonValue, pack, readValues, type Rebuild, rebuilt, unpack, valuesOf, } from "./serialization.js";
|
|
10
|
+
export { dayOf, installTimeZone, instantOf, isDay, timeZoneNow, wallTimeOf, } from "./time.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values of the wire, as the backend writes them: ids, a day and an instant, `Decimal`,
|
|
3
|
+
* `Money`, a to-many as a page of its records, and how any value is written and read.
|
|
4
|
+
*/
|
|
5
|
+
export { collectionOf, isCollection, isPartial, relatedCriteria, totalOf, } from "./collection.js";
|
|
6
|
+
export { Decimal } from "./decimal.js";
|
|
7
|
+
export { isEntityId, momentOf, newEntityId } from "./ids.js";
|
|
8
|
+
export { Money } from "./money.js";
|
|
9
|
+
export { BYTES, pack, readValues, rebuilt, unpack, valuesOf, } from "./serialization.js";
|
|
10
|
+
export { dayOf, installTimeZone, instantOf, isDay, timeZoneNow, wallTimeOf, } from "./time.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context in this process's memory: the default, on every platform. A test gets a fresh one
|
|
3
|
+
* from `configureProcess()`.
|
|
4
|
+
*/
|
|
5
|
+
export function memoryContexts() {
|
|
6
|
+
const held = new Map();
|
|
7
|
+
const versions = new Map();
|
|
8
|
+
const listeners = new Map();
|
|
9
|
+
const changed = (key) => {
|
|
10
|
+
versions.set(key, (versions.get(key) ?? 0) + 1);
|
|
11
|
+
for (const listener of [...(listeners.get(key) ?? [])])
|
|
12
|
+
listener();
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
load: (key) => held.get(key),
|
|
16
|
+
save(key, values) {
|
|
17
|
+
// A copy: whoever handed the values may keep changing their object.
|
|
18
|
+
held.set(key, { ...values });
|
|
19
|
+
changed(key);
|
|
20
|
+
},
|
|
21
|
+
delete(key) {
|
|
22
|
+
if (!held.delete(key))
|
|
23
|
+
return;
|
|
24
|
+
changed(key);
|
|
25
|
+
},
|
|
26
|
+
version: (key) => versions.get(key) ?? 0,
|
|
27
|
+
watch(key, listener) {
|
|
28
|
+
const own = listeners.get(key) ?? new Set();
|
|
29
|
+
listeners.set(key, own);
|
|
30
|
+
own.add(listener);
|
|
31
|
+
return () => void own.delete(listener);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { travelling, } from "../domain/codec.js";
|
|
2
|
+
/** The codec for anything outside the codebase: only the values that travel, as JSON. */
|
|
3
|
+
export const plainCodec = {
|
|
4
|
+
dumps: (values) => JSON.stringify(travelling(values)),
|
|
5
|
+
loads(text) {
|
|
6
|
+
try {
|
|
7
|
+
const found = JSON.parse(text);
|
|
8
|
+
return found !== null && typeof found === "object" && !Array.isArray(found)
|
|
9
|
+
? found
|
|
10
|
+
: {};
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -23,4 +23,3 @@ export interface ContextCodec {
|
|
|
23
23
|
export declare function travels(value: unknown): boolean;
|
|
24
24
|
/** The values of `context` that travel; the rest stays in this process. */
|
|
25
25
|
export declare function travelling(context: ContextValues): ContextValues;
|
|
26
|
-
export declare const plainCodec: ContextCodec;
|
|
@@ -28,17 +28,3 @@ export function travels(value) {
|
|
|
28
28
|
export function travelling(context) {
|
|
29
29
|
return Object.fromEntries(Object.entries(context).filter(([, value]) => travels(value)));
|
|
30
30
|
}
|
|
31
|
-
export const plainCodec = {
|
|
32
|
-
dumps: (values) => JSON.stringify(travelling(values)),
|
|
33
|
-
loads(text) {
|
|
34
|
-
try {
|
|
35
|
-
const found = JSON.parse(text);
|
|
36
|
-
return found !== null && typeof found === "object" && !Array.isArray(found)
|
|
37
|
-
? found
|
|
38
|
-
: {};
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return {};
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Span } from "../../observability/domain/tracing.js";
|
|
2
|
+
/**
|
|
3
|
+
* One run of a use case: its span, what it runs with, and who ran it.
|
|
4
|
+
*
|
|
5
|
+
* Context: sincpro_framework keeps the running use case in a context variable; a browser has
|
|
6
|
+
* nothing like it that survives an `await` (AsyncContext is a proposal, Zone.js patches the
|
|
7
|
+
* world), and a global stack mixes the parents of two use cases running at once — which a
|
|
8
|
+
* screen with several windows does all the time. So an execution is a value, handed to what
|
|
9
|
+
* the use case calls: the Features its `featureBus` runs, and its dependencies (`BIND`).
|
|
10
|
+
*/
|
|
11
|
+
export type Execution = {
|
|
12
|
+
/** UUID v7, as 32 hex: this run. */
|
|
13
|
+
readonly id: string;
|
|
14
|
+
/** `<bounded context>.<Service>` — the use case's identity, as the backend names its own. */
|
|
15
|
+
readonly useCase: string;
|
|
16
|
+
/** The bounded context the use case belongs to. */
|
|
17
|
+
readonly boundedContext: string;
|
|
18
|
+
/** The run that executed this one: an ApplicationService, for its Features. */
|
|
19
|
+
readonly parent: Execution | undefined;
|
|
20
|
+
readonly span: Span;
|
|
21
|
+
/** What it runs with, read once where the gesture started and inherited by every child. */
|
|
22
|
+
readonly context: Readonly<Record<string, unknown>>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A dependency that has something to say about the execution it is used in: given that
|
|
26
|
+
* execution, it answers itself bound to it. The transport, every generated client and the
|
|
27
|
+
* predefined loggers are; any other dependency of a bounded context is handed as it is.
|
|
28
|
+
*/
|
|
29
|
+
export declare const BIND: unique symbol;
|
|
30
|
+
export type Bindable<T> = {
|
|
31
|
+
[BIND]: (execution: Execution) => T;
|
|
32
|
+
};
|
|
33
|
+
export declare function isBindable<T>(value: T): value is T & Bindable<T>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dependency that has something to say about the execution it is used in: given that
|
|
3
|
+
* execution, it answers itself bound to it. The transport, every generated client and the
|
|
4
|
+
* predefined loggers are; any other dependency of a bounded context is handed as it is.
|
|
5
|
+
*/
|
|
6
|
+
export const BIND = Symbol.for("sincpro.client-framework.bind");
|
|
7
|
+
export function isBindable(value) {
|
|
8
|
+
return (value !== null &&
|
|
9
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
10
|
+
typeof value[BIND] === "function");
|
|
11
|
+
}
|
|
@@ -44,8 +44,3 @@ export interface ContextStore {
|
|
|
44
44
|
*/
|
|
45
45
|
watch?(key: string, listener: () => void): () => void;
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
* The context in this process's memory: the default, on every platform. A test gets a fresh one
|
|
49
|
-
* from `configureProcess()`.
|
|
50
|
-
*/
|
|
51
|
-
export declare function memoryContexts(): ContextStore;
|
|
@@ -23,37 +23,4 @@
|
|
|
23
23
|
* Reads are synchronous on purpose: a use case reads its context where a gesture starts, and an
|
|
24
24
|
* asynchronous store would make every use case wait for it.
|
|
25
25
|
*/
|
|
26
|
-
|
|
27
|
-
* The context in this process's memory: the default, on every platform. A test gets a fresh one
|
|
28
|
-
* from `configureProcess()`.
|
|
29
|
-
*/
|
|
30
|
-
export function memoryContexts() {
|
|
31
|
-
const held = new Map();
|
|
32
|
-
const versions = new Map();
|
|
33
|
-
const listeners = new Map();
|
|
34
|
-
const changed = (key) => {
|
|
35
|
-
versions.set(key, (versions.get(key) ?? 0) + 1);
|
|
36
|
-
for (const listener of [...(listeners.get(key) ?? [])])
|
|
37
|
-
listener();
|
|
38
|
-
};
|
|
39
|
-
return {
|
|
40
|
-
load: (key) => held.get(key),
|
|
41
|
-
save(key, values) {
|
|
42
|
-
// A copy: whoever handed the values may keep changing their object.
|
|
43
|
-
held.set(key, { ...values });
|
|
44
|
-
changed(key);
|
|
45
|
-
},
|
|
46
|
-
delete(key) {
|
|
47
|
-
if (!held.delete(key))
|
|
48
|
-
return;
|
|
49
|
-
changed(key);
|
|
50
|
-
},
|
|
51
|
-
version: (key) => versions.get(key) ?? 0,
|
|
52
|
-
watch(key, listener) {
|
|
53
|
-
const own = listeners.get(key) ?? new Set();
|
|
54
|
-
listeners.set(key, own);
|
|
55
|
-
own.add(listener);
|
|
56
|
-
return () => void own.delete(listener);
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
26
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContextStore, ContextValues } from "
|
|
1
|
+
import type { ContextStore, ContextValues } from "../domain/store.js";
|
|
2
2
|
/**
|
|
3
3
|
* One level of the context: the process's (`processContext`) or a bounded context's
|
|
4
4
|
* (`project.context`), read and written the same way on every platform.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context: the levels a call sees (the process, its bounded context, a scope), where they
|
|
3
|
+
* live (`ContextStore`, a port each platform implements), what travels with a request, and the
|
|
4
|
+
* execution a run carries.
|
|
5
|
+
*
|
|
6
|
+
* domain/ the ports and rules: `ContextStore`, `ContextCodec`, what travels, `Execution`
|
|
7
|
+
* adapters/ the implementations this package gives: memory, the plain codec
|
|
8
|
+
* entrypoint/ `Context`: one door over a level
|
|
9
|
+
* infrastructure/ the process's store and its level (`processContext`)
|
|
10
|
+
*/
|
|
11
|
+
export { memoryContexts } from "./adapters/memory.js";
|
|
12
|
+
export { plainCodec } from "./adapters/plain-codec.js";
|
|
13
|
+
export { CONTEXT_HEADER, type ContextCodec, travelling, travels, } from "./domain/codec.js";
|
|
14
|
+
export { BIND, type Bindable, type Execution, } from "./domain/execution.js";
|
|
15
|
+
export type { ContextStore, ContextValues, } from "./domain/store.js";
|
|
16
|
+
export type { Context } from "./entrypoint/context.js";
|
|
17
|
+
export { processContext } from "./infrastructure/process.js";
|