graphddb 0.7.10 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +6 -6
  2. package/dist/cdc/index.d.ts +389 -5
  3. package/dist/cdc/index.js +4 -4
  4. package/dist/{chunk-AD6ZQTTE.js → chunk-GS4C5VGO.js} +2 -6
  5. package/dist/{chunk-DFUKGU2Q.js → chunk-HNY2EJPV.js} +216 -229
  6. package/dist/{chunk-EOJDN3SA.js → chunk-I4LEJ4TF.js} +3744 -6144
  7. package/dist/{chunk-3ZU2VW3L.js → chunk-L2NEDS7U.js} +582 -781
  8. package/dist/chunk-L4QRCHRQ.js +278 -0
  9. package/dist/chunk-LGHSZIEE.js +187 -0
  10. package/dist/chunk-N4NWYNGZ.js +1987 -0
  11. package/dist/{chunk-PDUVTYC5.js → chunk-XTWXMOHD.js} +0 -1
  12. package/dist/cli.js +63 -254
  13. package/dist/index.d.ts +23 -1550
  14. package/dist/index.js +94 -1850
  15. package/dist/internal/index.d.ts +84 -0
  16. package/dist/internal/index.js +701 -0
  17. package/dist/{maintenance-view-adapter-BAZ9uBGe.d.ts → key-DR7_lpyk.d.ts} +504 -1910
  18. package/dist/linter/index.d.ts +39 -7
  19. package/dist/linter/index.js +22 -4
  20. package/dist/{registry-LWE54Sdc.d.ts → linter-C-vypgut.d.ts} +22 -22
  21. package/dist/prepared-artifact-BpPgkXEo.d.ts +281 -0
  22. package/dist/spec/index.d.ts +506 -5
  23. package/dist/spec/index.js +22 -18
  24. package/dist/testing/index.d.ts +2 -3
  25. package/dist/testing/index.js +4 -4
  26. package/dist/transform/index.d.ts +1 -1
  27. package/dist/transform/index.js +4 -4
  28. package/dist/{types-BQLzTEqh.d.ts → types-2PMXEn5x.d.ts} +8 -10
  29. package/dist/types-BXLzIcQD.d.ts +450 -0
  30. package/docs/cdc-projection.md +5 -5
  31. package/docs/class-hydration.md +1 -1
  32. package/docs/cqrs-contract.md +28 -20
  33. package/docs/design-patterns.md +5 -5
  34. package/docs/docs-generation.md +6 -6
  35. package/docs/middleware.md +15 -15
  36. package/docs/mutation-command-derivation.md +52 -42
  37. package/docs/prepared-statements.md +14 -14
  38. package/docs/python-bridge.md +96 -65
  39. package/docs/spec.md +153 -124
  40. package/docs/testing.md +9 -8
  41. package/package.json +14 -4
  42. package/dist/chunk-3UD3XIF2.js +0 -860
  43. package/dist/chunk-MMVHOUM4.js +0 -24
  44. package/dist/from-change-Ty95KA8C.d.ts +0 -327
  45. package/dist/index-Dc7d8mWI.d.ts +0 -1089
  46. package/dist/relation-depth-BRS513Tq.d.ts +0 -36
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * - the **TypeScript value type** `T` it represents (`string`, `number`, or a
10
10
  * string-literal union), preserved through the IR and the inferred return
11
- * types of `defineQueries` / `defineCommands`; and
11
+ * types of the contract layer (`publishQuery` / `publishCommand`); and
12
12
  * - a **runtime descriptor** (`kind` + optional `literals`) that the static
