graphddb 0.7.10 → 0.8.1
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 +6 -6
- package/dist/cdc/index.d.ts +389 -5
- package/dist/cdc/index.js +4 -4
- package/dist/{chunk-AD6ZQTTE.js → chunk-GS4C5VGO.js} +2 -6
- package/dist/{chunk-DFUKGU2Q.js → chunk-HNY2EJPV.js} +216 -229
- package/dist/{chunk-3ZU2VW3L.js → chunk-L2NEDS7U.js} +582 -781
- package/dist/chunk-L4QRCHRQ.js +278 -0
- package/dist/chunk-LAT64YCZ.js +1987 -0
- package/dist/chunk-S2NI4PBW.js +187 -0
- package/dist/{chunk-EOJDN3SA.js → chunk-T44OB5GU.js} +3757 -6138
- package/dist/{chunk-PDUVTYC5.js → chunk-XTWXMOHD.js} +0 -1
- package/dist/cli.js +63 -254
- package/dist/index.d.ts +23 -1550
- package/dist/index.js +94 -1850
- package/dist/internal/index.d.ts +84 -0
- package/dist/internal/index.js +701 -0
- package/dist/{maintenance-view-adapter-BAZ9uBGe.d.ts → key-DZtjAQDh.d.ts} +573 -1817
- package/dist/linter/index.d.ts +39 -7
- package/dist/linter/index.js +22 -4
- package/dist/{registry-LWE54Sdc.d.ts → linter-DQY7gUEk.d.ts} +22 -22
- package/dist/prepared-artifact-HFealr1q.d.ts +281 -0
- package/dist/spec/index.d.ts +506 -5
- package/dist/spec/index.js +22 -18
- package/dist/testing/index.d.ts +2 -3
- package/dist/testing/index.js +4 -4
- package/dist/transform/index.d.ts +1 -1
- package/dist/transform/index.js +4 -4
- package/dist/{types-BQLzTEqh.d.ts → types-2PMXEn5x.d.ts} +8 -10
- package/dist/types-DW__-Icc.d.ts +450 -0
- package/docs/cdc-projection.md +5 -5
- package/docs/class-hydration.md +1 -1
- package/docs/cqrs-contract.md +79 -20
- package/docs/design-patterns.md +5 -5
- package/docs/docs-generation.md +6 -6
- package/docs/middleware.md +15 -15
- package/docs/mutation-command-derivation.md +52 -42
- package/docs/prepared-statements.md +14 -14
- package/docs/python-bridge.md +96 -65
- package/docs/spec.md +153 -124
- package/docs/testing.md +9 -8
- package/package.json +14 -4
- package/dist/chunk-3UD3XIF2.js +0 -860
- package/dist/chunk-MMVHOUM4.js +0 -24
- package/dist/from-change-Ty95KA8C.d.ts +0 -327
- package/dist/index-Dc7d8mWI.d.ts +0 -1089
- 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 `
|
|
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
|
-
*
|
|
467
|
-
* relations attached directly), `'many'` for `
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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-DZtjAQDh.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 };
|
package/docs/cdc-projection.md
CHANGED
|
@@ -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. `
|
|
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 =
|
|
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 | `
|
|
142
|
-
| mutation | `
|
|
143
|
-
| subscription | `
|
|
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.
|
package/docs/class-hydration.md
CHANGED
|
@@ -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* (`
|
|
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
|