graphddb 0.9.1 → 0.9.3
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/README.md +89 -40
- package/dist/cdc/index.d.ts +4 -4
- package/dist/{chunk-MBJ4JVRM.js → chunk-U5WNYFHC.js} +216 -1
- package/dist/cli.js +23 -5
- package/dist/index.d.ts +4 -4
- package/dist/internal/index.d.ts +5 -5
- package/dist/{key-CWytoEaE.d.ts → key-C_6ScBbu.d.ts} +1 -1
- package/dist/linter/index.d.ts +5 -5
- package/dist/{linter-jEwmZotm.d.ts → linter-DeUvifz5.d.ts} +1 -1
- package/dist/{prepared-artifact-CwH5ezFq.d.ts → prepared-artifact-BzDhojXU.d.ts} +3 -3
- package/dist/spec/index.d.ts +8 -8
- package/dist/spec/index.js +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/transform/index.d.ts +1 -1
- package/dist/{types-CgXS-4Ox.d.ts → types-DGwo00JB.d.ts} +12 -0
- package/dist/{types-nk5okD7d.d.ts → types-DLpvjjV_.d.ts} +1 -1
- package/docs/diagrams/execution-paths.en.svg +14 -0
- package/docs/diagrams/execution-paths.ja.svg +14 -0
- package/docs/diagrams/query-pipeline.en.svg +10 -0
- package/docs/diagrams/query-pipeline.ja.svg +10 -0
- package/docs/diagrams/ssot-derivation.en.svg +13 -0
- package/docs/diagrams/ssot-derivation.ja.svg +13 -0
- package/docs/python-bridge.md +66 -28
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,41 +1,63 @@
|
|
|
1
1
|
# GraphDDB
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Portable, contract-first DynamoDB access patterns.**
|
|
4
4
|
|
|
5
|
-
GraphDDB
|
|
6
|
-
|
|
7
|
-
It is not an ORM
|
|
8
|
-
|
|
5
|
+
GraphDDB lets you define your DynamoDB access patterns once in TypeScript, validate them at build time,
|
|
6
|
+
inspect the exact execution plan before anything runs, and execute the *same* contract from five
|
|
7
|
+
languages — TypeScript, Python, Rust, Go, and PHP. It is **not an ORM**: it is a contract compiler +
|
|
8
|
+
runtime for explicit DynamoDB access patterns.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
The access patterns become a portable, executable artifact. TypeScript is the single source of truth;
|
|
11
|
+
a static planner lowers each contract to a language-neutral IR (`manifest.json` + `operations.json`),
|
|
12
|
+
and every language runtime executes that IR with results pinned **byte-identical** by a shared
|
|
13
|
+
conformance suite. GraphQL-like traversal is one of the authoring surfaces — ergonomic, but not the
|
|
14
|
+
point. The point is that PK / GSI / projection / relation / cursor / operation limits stay explicit,
|
|
15
|
+
verifiable, and portable rather than hidden behind an abstraction.
|
|
16
|
+
|
|
17
|
+
## ✨ What GraphDDB does — in three layers
|
|
18
|
+
|
|
19
|
+
**1. Define** explicit DynamoDB access patterns
|
|
11
20
|
|
|
12
21
|
- ✅ **Type-safe models** —— the TS class is the single source of truth. Keys / GSIs / field types are checked at compile time.
|
|
13
|
-
- ✅ **Graph-style traversal** —— nest `@hasMany` / `@belongsTo` / `@hasOne` inside `select
|
|
14
|
-
- ✅ **
|
|
15
|
-
- ✅ **
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- ✅ **
|
|
20
|
-
- ✅ **
|
|
21
|
-
- ✅ **
|
|
22
|
-
- ✅ **
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
- ✅ **Graph-style traversal** —— nest `@hasMany` / `@belongsTo` / `@hasOne` inside `select` (an ergonomic authoring surface — arguments → key lookup, selection → projection).
|
|
23
|
+
- ✅ **CQRS contracts** —— public Query/Command contracts as the read/write authoring surface (N+1-safe, context boundaries, composition across contracts).
|
|
24
|
+
- ✅ **Maintained access paths** —— declare embedded snapshots / aggregate counters kept synchronized atomically with the source write.
|
|
25
|
+
|
|
26
|
+
**2. Validate & explain** before anything touches DynamoDB
|
|
27
|
+
|
|
28
|
+
- ✅ **Compile-time safety** —— undefined relations, missing GSIs, limit-less lists, and over-depth traversals are rejected at compile time / by the linter — equally for human- and AI-written code.
|
|
29
|
+
- ✅ **Execution plan (Explain)** —— `explain()` returns the exact DynamoDB operations, so RCU shape is inspectable up front.
|
|
30
|
+
- ✅ **Automatic access-pattern resolution** —— the runtime picks the PK / GSI from the fields you give; `select`ed fields are the only ones read and the only ones in the result type.
|
|
31
|
+
- ✅ **In-memory testing** —— unit-test planner / traversal / transaction / CDC with no Docker, no DynamoDB Local.
|
|
32
|
+
|
|
33
|
+
**3. Execute the same contract, portably, from five languages**
|
|
34
|
+
|
|
35
|
+
- ✅ **Portable executable IR** —— the static planner lowers each contract to a language-neutral bridge bundle (`manifest.json` + `operations.json`, a `components[]` graph on the shared behavior-contracts vocabulary).
|
|
36
|
+
- ✅ **5 language runtimes** —— generate clients + runtimes for **Python / Rust / Go / PHP** (TypeScript runs the IR natively), all executing the same access patterns.
|
|
37
|
+
- ✅ **5-language conformance** —— TS / Python / Rust / Go / PHP run the same cases against DynamoDB Local; CI pins the results **byte-identical**.
|
|
38
|
+
|
|
39
|
+
**Operational surface** (all built on the layers above): **Middleware** (host-side read/write hooks) ·
|
|
40
|
+
**CDC emulator** (local DynamoDB-Streams-equivalent change events) · **CDC projection**
|
|
41
|
+
(`@cdcProjected()` + `fromChange` / `subscribe` into typed records) · **Prepared statements**
|
|
42
|
+
(compile-once / execute-many, AOT static plans) · **CloudFormation generation** ·
|
|
43
|
+
**Docs generation** (Markdown + Mermaid, Handlebars-overridable).
|
|
26
44
|
|
|
27
45
|
## 🤔 Philosophy
|
|
28
46
|
|
|
47
|
+
- **Access patterns as portable contracts** —— an access pattern is defined once and becomes a
|
|
48
|
+
language-neutral, executable artifact. The same contract is validated, explained, and then executed
|
|
49
|
+
identically across languages, instead of being re-implemented per stack.
|
|
29
50
|
- **Native DynamoDB access patterns** —— express access patterns directly in code instead of hiding them
|
|
30
51
|
behind an ORM abstraction. Because PK / GSI, projection, and limits stay visible, the correct design
|
|
31
52
|
becomes the easiest path.
|
|
32
|
-
- **Graph-style traversal** —— GraphQL's query model (arguments → key lookup, selection → projection,
|
|
33
|
-
relation → Query/Get/BatchGet) corresponds naturally to DynamoDB's access patterns.
|
|
34
53
|
- **Compile-time safety** —— undefined relations, missing GSIs, limit-less lists, and traversals exceeding
|
|
35
54
|
the allowed depth are rejected before execution at compile time / by the linter. The same constraints
|
|
36
55
|
apply equally to human-written and AI-generated code.
|
|
37
56
|
- **Runtime transparency** —— the runtime is not a black box. `explain()` shows the execution plan, and the
|
|
38
57
|
plan reveals the shape of RCU usage.
|
|
58
|
+
- **Graph-style traversal is a surface, not the essence** —— GraphQL's query model (arguments → key
|
|
59
|
+
lookup, selection → projection, relation → Query/Get/BatchGet) is a convenient way to *author* access
|
|
60
|
+
patterns; the essence is the explicit, portable contract underneath.
|
|
39
61
|
|
|
40
62
|
## 📦 Install
|
|
41
63
|
|
|
@@ -57,7 +79,7 @@ Node.js ≥ 22 is required.
|
|
|
57
79
|
## 🚀 Quick Start
|
|
58
80
|
|
|
59
81
|
```ts
|
|
60
|
-
import { DDBModel, model, string, key, k } from 'graphddb';
|
|
82
|
+
import { DDBModel, graphddb, model, string, key, k } from 'graphddb';
|
|
61
83
|
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
62
84
|
|
|
63
85
|
@model({ table: 'AppTable', prefix: 'USER' })
|
|
@@ -200,8 +222,16 @@ Each capability is summarized here only. For details, operator lists, and proced
|
|
|
200
222
|
authorization scoping). Host-only and non-serialized. For hook points, see [`docs/middleware.md`](./docs/middleware.md).
|
|
201
223
|
- **Class hydration** —— `options.hydrate` loads read results into host-language domain objects (opt-in,
|
|
202
224
|
Phase 1: `query` top level). [`docs/class-hydration.md`](./docs/class-hydration.md).
|
|
203
|
-
- **
|
|
204
|
-
conformance suite. [`docs/python-bridge.md`](./docs/python-bridge.md).
|
|
225
|
+
- **Multi-language bridge** —— generate Python / Rust / Go / PHP clients + runtimes with TS as the SSoT,
|
|
226
|
+
kept in step by a 5-language conformance suite. [`docs/python-bridge.md`](./docs/python-bridge.md).
|
|
227
|
+
- **Prepared statements** —— `graphddb.prepare($ => ({...}))` → `.execute(params)` (unified read/write).
|
|
228
|
+
"Contract-free precompilation": compile a declarative route once and execute it repeatedly with
|
|
229
|
+
different params. With the **true build-time compilation (AOT)** of `graphddb transform prepared
|
|
230
|
+
--aot`, plans are emitted as static artifacts and zero runtime compilation happens, including the
|
|
231
|
+
first call (a stale plan is loud-rejected at load; drift is caught in CI). Without the transform,
|
|
232
|
+
behavior stays identical through the lazy-slot + structural-memoization fallback. no-runtime-capture
|
|
233
|
+
is loudly enforced by a build-time lint + a runtime guard.
|
|
234
|
+
[`docs/prepared-statements.md`](./docs/prepared-statements.md).
|
|
205
235
|
- **CQRS contracts** —— public Query/Command contracts (cardinality matrix, N+1 safety, context boundaries,
|
|
206
236
|
composition across contracts). [`docs/cqrs-contract.md`](./docs/cqrs-contract.md) /
|
|
207
237
|
[`docs/mutation-command-derivation.md`](./docs/mutation-command-derivation.md).
|
|
@@ -212,6 +242,17 @@ Each capability is summarized here only. For details, operator lists, and proced
|
|
|
212
242
|
`graphddb.subscribe(handlers)` (the sibling of `query` / `mutate`) produces a `ChangeHandler` the
|
|
213
243
|
consumer mounts on its own stream. graphddb owns parse→typed record only; subscription, sink
|
|
214
244
|
delivery, and idempotency are the consumer's. [`docs/cdc-projection.md`](./docs/cdc-projection.md).
|
|
245
|
+
- **Multi-language codegen (the 3 paths, #257)** —— execution keeps 2 paths + 1 consumer surface,
|
|
246
|
+
coexisting permanently: (1) **dynamic JSON path (default)** — the `components[]` of
|
|
247
|
+
`operations.json` are loaded at runtime and interpreted by the shared `run_behavior` (for swapping
|
|
248
|
+
behavior without a deploy). (2) **codegen-static path (opt-in)** — `graphddb generate behaviors
|
|
249
|
+
--lang <ts|python|go|rust|php>` hands the same IR to the **shared upstream behavior-contracts
|
|
250
|
+
generator** (bc#13), emitting modules with the IR baked in as native literals (graphddb owns only
|
|
251
|
+
envelope + invoke — no generation logic). A generated module checks its fingerprint against the
|
|
252
|
+
live registry and loud-rejects when stale (#208 discipline). Rust generated modules use
|
|
253
|
+
`std::sync::LazyLock`, so **Rust >= 1.80** is required. (3) **typed bindings
|
|
254
|
+
(`generate python/php/rust/go`)** — the consumer-UX surface (typed repositories / DTOs), still
|
|
255
|
+
graphddb-owned; not an execution engine — it delegates to the paths above.
|
|
215
256
|
- **CloudFormation generation** —— `graphddb generate cloudformation` emits a CloudFormation template for
|
|
216
257
|
the DynamoDB table(s) a model set maps onto (GSI union / Streams / TTL / PITR / PROVISIONED / Auto
|
|
217
258
|
Scaling). [`docs/cloudformation.md`](./docs/cloudformation.md).
|
|
@@ -232,10 +273,11 @@ documentation available offline at hand. On GitHub, follow the links in the tabl
|
|
|
232
273
|
| [Specification](./docs/spec.md) | Core API: entities, structured keys/GSIs, query/filter, relations, batch/transaction, design rules, runtime behavior. |
|
|
233
274
|
| [Design patterns](./docs/design-patterns.md) | Mapping representative DynamoDB design patterns onto graphddb features (how to declare `pattern` / `@model({ kind })` + `@maintainedFrom` / `@aggregate`, read & write maintenance, per-phase coverage). |
|
|
234
275
|
| [Middleware / Hooks](./docs/middleware.md) | The `graphddb.config.use` host-side middleware: hook points read R1–R5 / write W1–W5, `{ context }`, ordering. |
|
|
276
|
+
| [Prepared statements](./docs/prepared-statements.md) | `graphddb.prepare` / `.execute` (unified read/write prepared statements), the no-runtime-capture constraint, `graphddb transform prepared` (AOT static plans / compile-time hoisting + build-time lint + drift detection): choosing between the 3 tiers AOT > lazy slot + structural memoization > ad-hoc. |
|
|
235
277
|
| [CQRS contract layer](./docs/cqrs-contract.md) | Public Query/Command contracts, cardinality matrix, N+1 safety, composition across contracts, context boundaries. |
|
|
236
278
|
| [Mutation → command derivation](./docs/mutation-command-derivation.md) | The internal write-plan composition DSL behind the Command IF (`entityWrites`, fragment composition, derivation of atomic `TransactWriteItems`). |
|
|
237
279
|
| [Class hydration](./docs/class-hydration.md) | Opt-in `options.hydrate` that loads read results into host objects (Phase 1: `query` top level). |
|
|
238
|
-
| [Multi-language
|
|
280
|
+
| [Multi-language bridge](./docs/python-bridge.md) | Code generation with TS as the SSoT and Python / Rust / Go / PHP runtimes; 5-language conformance. |
|
|
239
281
|
| [In-memory test adapter](./docs/testing.md) | Docker-free in-process testing with `graphddb/testing` and `MemoryInspector`. |
|
|
240
282
|
| [CDC emulator](./docs/cdc-emulator.md) | A change-event emulator for differential aggregation patterns (dev/test). |
|
|
241
283
|
| [CDC projection](./docs/cdc-projection.md) | The `@cdcProjected()` + `fromChange` / `subscribe` typed-consumer-IF: a contract that parses CDC change events into typed records. Boundary (parse→typed is graphddb; subscription, sink delivery, idempotency are the consumer's). |
|
|
@@ -256,23 +298,30 @@ documentation available offline at hand. On GitHub, follow the links in the tabl
|
|
|
256
298
|
|
|
257
299
|
A query travels from the TS Model through the Planner / Runtime to DynamoDB:
|
|
258
300
|
|
|
259
|
-
|
|
260
|
-
flowchart LR
|
|
261
|
-
M["TS Model<br/>Entity / Key / GSI / Relation"] --> P["Planner<br/>access pattern resolution / projection"]
|
|
262
|
-
P --> R["Runtime<br/>execute / retry / hydrate"]
|
|
263
|
-
R --> D[("DynamoDB")]
|
|
264
|
-
```
|
|
301
|
+

|
|
265
302
|
|
|
266
|
-
From the same TS Model (the SSoT), multiple targets are derived
|
|
303
|
+
From the same TS Model (the SSoT), multiple targets are derived. The language runtimes are fanned out to
|
|
304
|
+
4 languages through the portable IR (the `components[]` graph in `manifest.json` + `operations.json`),
|
|
305
|
+
and the execution results of all 5 languages — TS included — are pinned byte-identical by the
|
|
306
|
+
conformance suite:
|
|
267
307
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
308
|
+

|
|
309
|
+
|
|
310
|
+
| Runtime | Package | Distribution |
|
|
311
|
+
|---------|---------|--------------|
|
|
312
|
+
| TypeScript | `graphddb` | npm |
|
|
313
|
+
| Python | `graphddb-runtime` | PyPI |
|
|
314
|
+
| Rust | `graphddb_runtime` | crates.io |
|
|
315
|
+
| Go | `github.com/foo-log-inc/graphddb/go` | go module (`go/vX.Y.Z` tags) |
|
|
316
|
+
| PHP | `graphddb/runtime` | bundled in-repo (not on composer) |
|
|
317
|
+
|
|
318
|
+
Multi-language execution keeps 3 permanent paths — 2 execution paths + 1 consumer surface (see
|
|
319
|
+
"Multi-language codegen" under [More Capabilities](#-more-capabilities)):
|
|
320
|
+
|
|
321
|
+

|
|
322
|
+
|
|
323
|
+
> The figures are self-contained SVGs drawn with diagram-contracts (Draw TSX).
|
|
324
|
+
> Sources live in [`diagrams/`](./diagrams/); regenerate with `diagrams/build.sh`.
|
|
276
325
|
|
|
277
326
|
## 📄 License
|
|
278
327
|
|
package/dist/cdc/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { w as CdcEmulatorOptions, x as ChangeHandler, y as Unsubscribe, F as FaultSpec, z as ConcurrentRecomputeRef, v as ChangeEvent, A as EventLog, G as ReplayOptions, H as ShardId, I as MaintainTrigger, E as EffectPath, g as ProjectionMap, j as MembershipPredicate } from '../key-
|
|
2
|
-
export { J as BatchResult, K as CdcMode, L as ChangeBatch, N as ChangeEventName, O as ClockMode, V as StartingPosition, X as StreamViewType, Y as SubscribeHandler, Z as SubscribeHandlers, _ as buildSubscribeHandler } from '../key-
|
|
3
|
-
import { E as EntityMetadata } from '../types-
|
|
4
|
-
import '../types-
|
|
1
|
+
import { w as CdcEmulatorOptions, x as ChangeHandler, y as Unsubscribe, F as FaultSpec, z as ConcurrentRecomputeRef, v as ChangeEvent, A as EventLog, G as ReplayOptions, H as ShardId, I as MaintainTrigger, E as EffectPath, g as ProjectionMap, j as MembershipPredicate } from '../key-C_6ScBbu.js';
|
|
2
|
+
export { J as BatchResult, K as CdcMode, L as ChangeBatch, N as ChangeEventName, O as ClockMode, V as StartingPosition, X as StreamViewType, Y as SubscribeHandler, Z as SubscribeHandlers, _ as buildSubscribeHandler } from '../key-C_6ScBbu.js';
|
|
3
|
+
import { E as EntityMetadata } from '../types-DLpvjjV_.js';
|
|
4
|
+
import '../types-DGwo00JB.js';
|
|
5
5
|
import 'behavior-contracts';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -1876,6 +1876,209 @@ function portableIrDocument(components) {
|
|
|
1876
1876
|
};
|
|
1877
1877
|
}
|
|
1878
1878
|
|
|
1879
|
+
// src/spec/type-notation.ts
|
|
1880
|
+
import {
|
|
1881
|
+
assertPortableComponentGraph
|
|
1882
|
+
} from "behavior-contracts";
|
|
1883
|
+
var UndeterminablePortableType = class extends Error {
|
|
1884
|
+
constructor(message) {
|
|
1885
|
+
super(message);
|
|
1886
|
+
this.name = "UndeterminablePortableType";
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
function fieldTypeToPortable(fieldType, where) {
|
|
1890
|
+
switch (fieldType) {
|
|
1891
|
+
case "string":
|
|
1892
|
+
case "binary":
|
|
1893
|
+
return "string";
|
|
1894
|
+
case "number":
|
|
1895
|
+
return "float";
|
|
1896
|
+
case "boolean":
|
|
1897
|
+
return "bool";
|
|
1898
|
+
case "stringSet":
|
|
1899
|
+
return { arr: "string" };
|
|
1900
|
+
case "numberSet":
|
|
1901
|
+
return { arr: "float" };
|
|
1902
|
+
case "list":
|
|
1903
|
+
case "map":
|
|
1904
|
+
throw new UndeterminablePortableType(
|
|
1905
|
+
`type-notation: the '${fieldType}' field at ${where} has no portable type (a DynamoDB ${fieldType} attribute has no modeled element/record shape, and the portable type notation has no free-form/any type). The behavior is left untyped (byte-identical).`
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
var scalarType = fieldTypeToPortable;
|
|
1910
|
+
function relationSegments(nodeId) {
|
|
1911
|
+
return nodeId.split(".").filter((p) => p !== "items");
|
|
1912
|
+
}
|
|
1913
|
+
function nodePorts(node) {
|
|
1914
|
+
if ("map" in node) return node.map.ports;
|
|
1915
|
+
if ("cond" in node) return void 0;
|
|
1916
|
+
return node.ports;
|
|
1917
|
+
}
|
|
1918
|
+
function nodeComponent(node) {
|
|
1919
|
+
if ("map" in node) return node.map.component;
|
|
1920
|
+
if ("cond" in node) return void 0;
|
|
1921
|
+
return node.component;
|
|
1922
|
+
}
|
|
1923
|
+
function isConnectionRootOp(rootComponent, cardinalityOne) {
|
|
1924
|
+
return (rootComponent === "Query" || rootComponent === "BatchGetItem") && !cardinalityOne;
|
|
1925
|
+
}
|
|
1926
|
+
function nodeProjection(ports) {
|
|
1927
|
+
const port = ports.projection;
|
|
1928
|
+
return (port?.arr ?? []).filter((f) => typeof f === "string");
|
|
1929
|
+
}
|
|
1930
|
+
function insertNode(root, node, manifest, nodeById) {
|
|
1931
|
+
const ports = nodePorts(node);
|
|
1932
|
+
if (ports === void 0) return;
|
|
1933
|
+
const segments = relationSegments(node.id);
|
|
1934
|
+
if (segments.length === 0) return;
|
|
1935
|
+
let tree = root;
|
|
1936
|
+
let parent = root;
|
|
1937
|
+
let entity = root.entity;
|
|
1938
|
+
let kind = "single";
|
|
1939
|
+
for (const prop of segments) {
|
|
1940
|
+
parent = tree;
|
|
1941
|
+
const relation = manifest.entities[entity]?.relations[prop];
|
|
1942
|
+
if (relation === void 0) {
|
|
1943
|
+
throw new Error(
|
|
1944
|
+
`type-notation: relation segment '${prop}' of node has no relation on entity '${entity}'; cannot derive its result type. The components graph and the manifest have diverged.`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
kind = relation.type === "hasMany" || relation.type === "refs" ? "connection" : "single";
|
|
1948
|
+
const target = relation.target;
|
|
1949
|
+
let child = tree.children.get(prop);
|
|
1950
|
+
if (!child) {
|
|
1951
|
+
child = {
|
|
1952
|
+
entity: target,
|
|
1953
|
+
projection: [],
|
|
1954
|
+
children: /* @__PURE__ */ new Map(),
|
|
1955
|
+
implicitFields: /* @__PURE__ */ new Set(),
|
|
1956
|
+
relationKind: kind
|
|
1957
|
+
};
|
|
1958
|
+
tree.children.set(prop, child);
|
|
1959
|
+
}
|
|
1960
|
+
tree = child;
|
|
1961
|
+
entity = target;
|
|
1962
|
+
}
|
|
1963
|
+
tree.projection = nodeProjection(ports);
|
|
1964
|
+
tree.relationKind = kind;
|
|
1965
|
+
nodeById.set(node.id, tree);
|
|
1966
|
+
const implicit = ports.implicitSourceField;
|
|
1967
|
+
if (typeof implicit === "string") parent.implicitFields.add(implicit);
|
|
1968
|
+
}
|
|
1969
|
+
function wrapRelation(childObj, kind) {
|
|
1970
|
+
return kind === "connection" ? { obj: { cursor: { opt: "string" }, items: { arr: childObj } } } : { opt: childObj };
|
|
1971
|
+
}
|
|
1972
|
+
function nodeObjType(node, manifest, where, includeRelations) {
|
|
1973
|
+
const meta = manifest.entities[node.entity];
|
|
1974
|
+
const relationProps = new Set(node.children.keys());
|
|
1975
|
+
const scalarFields = node.projection.filter(
|
|
1976
|
+
(f) => !relationProps.has(f) && !node.implicitFields.has(f)
|
|
1977
|
+
);
|
|
1978
|
+
const fields = {};
|
|
1979
|
+
for (const f of scalarFields) {
|
|
1980
|
+
const ft = meta?.fields[f]?.type ?? "string";
|
|
1981
|
+
fields[f] = scalarType(ft, `${where}.${f}`);
|
|
1982
|
+
}
|
|
1983
|
+
if (includeRelations) {
|
|
1984
|
+
for (const [prop, child] of node.children) {
|
|
1985
|
+
const childObj = nodeObjType(child, manifest, `${where}.${prop}`, true);
|
|
1986
|
+
fields[prop] = wrapRelation(childObj, child.relationKind);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (Object.keys(fields).length === 0) {
|
|
1990
|
+
throw new UndeterminablePortableType(
|
|
1991
|
+
`type-notation: result node at ${where} (entity '${node.entity}') projects no fields and has no relations \u2014 an empty record has no useful portable type; the behavior is left untyped.`
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
return { obj: fields };
|
|
1995
|
+
}
|
|
1996
|
+
function canonicalizeDeep(value) {
|
|
1997
|
+
if (Array.isArray(value)) return value.map(canonicalizeDeep);
|
|
1998
|
+
if (value !== null && typeof value === "object") {
|
|
1999
|
+
const src = value;
|
|
2000
|
+
const out = {};
|
|
2001
|
+
for (const k of Object.keys(src).sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
2002
|
+
out[k] = canonicalizeDeep(src[k]);
|
|
2003
|
+
}
|
|
2004
|
+
return out;
|
|
2005
|
+
}
|
|
2006
|
+
return value;
|
|
2007
|
+
}
|
|
2008
|
+
function annotateQueryComponent(component, rootEntity, cardinalityOne, manifest) {
|
|
2009
|
+
const root = {
|
|
2010
|
+
entity: rootEntity,
|
|
2011
|
+
projection: [],
|
|
2012
|
+
children: /* @__PURE__ */ new Map(),
|
|
2013
|
+
implicitFields: /* @__PURE__ */ new Set()
|
|
2014
|
+
};
|
|
2015
|
+
const rootNode = component.body.find((n) => n.id === "root");
|
|
2016
|
+
if (rootNode !== void 0) {
|
|
2017
|
+
const ports = nodePorts(rootNode);
|
|
2018
|
+
if (ports !== void 0) root.projection = nodeProjection(ports);
|
|
2019
|
+
}
|
|
2020
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
2021
|
+
for (const node of component.body) {
|
|
2022
|
+
if (node.id === "root") continue;
|
|
2023
|
+
insertNode(root, node, manifest, nodeById);
|
|
2024
|
+
}
|
|
2025
|
+
const annotatedBody = component.body.map((node) => {
|
|
2026
|
+
if ("cond" in node) {
|
|
2027
|
+
throw new Error(
|
|
2028
|
+
`type-notation: unexpected 'cond' node '${node.id}' in query component '${component.name}'; query lowering does not emit conditionals, so its outType cannot be derived.`
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
if (node.id === "root") {
|
|
2032
|
+
const outType2 = isConnectionRootOp(nodeComponent(node), cardinalityOne) ? wrapRelation(nodeObjType(root, manifest, `${component.name}.root`, true), "connection") : nodeObjType(root, manifest, `${component.name}.root`, false);
|
|
2033
|
+
return { ...node, outType: outType2 };
|
|
2034
|
+
}
|
|
2035
|
+
const rn = nodeById.get(node.id);
|
|
2036
|
+
if (rn === void 0) {
|
|
2037
|
+
throw new Error(
|
|
2038
|
+
`type-notation: no result-tree node for body node '${node.id}' in component '${component.name}'; the model\u2192annotation walk is incomplete (fail-closed \u2014 not skipped).`
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
const obj = nodeObjType(rn, manifest, `${component.name}.${node.id}`, true);
|
|
2042
|
+
const outType = wrapRelation(obj, rn.relationKind);
|
|
2043
|
+
return { ...node, outType };
|
|
2044
|
+
});
|
|
2045
|
+
const hasRelations = root.children.size > 0;
|
|
2046
|
+
const rootIsConnection = rootNode !== void 0 && isConnectionRootOp(nodeComponent(rootNode), cardinalityOne);
|
|
2047
|
+
let outputType;
|
|
2048
|
+
if (rootIsConnection) {
|
|
2049
|
+
outputType = wrapRelation(nodeObjType(root, manifest, `${component.name}#output`, true), "connection");
|
|
2050
|
+
} else if (!hasRelations) {
|
|
2051
|
+
outputType = nodeObjType(root, manifest, `${component.name}#output.root`, false);
|
|
2052
|
+
} else {
|
|
2053
|
+
const rootObj = nodeObjType(root, manifest, `${component.name}#output.root`, false);
|
|
2054
|
+
const fields = { root: rootObj };
|
|
2055
|
+
for (const [prop, child] of root.children) {
|
|
2056
|
+
const childObj = nodeObjType(child, manifest, `${component.name}#output.${prop}`, true);
|
|
2057
|
+
fields[prop] = wrapRelation(childObj, child.relationKind);
|
|
2058
|
+
}
|
|
2059
|
+
outputType = { obj: fields };
|
|
2060
|
+
}
|
|
2061
|
+
return canonicalizeDeep({ ...component, body: annotatedBody, outputType });
|
|
2062
|
+
}
|
|
2063
|
+
function annotateComponents(components, typeFacts, manifest) {
|
|
2064
|
+
const out = components.map((c) => {
|
|
2065
|
+
const facts = typeFacts.get(c.name);
|
|
2066
|
+
if (facts === void 0) return c;
|
|
2067
|
+
try {
|
|
2068
|
+
return annotateQueryComponent(c, facts.rootEntity, facts.cardinalityOne, manifest);
|
|
2069
|
+
} catch (err) {
|
|
2070
|
+
if (err instanceof UndeterminablePortableType) return c;
|
|
2071
|
+
throw err;
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
assertPortableComponentGraph({
|
|
2075
|
+
irVersion: 1,
|
|
2076
|
+
exprVersion: EXPR_VERSION,
|
|
2077
|
+
components: out
|
|
2078
|
+
});
|
|
2079
|
+
return out;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1879
2082
|
// src/spec/operations.ts
|
|
1880
2083
|
function paramSpecs(params) {
|
|
1881
2084
|
const out = {};
|
|
@@ -2311,6 +2514,9 @@ function buildQuerySpec(def) {
|
|
|
2311
2514
|
operations,
|
|
2312
2515
|
// operation 'query' → a single entity object; 'list' → a connection.
|
|
2313
2516
|
cardinality: def.operation === "query" ? "one" : "many",
|
|
2517
|
+
// The root model — the SSoT anchor for the bc#45 type-notation derivation
|
|
2518
|
+
// (build-internal; dropped by toPortableQuery, so operations.json is unchanged).
|
|
2519
|
+
entity: def.entity.name,
|
|
2314
2520
|
...executionPlan !== void 0 ? { executionPlan } : {},
|
|
2315
2521
|
// Pure-documentation description (issue #154); absent → byte-identical spec.
|
|
2316
2522
|
...def.description !== void 0 ? { description: def.description } : {}
|
|
@@ -2398,6 +2604,15 @@ function buildOperations(queries = {}, commands = {}, transactions = {}, contrac
|
|
|
2398
2604
|
},
|
|
2399
2605
|
contractSpecs
|
|
2400
2606
|
);
|
|
2607
|
+
const typeFacts = /* @__PURE__ */ new Map();
|
|
2608
|
+
for (const [name, spec] of Object.entries(querySpecs)) {
|
|
2609
|
+
if (spec.entity === void 0) continue;
|
|
2610
|
+
typeFacts.set(name, {
|
|
2611
|
+
rootEntity: spec.entity,
|
|
2612
|
+
cardinalityOne: spec.cardinality === "one"
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
const annotatedComponents = typeFacts.size > 0 ? annotateComponents(components, typeFacts, buildManifest()) : components;
|
|
2401
2616
|
const portableQueries = {};
|
|
2402
2617
|
for (const [name, spec] of Object.entries(querySpecs)) {
|
|
2403
2618
|
portableQueries[name] = toPortableQuery(name, spec);
|
|
@@ -2416,7 +2631,7 @@ function buildOperations(queries = {}, commands = {}, transactions = {}, contrac
|
|
|
2416
2631
|
return {
|
|
2417
2632
|
version: operationsSpecVersion(content),
|
|
2418
2633
|
...content,
|
|
2419
|
-
...
|
|
2634
|
+
...annotatedComponents.length > 0 ? { components: annotatedComponents } : {}
|
|
2420
2635
|
};
|
|
2421
2636
|
}
|
|
2422
2637
|
|