13
13
  * planner (#42) and the code generator read to emit `operations.json`.
14
14
  *
@@ -132,8 +132,6 @@ declare const param: {
132
132
  */
133
133
  readonly array: <const E extends ArrayElementShape>(element: E, options?: ParamOptions) => ArrayParam<E>;
134
134
  };
135
- /** Runtime type guard: is `value` a parameter placeholder? */
136
- declare function isParam(value: unknown): value is Param<unknown>;
137
135
 
138
136
  /**
139
137
  * Serializable static operation specs and manifest (issue #42, Python-bridge
@@ -463,8 +461,8 @@ interface QuerySpec {
463
461
  readonly operations: readonly OperationSpec[];
464
462
  /**
465
463
  * Result cardinality of the **root** (`$`) operation, derived from the
466
- * definition kind: `'one'` for `defineQuery` (a single entity object, with any
467
- * relations attached directly), `'many'` for `defineList` (a `{ items, cursor }`
464
+ * operation kind: `'one'` for a `query` (a single entity object, with any
465
+ * relations attached directly), `'many'` for a `list` (a `{ items, cursor }`
468
466
  * connection). The relation runtime (#45) uses this to shape the root result:
469
467
  * a `'one'` Query root takes the first matched item rather than returning a
470
468
  * connection. Absent in specs produced before #45; consumers should treat an
@@ -481,8 +479,8 @@ interface QuerySpec {
481
479
  */
482
480
  readonly executionPlan?: ExecutionPlanSpec;
483
481
  /**
484
- * Optional human-readable description of the query (issue #154), from
485
- * `defineQuery(..., { description })`. Pure documentation — absent unless
482
+ * Optional human-readable description of the query (issue #154), from a
483
+ * contract method's `description` field. Pure documentation — absent unless
486
484
  * declared, so a query with no description serializes byte-identically to the
487
485
  * pre-#154 spec. Surfaces as the generated Python repository-method docstring.
488
486
  */
@@ -656,8 +654,8 @@ interface CommandSpec {
656
654
  /** Optional write condition (subset). */
657
655
  readonly condition?: ConditionSpec;
658
656
  /**
659
- * Optional human-readable description of the command (issue #154), from
660
- * `definePut`/`defineUpdate`/`defineDelete(..., { description })`. Pure
657
+ * Optional human-readable description of the command (issue #154), from a
658
+ * contract method's `description` field. Pure
661
659
  * documentation — absent unless declared, so a command with no description
662
660
  * serializes byte-identically to the pre-#154 spec. Surfaces as the generated
663
661
  * Python repository-method docstring.
@@ -1204,4 +1202,4 @@ interface BridgeBundle {
1204
1202
  readonly operations: OperationsDocument;
1205
1203
  }
1206
1204
 
1207
- export { type WriteOperationType as $, type ExpressionScalar as A, type BridgeBundle as B, type ContractSpec as C, type ManifestEntity as D, type ExpressionSpec as E, type FilterSpec as F, type ManifestField as G, type ManifestFieldType as H, type ManifestGsi as I, type ManifestKey as J, type ManifestRelation as K, type ManifestTable as L, type Manifest as M, type OperationSpec as N, type OperationsDocument as O, type ParamSpec as P, type QuerySpec as Q, type QueryContractMethodSpec as R, SPEC_VERSION as S, type TransactionSpec as T, type RangeConditionSpec as U, type ReadOperationType as V, SPEC_VERSION_SCP as W, SPEC_VERSION_SUPPORTED as X, type TransactionItemSpec as Y, type TransactionItemType as Z, type WhenSpec as _, type CommandSpec as a, type ParamKind as a0, type LiteralParam as a1, type NumberParam as a2, type StringParam as a3, isParam as a4, param as a5, type ContextSpec as b, type Param as c, type ConditionSpec as d, type SpecVersion as e, type CommandContractMethodSpec as f, type CommandResolutionTarget as g, type ComposeSpec as h, type CompositionPlanSpec as i, type ContractCardinality as j, type ContractCommandResult as k, type ContractInputArity as l, type ContractKeySpec as m, type ContractKind as n, type ContractResolution as o, EXPR_VERSION as p, type ExecutionPlanSpec as q, type ExpressionArrNode as r, type ExpressionFloatNode as s, type ExpressionIntNode as t, type ExpressionNode as u, type ExpressionObjNode as v, type ExpressionOpNode as w, type ExpressionOperator as x, type ExpressionRefNode as y, type ExpressionRefOptNode as z };
1205
+ export { type LiteralParam as $, type ManifestEntity as A, type BridgeBundle as B, type ContractSpec as C, type ManifestField as D, type ExpressionSpec as E, type FilterSpec as F, type ManifestFieldType as G, type ManifestGsi as H, type ManifestKey as I, type ManifestRelation as J, type ManifestTable as K, type OperationSpec as L, type Manifest as M, type QueryContractMethodSpec as N, type OperationsDocument as O, type ParamSpec as P, type QuerySpec as Q, type RangeConditionSpec as R, SPEC_VERSION as S, type TransactionSpec as T, type ReadOperationType as U, SPEC_VERSION_SCP as V, SPEC_VERSION_SUPPORTED as W, type TransactionItemSpec as X, type TransactionItemType as Y, type WhenSpec as Z, type WriteOperationType as _, type CommandSpec as a, type NumberParam as a0, type Param as a1, type ParamKind as a2, type StringParam as a3, param as a4, type ContextSpec as b, type ConditionSpec as c, type SpecVersion as d, type CommandContractMethodSpec as e, type CommandResolutionTarget as f, type ComposeSpec as g, type CompositionPlanSpec as h, type ContractCardinality as i, type ContractCommandResult as j, type ContractInputArity as k, type ContractKeySpec as l, type ContractKind as m, type ContractResolution as n, EXPR_VERSION as o, type ExecutionPlanSpec as p, type ExpressionArrNode as q, type ExpressionFloatNode as r, type ExpressionIntNode as s, type ExpressionNode as t, type ExpressionObjNode as u, type ExpressionOpNode as v, type ExpressionOperator as w, type ExpressionRefNode as x, type ExpressionRefOptNode as y, type ExpressionScalar as z };
@@ -0,0 +1,450 @@
1
+ import { d as SegmentedKey, e as ProjectionTransform, f as MaintainEvent, g as ProjectionMap, h as MaintainConsistency, i as MaintainUpdateMode, E as EffectPath, j as MembershipPredicate } from './key-DR7_lpyk.js';
2
+
3
+ type DynamoType = 'S' | 'N' | 'BOOL' | 'B' | 'SS' | 'NS' | 'L' | 'M';
4
+ interface FieldOptions {
5
+ format?: 'datetime' | 'date';
6
+ readonly?: boolean;
7
+ serialize?: (value: unknown) => unknown;
8
+ deserialize?: (value: unknown) => unknown;
9
+ /**
10
+ * Optional human-readable description of the field (issue #154). Pure
11
+ * documentation metadata — it does NOT affect storage, hydration, key handling,
12
+ * or any runtime behaviour. When present it is propagated to the field entry in
13
+ * `manifest.json` ({@link ManifestField.description}) and surfaces as a field
14
+ * comment in the generated Python `types.py`. Absent → byte-for-byte unchanged
15
+ * output (backward compatible).
16
+ */
17
+ description?: string;
18
+ }
19
+ interface FieldMetadata {
20
+ propertyName: string;
21
+ dynamoType: DynamoType;
22
+ options?: FieldOptions;
23
+ /**
24
+ * For a `@literal(...)` field — a `string`-stored field whose value is one of a
25
+ * known, finite set — the allowed literal values, in declaration order (issue
26
+ * #75). Recorded purely as model metadata so a Contract param bound into the
27
+ * field can recover the precise `literal` param-kind from its bind position
28
+ * (`src/spec/contracts.ts`), serializing as `{ type: 'literal', literals: [...] }`
29
+ * instead of the imprecise `'string'`. JSON-safe by construction (a non-empty
30
+ * array of strings). `undefined` for every other field decorator (`@string`,
31
+ * `@number`, …), so plain fields are byte-for-byte unchanged (backward compat).
32
+ */
33
+ literals?: readonly string[];
34
+ /**
35
+ * For an `@ttl` field (issue #172, Epic #167 — CFn generator C4) — marks this
36
+ * field as the model's **DynamoDB Time-To-Live** attribute. `@ttl` is a
37
+ * standalone semantic decorator pinned to `number` (it records `dynamoType: 'N'`
38
+ * itself, so `@ttl expiresAt!: string` is a compile error), because DynamoDB TTL
39
+ * requires a Number attribute holding **epoch seconds**. It is a **physical
40
+ * schema** fact (unlike the maintenance signals kept off the manifest): the flag
41
+ * flows through {@link EntityMetadata} into `manifest.json`
42
+ * ({@link ManifestEntity.ttlAttribute}), which the CloudFormation emitter consumes
43
+ * to render `TimeToLiveSpecification`. DynamoDB allows exactly one TTL attribute
44
+ * per table, enforced loudly at model registration (`@model`). `undefined` for
45
+ * every other field decorator, so non-TTL fields are byte-for-byte unchanged.
46
+ */
47
+ ttl?: boolean;
48
+ }
49
+
50
+ /**
51
+ * The maintenance kind of a model (issue #152). `'entity'` (the default) is a plain
52
+ * stored entity. `'materializedView'` / `'sparseView'` mark a model as a maintained
53
+ * **view** row whose `@maintainedFrom` declarations feed the maintenance graph (the
54
+ * adapter that replaces the old `defineView` / `ViewRegistry` path). A `sparseView`
55
+ * requires every `@maintainedFrom` to carry a `when` membership predicate.
56
+ */
57
+ type ModelKind = 'entity' | 'materializedView' | 'sparseView';
58
+ /**
59
+ * One lowered `@maintainedFrom(source, (self, source) => options)` declaration on a
60
+ * view model (issue #152). It is the decorator-declarative replacement for one
61
+ * `defineView` source slice: the symbolic `(self, source) =>` callback has already
62
+ * been evaluated into the SAME payload-rooted IR (`keyBind` → `$.entity.*` key
63
+ * binding, `project` → {@link ProjectionMap}), so the adapter
64
+ * (`src/relation/maintenance-view-adapter.ts`) reconstructs a byte-identical
65
+ * `ViewSourceSlice` from it.
66
+ *
67
+ * `collection` and `when` are mutually exclusive (a row either holds a maintained
68
+ * list or appears/disappears as a whole — the same rule the old builder enforced).
69
+ */
70
+ interface MaintainedFromMetadata {
71
+ /** Resolves the source entity whose lifecycle events maintain the view row. */
72
+ sourceFactory: () => new (...args: unknown[]) => unknown;
73
+ /** The lifecycle events that maintain the view row (`created`/`updated`/`removed`). */
74
+ on: readonly MaintainEvent[];
75
+ /** The view-row key binding (view-row key field → payload-rooted source value). */
76
+ keyBind: Readonly<Record<string, EffectPath>>;
77
+ /** The projection of the source payload into the view row (target attr → transform). */
78
+ project: ProjectionMap;
79
+ /** Bounded/ordered collection options when this declaration maintains a list. */
80
+ collection?: {
81
+ /** The view-row attribute that holds the maintained list (a `self.<field>`). */
82
+ field: string;
83
+ maxItems?: number;
84
+ order?: 'ASC' | 'DESC';
85
+ /** The source value the collection orders by / trims on (`$.entity.<field>`). */
86
+ orderBy?: EffectPath;
87
+ };
88
+ /** Sparse-view membership predicate (mutually exclusive with `collection`). */
89
+ when?: MembershipPredicate;
90
+ consistency?: MaintainConsistency;
91
+ updateMode?: MaintainUpdateMode;
92
+ /**
93
+ * Optional human-readable description of this maintained-from source slice (issue
94
+ * #166, follow-up of #154), supplied via
95
+ * `@maintainedFrom(() => Source, (self, source) => ({ description, ... }))`. Pure
96
+ * documentation metadata — it does NOT affect the maintenance IR (`keyBind` /
97
+ * `project` / `on` / `collection` / `when`) or any runtime behaviour.
98
+ *
99
+ * NOTE (representation): the maintenance IR is deliberately kept OFF the
100
+ * serializable `manifest.json` / `operations.json` — the {@link Manifest} carries
101
+ * only the physical shape (table / keys / GSIs / relation navigation), NOT the
102
+ * maintenance graph (see `detectStreamMaintenance` in `src/codegen/cloudformation.ts`).
103
+ * There is therefore no manifest / operations / generated-Python site for a
104
+ * maintained-from source; this description is captured at the metadata layer only
105
+ * (mirroring where #154 captured its descriptions before propagation), available to
106
+ * any future consumer that DOES surface `@maintainedFrom` (e.g. the CDC-projection
107
+ * typed-consumer path, #153). Absent → byte-for-byte unchanged metadata.
108
+ */
109
+ description?: string;
110
+ }
111
+ interface KeyDefinition {
112
+ /** The canonical structured key (PK/SK segment lists). */
113
+ segmented: SegmentedKey;
114
+ inputFieldNames: string[];
115
+ }
116
+ interface GsiDefinition {
117
+ indexName: string;
118
+ /** The canonical structured key (PK/SK segment lists). */
119
+ segmented: SegmentedKey;
120
+ inputFieldNames: string[];
121
+ unique: boolean;
122
+ /**
123
+ * Optional human-readable description of the GSI (issue #166, follow-up of #154),
124
+ * supplied via `gsi(name, key, { description })`. Pure documentation metadata — it
125
+ * does NOT affect the index key, projection, or any runtime behaviour. When present
126
+ * it is propagated to the index entry in `manifest.json`
127
+ * ({@link ManifestGsi.description}) and surfaces as the docstring of any generated
128
+ * Python query method that reads through this index (a query carrying the GSI's
129
+ * `indexName`, when the query itself declares no description). Absent →
130
+ * byte-for-byte unchanged output (backward compatible).
131
+ */
132
+ description?: string;
133
+ }
134
+ interface RelationLimitOptions {
135
+ default: number;
136
+ max: number;
137
+ }
138
+ /**
139
+ * The named maintenance pattern a relation participates in (Epic #118 §5.1).
140
+ *
141
+ * `pattern` is the *preset* selector: omitting it leaves the relation a plain
142
+ * read-only navigation (the historical `hasMany`/`belongsTo`/`hasOne`
143
+ * behaviour), exactly backward compatible. When present it names the AWS
144
+ * design pattern the relation lowers to — the lowering itself (maintenance
145
+ * graph / compile injection) is out of scope for issue #121 and lands in
146
+ * #124/#125; here the value is recorded purely as declaration metadata.
147
+ *
148
+ * Typed as a `string` union of the known presets but kept open-ended via the
149
+ * `(string & {})` tail so a future preset can be authored before this union is
150
+ * widened, without a breaking change to callers.
151
+ *
152
+ * The union lists only the presets that are actually *reachable* as a recorded
153
+ * `RelationOptions.pattern` / `AggregateOptions.pattern` value: `samePartition`,
154
+ * `counter`, `embeddedSnapshot`, and the two versioned discriminators
155
+ * `versionedLatest` / `versionedHistory` (issue #152). `materializedView` /
156
+ * `sparseView` migrated to the model-level `@model({ kind })` selector (they are
157
+ * a {@link ModelKind}, not a relation preset); the placeholder `edge` /
158
+ * `dualEdge` / `versioned` / `externalProjection` names are not authored anywhere
159
+ * and were removed. A future preset can still be written against the `(string & {})`
160
+ * tail before it is added to this union.
161
+ */
162
+ type RelationPattern = 'samePartition' | 'counter' | 'embeddedSnapshot' | 'versionedLatest' | 'versionedHistory' | (string & {});
163
+ /**
164
+ * Consistency boundary a maintained write is declared against (Epic #118 §4.A.7):
165
+ * `transactional` = same `TransactWriteItems`; `eventual` = asynchronous catch-up.
166
+ */
167
+ type RelationConsistency = 'transactional' | 'eventual';
168
+ /**
169
+ * How a maintained write is applied (Epic #118 §4.A.7):
170
+ * `mutation` = composed into the same mutation; `stream` = driven asynchronously
171
+ * off a stream / outbox.
172
+ */
173
+ type RelationUpdateMode = 'mutation' | 'stream';
174
+ /**
175
+ * Read-strategy hints for a maintained relation (Epic #118 §5.1 `read`).
176
+ *
177
+ * All fields optional — present only when a `pattern` declares a read shape
178
+ * (e.g. an `embeddedSnapshot` keeping the latest N items). A bare relation
179
+ * carries no `read` block.
180
+ */
181
+ interface RelationReadOptions {
182
+ /** Cap on materialised items kept inline (e.g. "latest 3 posts"). */
183
+ maxItems?: number;
184
+ /** Ordering of the maintained collection. */
185
+ order?: 'ASC' | 'DESC';
186
+ }
187
+ /**
188
+ * Write-maintenance declaration for a relation (Epic #118 §5.1 `write`).
189
+ *
190
+ * `maintainedOn` is the cross-entity trigger list (`"<Entity>.<event>"`, e.g.
191
+ * `"Post.created"`) that the maintenance graph (#124/#125) will key off. Recorded
192
+ * here as declaration metadata only.
193
+ */
194
+ interface RelationWriteOptions {
195
+ /**
196
+ * Cross-entity triggers, e.g. `['Post.created', 'Post.removed']`. The event
197
+ * segment is the confirmed shared lifecycle vocabulary (epic #118 A/B
198
+ * reconciliation): `created` / `updated` / `removed`.
199
+ */
200
+ maintainedOn?: string[];
201
+ consistency?: RelationConsistency;
202
+ updateMode?: RelationUpdateMode;
203
+ }
204
+ /**
205
+ * Snapshot / projection capture map for a maintained relation
206
+ * (Epic #118 §5.1 / §5.2 `projection`).
207
+ *
208
+ * Maps each captured field name on the maintaining side to **how** the source
209
+ * value is projected. The confirmed shared vocabulary (epic #118 A/B
210
+ * reconciliation) is the **function-form IR** — the same {@link
211
+ * ProjectionTransform} node A (#120) defines on `src/define/entity-writes.ts`,
212
+ * re-used here (NOT re-defined) so the two surfaces never re-diverge. Each value
213
+ * is either:
214
+ *
215
+ * - a {@link ProjectionTransform} (built with the standalone authoring helpers,
216
+ * e.g. `preview('body', 120)` — RFC §5.2 `textPreview: preview('body', 120)`),
217
+ * or
218
+ * - a bare `string` — the **identity shorthand**: project that source path
219
+ * through unchanged (e.g. `{ postId: 'postId' }`).
220
+ *
221
+ * `Readonly` so the recorded declaration metadata is not mutated downstream.
222
+ * Issue #121 scopes this to the declaration shape; the maintenance graph /
223
+ * compile injection that consumes the IR is #124/#125.
224
+ *
225
+ * @example `{ postId: 'postId', textPreview: preview('body', 120) }`
226
+ */
227
+ type RelationProjection = Readonly<Record<string, ProjectionTransform | string>>;
228
+ interface RelationOptions {
229
+ limit?: RelationLimitOptions;
230
+ order?: 'ASC' | 'DESC';
231
+ /**
232
+ * Optional human-readable description of the relation (issue #166, follow-up of
233
+ * #154), supplied via `@hasMany/@belongsTo/@hasOne(() => T, keyBind, { description })`.
234
+ * Pure documentation metadata — it does NOT affect navigation, key binding, or any
235
+ * runtime behaviour. When present it is propagated to the relation entry in
236
+ * `manifest.json` ({@link ManifestRelation.description}) and surfaces as a trailing
237
+ * `# …` comment on the relation's field in the generated Python result TypedDict /
238
+ * dataclass. Absent → byte-for-byte unchanged output (backward compatible).
239
+ */
240
+ description?: string;
241
+ /**
242
+ * Named maintenance preset (Epic #118). Omitted = plain read-only navigation,
243
+ * fully backward compatible with the historical relation decorators.
244
+ */
245
+ pattern?: RelationPattern;
246
+ read?: RelationReadOptions;
247
+ write?: RelationWriteOptions;
248
+ projection?: RelationProjection;
249
+ /**
250
+ * Lowered versioned-pattern declaration (issue #152, section B). Present only when
251
+ * the relation declares `pattern: 'versionedLatest' | 'versionedHistory'` via the
252
+ * `@hasOne/@hasMany(() => Target, (self, source) => ({...}))` callback form. The
253
+ * declaring model is the SOURCE (a revision); the relation target is the
254
+ * DESTINATION view row. The adapter
255
+ * (`src/relation/maintenance-view-adapter.ts`) lowers this to the SAME `defineView`
256
+ * snapshot IR the old `defineVersioned` produced (latest = overwrite, history =
257
+ * append). `on` are the lifecycle events; `project` is the lowered projection map.
258
+ */
259
+ versioned?: {
260
+ readonly mode: 'latest' | 'history';
261
+ readonly on: readonly MaintainEvent[];
262
+ readonly project: ProjectionMap;
263
+ readonly consistency?: MaintainConsistency;
264
+ readonly updateMode?: MaintainUpdateMode;
265
+ };
266
+ }
267
+ /**
268
+ * Descriptor for a `refs` relation (issue #197, Pattern 2 Embedded Refs read
269
+ * side). A `refs` relation resolves an **inline id-list** held on the parent row
270
+ * — e.g. `PPost.tagRefs: { tagId: string }[]` — into the referenced child
271
+ * bodies, in ONE deduped `BatchGetItem` (never per-ref `GetItem`).
272
+ *
273
+ * Unlike `hasMany`/`belongsTo`/`hasOne`, whose key is bound from a parent
274
+ * **scalar** field ({@link RelationMetadata.keyBinding} — `target key ← parent
275
+ * scalar`), a `refs` key is bound from EACH ELEMENT of a parent LIST attribute:
276
+ * the traversal reads `parent[from]` (a list), pulls `.key` off every element,
277
+ * and fans those out into a single BatchGet against the target. This descriptor
278
+ * captures exactly that list-valued source shape, which the scalar `keyBinding`
279
+ * `Record<string,string>` cannot express.
280
+ *
281
+ * `keyBinding` on a `refs` relation still records the **target child key field ←
282
+ * element key name** mapping (e.g. `{ tagId: 'tagId' }`), so key building /
283
+ * projection reuse the existing scalar machinery per resolved element; `refs`
284
+ * only adds *where the scalar values come from* (the parent list), not a new key
285
+ * grammar. That keeps `refs` byte-compatible with every scalar-keyBinding
286
+ * consumer (manifest, planner, dedupe) — an unknown-to-them relation kind is
287
+ * simply carried through with a valid scalar `keyBinding`.
288
+ */
289
+ interface RefsBinding {
290
+ /** The parent LIST attribute holding the inline reference elements (e.g. `'tagRefs'`). */
291
+ from: string;
292
+ /** The key field to read off EACH list element (e.g. `'tagId'`). */
293
+ key: string;
294
+ }
295
+ interface RelationMetadata {
296
+ type: 'hasMany' | 'hasOne' | 'belongsTo' | 'refs';
297
+ propertyName: string;
298
+ targetFactory: () => new (...args: unknown[]) => unknown;
299
+ keyBinding: Record<string, string>;
300
+ /**
301
+ * List-valued source descriptor for a `refs` relation (issue #197). Present
302
+ * IFF `type === 'refs'`; the traversal reads the parent list `refs.from`, pulls
303
+ * `refs.key` off each element, and resolves the referenced child bodies in one
304
+ * deduped BatchGet. Absent for every scalar-keyed relation (byte-compatible).
305
+ */
306
+ refs?: RefsBinding;
307
+ options?: RelationOptions;
308
+ }
309
+ /**
310
+ * The scalar-aggregate value an `@aggregate` field derives from its source entity
311
+ * (Epic #118 §5.2 counter / latest preset; issue #122). It is the function-form
312
+ * IR built by the {@link count} / {@link max} value helpers — the aggregation an
313
+ * `@aggregate(() => Source, keyBinding, { pattern, value })` field maintains:
314
+ *
315
+ * - `count` — the cardinality of source rows matched by the key binding (no
316
+ * source field; e.g. `postCount!: number` ← `count()`).
317
+ * - `max` — the maximum value of a named source `field` (e.g. `lastPostAt!:
318
+ * string` ← `max('createdAt')`).
319
+ *
320
+ * Recorded purely as declaration metadata (issue #122 scope = declaration
321
+ * surface): the maintenance graph (#124) / compile injection (#125) consume it;
322
+ * the effect itself is out of scope here. Kept open-ended via the discriminated
323
+ * `op` so a future aggregation (`min`, `sum`, …) extends the union without a
324
+ * breaking change.
325
+ *
326
+ * @example `count()` → `{ op: 'count' }`
327
+ * @example `max('createdAt')` → `{ op: 'max', field: 'createdAt' }`
328
+ */
329
+ type AggregateValue = {
330
+ readonly op: 'count';
331
+ } | {
332
+ readonly op: 'max';
333
+ readonly field: string;
334
+ };
335
+ /**
336
+ * Options for an `@aggregate` field (Epic #118 §5.2 counter / latest preset;
337
+ * issue #122). Mirrors the maintenance vocabulary a relation declares so the two
338
+ * authoring surfaces stay in lockstep:
339
+ *
340
+ * - `pattern` reuses {@link RelationPattern} (e.g. `'counter'`) — the named
341
+ * maintenance preset, NOT re-defined here.
342
+ * - `value` is the {@link AggregateValue} the field maintains (`count()` /
343
+ * `max('createdAt')`).
344
+ * - `write` reuses {@link RelationWriteOptions} verbatim — `maintainedOn`
345
+ * triggers (`Post.created` / `Post.removed`, the confirmed shared
346
+ * `created|updated|removed` event vocabulary) plus `consistency`
347
+ * (`transactional|eventual`) / `updateMode` (`mutation|stream`). These are NOT
348
+ * re-defined; they are the same shared types B (#121) added.
349
+ */
350
+ interface AggregateOptions {
351
+ /** Named maintenance preset (typically `'counter'`); reuses {@link RelationPattern}. */
352
+ pattern?: RelationPattern;
353
+ /** The scalar aggregation this field maintains (`count()` / `max('createdAt')`). */
354
+ value: AggregateValue;
355
+ /** Cross-entity maintenance triggers / consistency / update-mode (reused from B). */
356
+ write?: RelationWriteOptions;
357
+ }
358
+ /**
359
+ * Declaration metadata recorded by an `@aggregate` field decorator (issue #122).
360
+ *
361
+ * An `@aggregate` is a **scalar** field (`postCount!: number`,
362
+ * `lastPostAt!: string`) whose value is derived — by aggregating rows of a source
363
+ * entity resolved by {@link targetFactory} + {@link keyBinding} — rather than
364
+ * stored directly. It is therefore recorded in its **own** metadata array
365
+ * (`EntityMetadata.aggregates`), distinct from both {@link FieldMetadata} (a
366
+ * plain stored attribute) and {@link RelationMetadata} (a navigation yielding
367
+ * model instances): an aggregate carries a source binding like a relation, but
368
+ * surfaces a scalar value like a field. The collector path mirrors relations
369
+ * (decorator pushes, `@model` drains).
370
+ *
371
+ * Issue #122 scope is the declaration surface + metadata pass-through only; the
372
+ * aggregation effect's compile / runtime (#124/#125/#127) is out of scope.
373
+ */
374
+ interface AggregateMetadata {
375
+ /** The scalar field the aggregate value is written to. */
376
+ propertyName: string;
377
+ /** Resolves the source entity whose rows are aggregated. */
378
+ targetFactory: () => new (...args: unknown[]) => unknown;
379
+ /** The key binding selecting the source rows (target key field → source field). */
380
+ keyBinding: Record<string, string>;
381
+ /** The aggregate preset / value / maintenance-write declaration. */
382
+ options: AggregateOptions;
383
+ }
384
+ interface EmbeddedMetadata {
385
+ propertyName: string;
386
+ modelFactory: () => new (...args: unknown[]) => unknown;
387
+ }
388
+ interface EntityMetadata {
389
+ tableName: string;
390
+ prefix: string;
391
+ fields: FieldMetadata[];
392
+ primaryKey: KeyDefinition | null;
393
+ gsiDefinitions: GsiDefinition[];
394
+ relations: RelationMetadata[];
395
+ /** Scalar `@aggregate` fields (Epic #118 §5.2 counter / latest; issue #122). */
396
+ aggregates: AggregateMetadata[];
397
+ embeddedFields: EmbeddedMetadata[];
398
+ /**
399
+ * The model's maintenance kind (issue #152). `'entity'` (default; also assumed
400
+ * when absent, for hand-built metadata in tests) is a plain stored entity;
401
+ * `'materializedView'` / `'sparseView'` mark it a maintained view row fed by
402
+ * {@link maintainedFrom}.
403
+ */
404
+ kind?: ModelKind;
405
+ /**
406
+ * Optional human-readable description of the entity (issue #154), supplied via
407
+ * `@model({ description })`. Pure documentation metadata — it does NOT affect
408
+ * storage, keys, or any runtime behaviour. When present it is propagated to the
409
+ * entity entry in `manifest.json` ({@link ManifestEntity.description}) and
410
+ * surfaces as the class docstring in the generated Python `types.py`. Absent →
411
+ * byte-for-byte unchanged output (backward compatible).
412
+ */
413
+ description?: string;
414
+ /**
415
+ * Class-level `@maintainedFrom(...)` declarations (issue #152). Non-empty only on
416
+ * a `materializedView` / `sparseView` model; each is one source slice the view
417
+ * row is maintained from. The adapter lowers these to the maintenance graph's
418
+ * `ViewDefinition` IR (replacing the old `defineView` / `ViewRegistry` path).
419
+ * Absent / empty for a plain entity.
420
+ */
421
+ maintainedFrom?: MaintainedFromMetadata[];
422
+ /**
423
+ * Whether the model is flagged **CDC-parseable** by `@cdcProjected()` (issue
424
+ * #153). It carries NO runtime side effect — it is purely a compile-/registration-
425
+ * time capability marker: `Model.fromChange(event)` (and, transitively,
426
+ * `DDBModel.subscribe` routing to this model) is gated on it, and it is the SSoT
427
+ * the codegen reads to emit the `CdcModelRegistry` module augmentation that types
428
+ * `subscribe`'s handlers by model name. `true` only on a model carrying the
429
+ * decorator; absent (⇒ falsy) for every plain entity, so a non-`@cdcProjected`
430
+ * model is byte-for-byte unchanged (backward compatible). Deliberately kept OFF
431
+ * the manifest / bridge (it is a host-runtime + TS-typing concern, like the
432
+ * maintenance signals — not a physical-schema fact like {@link ttlAttribute}).
433
+ */
434
+ cdcProjected?: boolean;
435
+ /**
436
+ * The physical attribute name of the model's `@ttl` field, when one is declared
437
+ * (issue #172, Epic #167 — CFn generator C4). DynamoDB TTL requires a single
438
+ * Number attribute holding epoch seconds; `@model` derives this from the (unique)
439
+ * field carrying {@link FieldMetadata.ttl} and throws if more than one field on the
440
+ * SAME model is `@ttl` (the cross-entity, same-physical-table single-TTL rule is
441
+ * enforced downstream, at manifest build, where all entities on a table are known).
442
+ * A non-key field's physical attribute name is its property name, so this is the
443
+ * `@ttl` field's property name. Propagated to the manifest
444
+ * ({@link ManifestEntity.ttlAttribute}) — the SSoT the CloudFormation emitter reads
445
+ * to render `TimeToLiveSpecification`. Absent when the model declares no `@ttl`.
446
+ */
447
+ ttlAttribute?: string;
448
+ }
449
+
450
+ export type { AggregateOptions as A, DynamoType as D, EntityMetadata as E, FieldOptions as F, ModelKind as M, RelationOptions as R, AggregateValue as a };
@@ -80,7 +80,7 @@ A valid event for this model always has at least one non-null side, so `[null, n
80
80
  is an unambiguous "this event is not for this model" signal. You don't need a separate
81
81
  guard — `fromChange` does routing **and** parse in one call.
82
82
 
83
- ### 3. `DDBModel.subscribe(handlers)` — declarative, batch
83
+ ### 3. `graphddb.subscribe(handlers)` — declarative, batch
84
84
 
85
85
  The third sibling of `query` / `mutate` (GraphQL's query / mutation / **subscription**).
86
86
  It takes a handler map keyed by model name. Unlike `query` / `mutate`, which hit
@@ -89,7 +89,7 @@ DynamoDB, **`subscribe` does not subscribe**: it *returns* a `ChangeHandler`
89
89
 
90
90
  ```ts
91
91
  // graphddb side: build a batch handler that parses + routes + dispatches (no sink, no dedup)
92
- const handler = DDBModel.subscribe({
92
+ const handler = graphddb.subscribe({
93
93
  OrderModel: (old, neu) => { // old, neu: OrderModel | null (typed via the generated registry)
94
94
  if (!neu) return opensearch.delete({ id: old!.orderId });
95
95
  return opensearch.index({ id: neu.orderId, body: { status: neu.status } });
@@ -138,9 +138,9 @@ inferred from the key.
138
138
 
139
139
  | GraphQL | DDBModel | Executed by | Returns |
140
140
  | --- | --- | --- | --- |
141
- | query | `DDBModel.query(envelope)` | graphddb (hits DynamoDB) | result object |
142
- | mutation | `DDBModel.mutate(envelope)` | graphddb (hits DynamoDB) | result object |
143
- | subscription | `DDBModel.subscribe(handlers)` | **you** (mount it) | `ChangeHandler` |
141
+ | query | `graphddb.query(envelope)` | graphddb (hits DynamoDB) | result object |
142
+ | mutation | `graphddb.mutate(envelope)` | graphddb (hits DynamoDB) | result object |
143
+ | subscription | `graphddb.subscribe(handlers)` | **you** (mount it) | `ChangeHandler` |
144
144
 
145
145
  Only subscription is "executed by you, returns a handler rather than a result" —
146
146
  because CDC delivery lives outside the boundary.
@@ -267,7 +267,7 @@ serialized query/`OperationSpec`, or `operations.json`. This is the same rule
267
267
  `#50` (middleware) and `updatable` / `consistentRead` already follow.
268
268
 
269
269
  - **The declarative layer is "what to fetch."** `operations.json` is generated
270
- from *contract definitions* (`publicQueryModel`, etc.) — key, select, filter,
270
+ from *contract definitions* (`publishQuery`, etc.) — key, select, filter,
271
271
  projection, result paths, cardinality, execution plan. It is the serializable,
272
272
  language-neutral plan the Python runtime (`#48`) replays. It already contains
273
273
  **none** of the host-runtime call options: a `grep` of
@@ -83,7 +83,9 @@ field list argument. The access pattern — primary key or GSI — is resolved f
83
83
  the `Model` plus the `key` fields named in the descriptor:
84
84
 
85
85
  ```ts
86
- const UserByEmail = publicQueryModel({
86
+ import { graphddb, param } from 'graphddb';
87
+
88
+ const UserByEmail = graphddb.publishQuery({
87
89
  get: { query: User, key: { email: param.string() }, select: { userId: true, email: true } },
88
90
  });
89
91
  ```
@@ -95,7 +97,7 @@ key, select, options? }`, with exactly one of `query` / `list`. `select` and
95
97
  `options` are separate keys (no `select` nested inside `options`).
96
98
 
97
99
  ```ts
98
- export const ArticleById = publicQueryModel({
100
+ export const ArticleById = graphddb.publishQuery({
99
101
  get: { query: Article, key: { articleId: param.string() },
100
102
  select: { articleId: true, title: true, body: true },
101
103
  options: { consistentRead: false } },
@@ -193,7 +195,7 @@ write-use-case Methods. A command method is a single declarative write
193
195
  result?, mode? }`, with exactly one of `create` / `update` / `remove`.
194
196
 
195
197
  ```ts
196
- export const UserCommands = publicCommandModel({
198
+ export const UserCommands = graphddb.publishCommand({
197
199
  disable: { update: User, key: { userId: param.string() },
198
200
  input: { status: 'disabled', disableReason: param.string() },
199
201
  result: { select: { userId: true, status: true } } },
@@ -214,7 +216,7 @@ export const UserCommands = publicCommandModel({
214
216
  **or** a raw `cond\`…\`` fragment whose value slots may be `param.*` bindings:
215
217
 
216
218
  ```ts
217
- export const UserCommands = publicCommandModel({
219
+ export const UserCommands = graphddb.publishCommand({
218
220
  // declarative operators in a write condition
219
221
  promote: { update: User, key: { userId: param.string() },
220
222
  input: { role: 'admin' },
@@ -276,7 +278,7 @@ read dependency, declared with the `query($ => ({...}))` builder; a cross-
276
278
  fragment reference uses `$.alias.field`:
277
279
 
278
280
  ```ts
279
- export const AccountAccess = publicQueryModel({
281
+ export const AccountAccess = graphddb.publishQuery({
280
282
  get: query($ => ({
281
283
  account: { query: Account, key: { accountId: param.string() },
282
284
  select: { accountId: true } },
@@ -361,7 +363,8 @@ single-model lint framework.
361
363
 
362
364
  A method body is evaluated at definition time with throwing sentinels in place
363
365
  of `keys` / `params`, recorded through a model recorder, and subjected to the
364
- same hardening `defineTransaction` applies:
366
+ same no-runtime-capture build-time hardening the prepared-statement compiler
367
+ applies:
365
368
 
366
369
  1. **Throwing sentinel Proxy** — any property / method access other than the
367
370
  internal brand reads or primitive coercion throws at the access site.
@@ -489,31 +492,36 @@ input produces a byte-identical pre-contract document).
489
492
 
490
493
  ## Public API surface
491
494
 
492
- Definition DSL (`src/define/contract.ts`):
495
+ Definition DSL (`src/define/contract.ts`) — import the `graphddb` namespace
496
+ (`import { graphddb } from 'graphddb'`); `graphddb.publishQuery` /
497
+ `graphddb.publishCommand` are the sole read/write authoring surface:
493
498
 
494
499
  ```ts
495
- publicQueryModel({ alias: { query | list: Model, key, select, options? } })
496
- publicCommandModel({ alias: { create | update | remove: Model, key, input, condition?, result?, mode? } })
497
- query($ => ({ alias: descriptor, ... })) // composite read; cross-fragment refs $.alias.field
498
- mutation($ => ({ alias: descriptor, ... })) // composite write; cross-fragment refs $.alias.field
500
+ graphddb.publishQuery({ alias: { query | list: Model, key, select, options? } })
501
+ graphddb.publishCommand({ alias: { create | update | remove: Model, key, input, condition?, result?, use?, mode? } })
499
502
  param.string() / param.number() / ... // descriptor key/input parameters
500
503
  ```
501
504
 
502
- Runtime (`src/runtime/*`):
505
+ A query method may compose another query contract's method (cross-fragment refs
506
+ via `$.alias.field`); a command descriptor composes its derived write ops and
507
+ maintenance writes into one plan. These are declared inside the
508
+ `publishQuery` / `publishCommand` map — there is no separate public `query()` /
509
+ `mutation()` composite verb (removed in 0.8.0).
503
510
 
504
- ```ts
505
- executeQueryMethod(contract, methodName, key | keys[], params?) // contract-runtime
506
- executeCommandMethod(contract, methodName, key | keys[], params?) // command-runtime
507
- encodePerKeyCursor / decodePerKeyCursor / serializeContractKey // per-key-cursor
508
- ```
511
+ Execution is the two `graphddb.*` verbs (`graphddb.query` / `graphddb.mutate`) —
512
+ they route contract methods through the in-process runtime.
509
513
 
510
- Build / static analysis (`src/spec/*`):
514
+ The contract runtime (`executeQueryMethod` / `executeCommandMethod` /
515
+ per-key-cursor helpers, `src/runtime/*`) is **internal** machinery behind those
516
+ verbs — not a public export.
517
+
518
+ Build / static analysis is the advanced `graphddb/spec` subpath:
511
519
 
512
520
  ```ts
521
+ import { buildContracts, buildContexts } from 'graphddb/spec';
522
+
513
523
  buildContracts(contracts) // → { contracts, queries, commands, transactions }
514
524
  buildContexts(contexts) // → context-ownership specs
515
- assertContractN1Safe(name, spec, isGsiPoint?)
516
- assertContractBoundaries(...) // context-boundary lint
517
525
  ```
518
526
 
519
527
  ## Query vs Command