graphddb 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -9
- package/dist/{chunk-6LEHSX45.js → chunk-F27INYI2.js} +2839 -1081
- package/dist/cli.js +28 -5
- package/dist/index.d.ts +59 -3318
- package/dist/index.js +22 -1822
- package/dist/testing/index.d.ts +2 -2
- package/dist/types-D6qLpw2M.d.ts +4514 -0
- package/package.json +7 -2
- package/dist/types-CDrWiPxp.d.ts +0 -1203
|
@@ -0,0 +1,4514 @@
|
|
|
1
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
|
|
3
|
+
interface RangeCondition {
|
|
4
|
+
operator: 'begins_with';
|
|
5
|
+
key: string;
|
|
6
|
+
value: unknown;
|
|
7
|
+
}
|
|
8
|
+
interface DynamoDBOperationBase {
|
|
9
|
+
tableName: string;
|
|
10
|
+
projectionExpression?: string;
|
|
11
|
+
expressionAttributeNames?: Record<string, string>;
|
|
12
|
+
consistentRead?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Server-side DynamoDB FilterExpression (independent of the
|
|
15
|
+
* KeyConditionExpression and ProjectionExpression). Names/values here are
|
|
16
|
+
* merged into the command's ExpressionAttributeNames/Values by the executor.
|
|
17
|
+
*/
|
|
18
|
+
filterExpression?: string;
|
|
19
|
+
filterExpressionAttributeNames?: Record<string, string>;
|
|
20
|
+
filterExpressionAttributeValues?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
interface GetItemOperation extends DynamoDBOperationBase {
|
|
23
|
+
type: 'GetItem';
|
|
24
|
+
keyCondition: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
interface QueryOperation extends DynamoDBOperationBase {
|
|
27
|
+
type: 'Query';
|
|
28
|
+
indexName?: string;
|
|
29
|
+
keyCondition: Record<string, unknown>;
|
|
30
|
+
rangeCondition?: RangeCondition;
|
|
31
|
+
scanIndexForward?: boolean;
|
|
32
|
+
limit?: number;
|
|
33
|
+
exclusiveStartKey?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
interface BatchGetItemOperation extends DynamoDBOperationBase {
|
|
36
|
+
type: 'BatchGetItem';
|
|
37
|
+
keys: Record<string, unknown>[];
|
|
38
|
+
}
|
|
39
|
+
type DynamoDBOperation = GetItemOperation | QueryOperation | BatchGetItemOperation;
|
|
40
|
+
interface ExecutionPlan {
|
|
41
|
+
operations: DynamoDBOperation[];
|
|
42
|
+
}
|
|
43
|
+
type ResolvedKey = {
|
|
44
|
+
type: 'pk';
|
|
45
|
+
partial: boolean;
|
|
46
|
+
inputFieldNames: string[];
|
|
47
|
+
} | {
|
|
48
|
+
type: 'gsi';
|
|
49
|
+
indexName: string;
|
|
50
|
+
unique: boolean;
|
|
51
|
+
partial: boolean;
|
|
52
|
+
inputFieldNames: string[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Internal brand identifying a {@link RawCondition} produced by {@link cond}.
|
|
57
|
+
*/
|
|
58
|
+
declare const RAW_COND: unique symbol;
|
|
59
|
+
/**
|
|
60
|
+
* A compiled raw filter fragment produced by the {@link cond} escape hatch.
|
|
61
|
+
* Column references are pre-resolved to `#`-aliased placeholders and embedded
|
|
62
|
+
* values to `:`-aliased parameters; the executor merges these alongside the
|
|
63
|
+
* declarative filter's names/values.
|
|
64
|
+
*
|
|
65
|
+
* @typeParam M - The owning model brand. A `cond` is only assignable to a
|
|
66
|
+
* `FilterInput<M>` of the same model, so embedding a different model's column
|
|
67
|
+
* is a compile error.
|
|
68
|
+
*/
|
|
69
|
+
interface RawCondition<M = unknown> {
|
|
70
|
+
readonly [RAW_COND]: true;
|
|
71
|
+
/** @internal phantom — owning-model brand. */
|
|
72
|
+
readonly __model?: M;
|
|
73
|
+
/** Expression fragment with `{name}` / `{value}` markers already inlined. */
|
|
74
|
+
readonly template: TemplateStringsArray;
|
|
75
|
+
/** The interpolated parts, each either a Column ref or an embedded value. */
|
|
76
|
+
readonly parts: unknown[];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The interpolation slot type accepted by {@link cond}. A column reference
|
|
80
|
+
* (`Model.col.<field>`) — required for naming a column — or an embedded value
|
|
81
|
+
* that is type-checked against that column. Bare column-name strings are NOT
|
|
82
|
+
* accepted; you must use a {@link Column} reference, which keeps the fragment
|
|
83
|
+
* refactor-safe.
|
|
84
|
+
*
|
|
85
|
+
* @typeParam M - The owning model brand. Constrains all columns to one model,
|
|
86
|
+
* so passing another model's column is a compile error.
|
|
87
|
+
*/
|
|
88
|
+
type CondSlot<M> = Column<any, M> | string | number | boolean | Date;
|
|
89
|
+
/**
|
|
90
|
+
* Raw DynamoDB condition escape hatch.
|
|
91
|
+
*
|
|
92
|
+
* Use only for conditions the declarative operator objects cannot express.
|
|
93
|
+
* Column names MUST be embedded via `Model.col.<field>` references (never bare
|
|
94
|
+
* strings); values are embedded directly and parameterized.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* filter: cond`${User.col.age} > ${18} and attribute_exists(${User.col.email})`
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
declare function cond<M = unknown>(template: TemplateStringsArray, ...parts: CondSlot<M>[]): RawCondition<M>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Type-safe attribute condition API (DynamoDB `FilterExpression`).
|
|
105
|
+
*
|
|
106
|
+
* Modeled on AWS AppSync's `ModelFilterInput`: field keys map to either an
|
|
107
|
+
* equality shorthand (a bare value) or an operator object. Operators are
|
|
108
|
+
* constrained per field type so that e.g. `beginsWith` on a numeric field is a
|
|
109
|
+
* compile error. Logical groups are expressed with the reserved `and` / `or`
|
|
110
|
+
* keys (arrays) and `not` (a single nested filter).
|
|
111
|
+
*
|
|
112
|
+
* The condition is typed against the *full* Entity `T` (its scalar fields),
|
|
113
|
+
* **not** the `select` projection — so server-side filters can reference
|
|
114
|
+
* attributes that are not projected, and nested-relation filters stay fully
|
|
115
|
+
* type-checked without callbacks (this is what structurally closes the #29
|
|
116
|
+
* type hole).
|
|
117
|
+
*/
|
|
118
|
+
type IsString<V> = [V] extends [string] ? true : false;
|
|
119
|
+
type IsNumber<V> = [V] extends [number | bigint] ? true : false;
|
|
120
|
+
/**
|
|
121
|
+
* Operators common to all scalar field types.
|
|
122
|
+
*
|
|
123
|
+
* @typeParam V - The declared field value type.
|
|
124
|
+
*/
|
|
125
|
+
interface CommonOperators<V> {
|
|
126
|
+
/** `#f = :v` */
|
|
127
|
+
eq?: V;
|
|
128
|
+
/** `#f <> :v` */
|
|
129
|
+
ne?: V;
|
|
130
|
+
/** `#f IN (:v0, :v1, …)` */
|
|
131
|
+
in?: readonly V[];
|
|
132
|
+
/** `attribute_exists(#f)` (true) / `attribute_not_exists(#f)` (false) */
|
|
133
|
+
attributeExists?: boolean;
|
|
134
|
+
/** `attribute_type(#f, :v)` — DynamoDB attribute type code, e.g. `'S'`, `'N'`. */
|
|
135
|
+
attributeType?: string;
|
|
136
|
+
}
|
|
137
|
+
/** Comparison operators valid on orderable (string / number / Date) fields. */
|
|
138
|
+
interface ComparableOperators<V> {
|
|
139
|
+
/** `#f > :v` */
|
|
140
|
+
gt?: V;
|
|
141
|
+
/** `#f >= :v` */
|
|
142
|
+
ge?: V;
|
|
143
|
+
/** `#f < :v` */
|
|
144
|
+
lt?: V;
|
|
145
|
+
/** `#f <= :v` */
|
|
146
|
+
le?: V;
|
|
147
|
+
/** `#f BETWEEN :lo AND :hi` */
|
|
148
|
+
between?: readonly [V, V];
|
|
149
|
+
}
|
|
150
|
+
/** Operators valid only on string fields. */
|
|
151
|
+
interface StringOnlyOperators {
|
|
152
|
+
/** `begins_with(#f, :v)` */
|
|
153
|
+
beginsWith?: string;
|
|
154
|
+
/** `contains(#f, :v)` */
|
|
155
|
+
contains?: string;
|
|
156
|
+
/** `NOT contains(#f, :v)` */
|
|
157
|
+
notContains?: string;
|
|
158
|
+
/** `size(#f) = :v` (string length) */
|
|
159
|
+
size?: number;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The operator object accepted for a field of value type `V`.
|
|
163
|
+
*
|
|
164
|
+
* - string → common + comparable + string-only (`beginsWith`, `contains`, …)
|
|
165
|
+
* - number → common + comparable (no `beginsWith` / `contains`)
|
|
166
|
+
* - Date → common + comparable (compared against `Date`)
|
|
167
|
+
* - boolean → common only
|
|
168
|
+
* - other → common only
|
|
169
|
+
*/
|
|
170
|
+
type FieldCondition<V> = IsString<V> extends true ? CommonOperators<V> & ComparableOperators<V> & StringOnlyOperators : IsNumber<V> extends true ? CommonOperators<V> & ComparableOperators<V> : [NonNullable<V>] extends [Date] ? CommonOperators<V> & ComparableOperators<V> : CommonOperators<V>;
|
|
171
|
+
/** A field key may be given as an equality shorthand or an operator object. */
|
|
172
|
+
type FieldFilter<V> = V | FieldCondition<V>;
|
|
173
|
+
/**
|
|
174
|
+
* Same scalar classification used across the public API: a field is filterable
|
|
175
|
+
* when its value type is a leaf scalar (string/number/boolean/Date/bigint/…),
|
|
176
|
+
* not a relation (`DDBModel` / `DDBModel[]`) or embedded object.
|
|
177
|
+
*/
|
|
178
|
+
type FilterableScalar = string | number | boolean | bigint | Date | null | undefined;
|
|
179
|
+
/**
|
|
180
|
+
* The set of filterable scalar field keys of `T` (relations and embedded
|
|
181
|
+
* objects excluded). Mirrors the projection rules in `selectable.ts`.
|
|
182
|
+
*/
|
|
183
|
+
type ScalarKeys<T> = {
|
|
184
|
+
[K in keyof T]: T[K] extends Function ? never : T[K] extends Array<infer E> ? [E] extends [DDBModel] ? never : K : NonNullable<T[K]> extends DDBModel ? never : NonNullable<T[K]> extends FilterableScalar ? K : never;
|
|
185
|
+
}[keyof T];
|
|
186
|
+
/**
|
|
187
|
+
* AppSync `ModelFilterInput`-compatible filter for the full Entity `T`.
|
|
188
|
+
*
|
|
189
|
+
* Field keys (scalar attributes of `T`) accept an equality shorthand or an
|
|
190
|
+
* operator object; the reserved `and` / `or` keys take arrays of nested
|
|
191
|
+
* filters and `not` takes a single nested filter.
|
|
192
|
+
*/
|
|
193
|
+
type FilterInput<T> = ({
|
|
194
|
+
[K in ScalarKeys<T>]?: FieldFilter<T[K]>;
|
|
195
|
+
} & {
|
|
196
|
+
and?: readonly (FilterInput<T> | RawCondition<T>)[];
|
|
197
|
+
or?: readonly (FilterInput<T> | RawCondition<T>)[];
|
|
198
|
+
not?: FilterInput<T> | RawCondition<T>;
|
|
199
|
+
}) | RawCondition<T>;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Internal brand symbol identifying a compiled select builder at runtime.
|
|
203
|
+
* Consumers should not depend on this; it is used by the execution layer to
|
|
204
|
+
* distinguish a builder instance from a plain select object.
|
|
205
|
+
*/
|
|
206
|
+
declare const SELECT_BUILDER: unique symbol;
|
|
207
|
+
/**
|
|
208
|
+
* The normalized spec a select builder resolves to. This is the shape the
|
|
209
|
+
* planner / traversal layer consumes. `filter` is the declarative server-side
|
|
210
|
+
* DynamoDB FilterExpression input.
|
|
211
|
+
*
|
|
212
|
+
* @internal
|
|
213
|
+
*/
|
|
214
|
+
interface SelectBuilderSpec {
|
|
215
|
+
readonly [SELECT_BUILDER]: true;
|
|
216
|
+
select: Record<string, unknown>;
|
|
217
|
+
filter?: Record<string, unknown>;
|
|
218
|
+
limit?: number;
|
|
219
|
+
after?: string;
|
|
220
|
+
order?: 'ASC' | 'DESC';
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Top-level select builder produced by `Model.project(...)`.
|
|
224
|
+
*
|
|
225
|
+
* Two-stage, model-bound inference: the entity type `T` is fixed by the model,
|
|
226
|
+
* and the projection `S` is inferred from the `project(...)` argument.
|
|
227
|
+
*
|
|
228
|
+
* `.filter(...)` is the declarative server-side DynamoDB FilterExpression,
|
|
229
|
+
* typed against the full Entity `T` (so it may reference unprojected attributes
|
|
230
|
+
* and type-checks operators / value types).
|
|
231
|
+
*
|
|
232
|
+
* @typeParam T - The (model-bound) Entity type.
|
|
233
|
+
* @typeParam S - The (fixed) select projection.
|
|
234
|
+
*/
|
|
235
|
+
interface SelectBuilder<T, S> {
|
|
236
|
+
/** Declarative server-side DynamoDB FilterExpression, typed against `T`. */
|
|
237
|
+
filter(filter: FilterInput<T>): SelectBuilder<T, S>;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Relation select builder produced by `Model.relation(...)`. Same model-bound
|
|
241
|
+
* two-stage inference as {@link SelectBuilder}, plus per-relation pagination
|
|
242
|
+
* controls.
|
|
243
|
+
*
|
|
244
|
+
* @typeParam T - The (model-bound) relation target Entity type.
|
|
245
|
+
* @typeParam S - The (fixed) select projection for this relation.
|
|
246
|
+
*/
|
|
247
|
+
interface RelationBuilder<T, S> {
|
|
248
|
+
filter(filter: FilterInput<T>): RelationBuilder<T, S>;
|
|
249
|
+
limit(limit: number): RelationBuilder<T, S>;
|
|
250
|
+
after(cursor: string): RelationBuilder<T, S>;
|
|
251
|
+
order(order: 'ASC' | 'DESC'): RelationBuilder<T, S>;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Extracts the projection `S` carried by a top-level or relation builder.
|
|
255
|
+
* Used by `QueryResult` so a builder in a select position resolves to the same
|
|
256
|
+
* result type as the bare projection it wraps.
|
|
257
|
+
*/
|
|
258
|
+
type SelectOf<B> = B extends SelectBuilder<infer _T, infer S> ? S : B extends RelationBuilder<infer _RT, infer RS> ? RS : never;
|
|
259
|
+
|
|
260
|
+
type Scalar = string | number | boolean | bigint | symbol | null | undefined | Date | Uint8Array | Set<any>;
|
|
261
|
+
/**
|
|
262
|
+
* Derives the select specification type from an Entity type.
|
|
263
|
+
*
|
|
264
|
+
* - Scalar fields → `true`
|
|
265
|
+
* - Embedded objects (non-DDBModel) → nested `SelectableOf<E>`
|
|
266
|
+
* - hasMany relations (`DDBModel[]`) → `RelationSelect<E>`
|
|
267
|
+
* - belongsTo/hasOne relations (`DDBModel | null`) → `RelationSelect<E>`
|
|
268
|
+
* - Function properties → excluded
|
|
269
|
+
*/
|
|
270
|
+
type SelectableOf<T> = {
|
|
271
|
+
[K in keyof T as T[K] extends Function ? never : K]?: T[K] extends Array<infer E> ? [E] extends [DDBModel] ? RelationSpec<E> : true : NonNullable<T[K]> extends DDBModel ? null extends T[K] ? RelationSpec<NonNullable<T[K]>> : SelectableOf<T[K]> : T[K] extends Scalar ? true : SelectableOf<T[K]>;
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* What a relation field accepts in a `select`: either the plain
|
|
275
|
+
* {@link RelationSelect} object, or a {@link RelationBuilder} produced by the
|
|
276
|
+
* target model's `Model.relation(...)`. The model-bound builder pins the
|
|
277
|
+
* relation's target type, so passing another model's builder here is a compile
|
|
278
|
+
* error (closes #33).
|
|
279
|
+
*/
|
|
280
|
+
type RelationSpec<T> = RelationSelect<T> | RelationBuilder<T, any>;
|
|
281
|
+
/**
|
|
282
|
+
* Plain-object selection spec for a Relation field. Wraps `SelectableOf<T>`
|
|
283
|
+
* with pagination controls and an optional declarative server-side `filter`
|
|
284
|
+
* (typed against the full relation target `T`, so it may reference attributes
|
|
285
|
+
* not in the projection).
|
|
286
|
+
*
|
|
287
|
+
* For arbitrary post-load TypeScript filtering, narrow as far as possible with
|
|
288
|
+
* the declarative server-side `filter`, then apply any remaining JS-only
|
|
289
|
+
* condition on the (already fully typed) result with `result.items.filter(...)`.
|
|
290
|
+
*
|
|
291
|
+
* @typeParam T - The target relation Entity type
|
|
292
|
+
* @typeParam S - The select specification for this relation (inferred per call)
|
|
293
|
+
*/
|
|
294
|
+
type RelationSelect<T, S extends SelectableOf<T> = SelectableOf<T>> = {
|
|
295
|
+
select: S;
|
|
296
|
+
limit?: number;
|
|
297
|
+
after?: string;
|
|
298
|
+
order?: 'ASC' | 'DESC';
|
|
299
|
+
filter?: FilterInput<T>;
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Strict ("exact") version of a select projection that rejects excess
|
|
303
|
+
* properties.
|
|
304
|
+
*
|
|
305
|
+
* # Why this exists
|
|
306
|
+
*
|
|
307
|
+
* `query` / `list` infer the projection as a free generic parameter
|
|
308
|
+
* (`<Sel extends SelectSpec<T>>`, `select: Sel`). When an object literal is
|
|
309
|
+
* matched against a *bare type parameter* TypeScript infers `Sel` to the
|
|
310
|
+
* literal's exact shape and then only checks the constraint
|
|
311
|
+
* `Sel extends SelectSpec<T>` **structurally** — there is no object-literal
|
|
312
|
+
* *freshness* (excess-property) check against a concrete target, because the
|
|
313
|
+
* target (`Sel`) is whatever the literal is. `SelectableOf<T>` has all-optional
|
|
314
|
+
* properties, so an extra key like `BOGUS: true` satisfies the constraint and
|
|
315
|
+
* compiles. The `SelectableOf<T> | SelectBuilder<…>` union compounds this
|
|
316
|
+
* (freshness checks are also suppressed against unions).
|
|
317
|
+
*
|
|
318
|
+
* `StrictSelect` closes the hole without `any`: it maps every key of the
|
|
319
|
+
* inferred `Sel` that is **not** a known key of `SelectableOf<T>` to `never`.
|
|
320
|
+
* Used as an *additional* constraint at the call site
|
|
321
|
+
* (`select: Sel & StrictSelect<T, Sel>`), a stray key forces that property's
|
|
322
|
+
* type to `never`, which the supplied `true` (or nested object) is not
|
|
323
|
+
* assignable to — so the call is a compile error. Known keys keep their
|
|
324
|
+
* inferred value type, so the result-type inference (`QueryResult`) and the
|
|
325
|
+
* builder branch are unaffected.
|
|
326
|
+
*
|
|
327
|
+
* Recurses into the plain-object nested forms so the same rejection applies to
|
|
328
|
+
* nested **relation** selects (`{ select: { … } }`) and **embedded** object
|
|
329
|
+
* selects. Builder values (`RelationBuilder` / `SelectBuilder`) are left as-is —
|
|
330
|
+
* their projection was already constrained to `SelectableOf<T>` when the
|
|
331
|
+
* `Model.relation(...)` / `Model.project(...)` factory produced them.
|
|
332
|
+
*
|
|
333
|
+
* @typeParam T - The Entity type the projection is taken against.
|
|
334
|
+
* @typeParam Sel - The inferred select object (per call).
|
|
335
|
+
*/
|
|
336
|
+
type StrictSelect<T, Sel> = {
|
|
337
|
+
[K in keyof Sel]: K extends keyof SelectableOf<T> ? Sel[K] extends {
|
|
338
|
+
select: infer Nested;
|
|
339
|
+
} ? K extends keyof T ? Sel[K] & {
|
|
340
|
+
select: StrictSelect<RelationTargetOf<T[K]>, Nested>;
|
|
341
|
+
} : Sel[K] : Sel[K] extends object ? Sel[K] extends RelationBuilder<infer _RT, infer _RS> ? Sel[K] : K extends keyof T ? Sel[K] & StrictSelect<NonNullable<T[K]>, Sel[K]> : Sel[K] : Sel[K] : never;
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* Extracts the relation **target entity type** from an entity field type:
|
|
345
|
+
* `E[]` → `E`, `E | null` → `E`. Mirrors the helper in `select-builder.ts`;
|
|
346
|
+
* used by {@link StrictSelect} to recurse a nested relation select against the
|
|
347
|
+
* related model's fields.
|
|
348
|
+
*/
|
|
349
|
+
type RelationTargetOf<F> = F extends Array<infer E> ? E : NonNullable<F> extends DDBModel ? NonNullable<F> : NonNullable<F>;
|
|
350
|
+
/**
|
|
351
|
+
* Call-site excess-property constraint for a top-level `select` (which may be a
|
|
352
|
+
* plain projection **or** a `Model.project(...)` builder).
|
|
353
|
+
*
|
|
354
|
+
* - If `Sel` is a {@link SelectBuilder}, it passes through untouched — the
|
|
355
|
+
* builder's projection was already constrained to `SelectableOf<T>` by the
|
|
356
|
+
* `Model.project(...)` factory, and applying {@link StrictSelect} would
|
|
357
|
+
* wrongly poison the builder's own `filter` member.
|
|
358
|
+
* - Otherwise `Sel` is a plain object and {@link StrictSelect} rejects any key
|
|
359
|
+
* not declared on `T` (top-level and nested).
|
|
360
|
+
*
|
|
361
|
+
* Applied as `select: Sel & StrictSelectSpec<T, Sel>`, so known keys keep their
|
|
362
|
+
* inferred type (result inference unaffected) and stray keys become `never`.
|
|
363
|
+
*
|
|
364
|
+
* @typeParam T - The Entity type the projection is taken against.
|
|
365
|
+
* @typeParam Sel - The inferred select spec (per call).
|
|
366
|
+
*/
|
|
367
|
+
type StrictSelectSpec<T, Sel> = Sel extends SelectBuilder<any, any> ? Sel : StrictSelect<T, Sel>;
|
|
368
|
+
/**
|
|
369
|
+
* Derives the persistable input type for `put()` from an Entity type.
|
|
370
|
+
*
|
|
371
|
+
* - Function properties → excluded
|
|
372
|
+
* - Relation fields (`DDBModel[]`, `DDBModel | null`) → excluded
|
|
373
|
+
* (relations are not written by `put`)
|
|
374
|
+
* - Scalar fields → kept with their declared type
|
|
375
|
+
* - Embedded objects (non-`DDBModel`) → recursively `EntityInput`
|
|
376
|
+
*/
|
|
377
|
+
type EntityInput<T> = {
|
|
378
|
+
[K in keyof T as T[K] extends Function ? never : T[K] extends Array<infer E> ? [E] extends [DDBModel] ? never : K : NonNullable<T[K]> extends DDBModel ? null extends T[K] ? never : K : K]: T[K] extends Scalar ? T[K] : NonNullable<T[K]> extends DDBModel ? EntityInput<NonNullable<T[K]>> : T[K] extends object ? EntityInput<T[K]> : T[K];
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Derives the return type from an Entity type `T` and a select specification `S`.
|
|
383
|
+
*
|
|
384
|
+
* - `S[K] === true` → `T[K]` (scalar value)
|
|
385
|
+
* - `S[K]` has `select` property and `T[K]` is an array (hasMany) →
|
|
386
|
+
* `{ items: QueryResult<E, Sel>[]; cursor: string | null }`
|
|
387
|
+
* - `S[K]` has `select` property and `T[K]` is nullable (belongsTo/hasOne) →
|
|
388
|
+
* `QueryResult<E, Sel> | null`
|
|
389
|
+
* - `S[K]` is a plain object → nested `QueryResult` (embedded)
|
|
390
|
+
* - fallback → `T[K]` (for dynamic/widened select types)
|
|
391
|
+
*
|
|
392
|
+
* Distributes over union types in `S` so that dynamically constructed
|
|
393
|
+
* selects produce a union of result types.
|
|
394
|
+
*/
|
|
395
|
+
|
|
396
|
+
type QueryResult<T, S> = S extends unknown ? {
|
|
397
|
+
[K in keyof S & keyof T]: S[K] extends true ? T[K] : S[K] extends RelationBuilder<infer _RT, infer Sel> ? T[K] extends Array<infer E> ? {
|
|
398
|
+
items: QueryResult<E, Sel>[];
|
|
399
|
+
cursor: string | null;
|
|
400
|
+
} : QueryResult<NonNullable<T[K]>, Sel> | null : S[K] extends {
|
|
401
|
+
select: infer Sel;
|
|
402
|
+
} ? T[K] extends Array<infer E> ? {
|
|
403
|
+
items: QueryResult<E, Sel>[];
|
|
404
|
+
cursor: string | null;
|
|
405
|
+
} : QueryResult<NonNullable<T[K]>, Sel> | null : S[K] extends Record<string, unknown> ? QueryResult<T[K], S[K]> : T[K];
|
|
406
|
+
} : never;
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Compile-time brand for read results that carry a hidden, resolved base-table
|
|
410
|
+
* key (issue #54). A `query(..., { updatable: true })` / `list(..., { updatable:
|
|
411
|
+
* true })` result is typed `QueryResult<...> & Updatable` so it can be passed
|
|
412
|
+
* straight back to `update()`.
|
|
413
|
+
*
|
|
414
|
+
* The brand is phantom (no runtime field). Spreading / cloning / JSON
|
|
415
|
+
* round-tripping a branded value produces a plain object that no longer
|
|
416
|
+
* structurally matches `Updatable`, so the compiler rejects passing a
|
|
417
|
+
* stripped copy where an updatable entity is required. (Runtime existence of
|
|
418
|
+
* the hidden key is enforced separately by `update()`, which throws when it is
|
|
419
|
+
* absent — see `resolveUpdateKey`.)
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* ```ts
|
|
423
|
+
* const user = await User.query({ userId: 'u' }, { name: true }, { updatable: true });
|
|
424
|
+
* if (user) await User.updateItem(user, { name: 'New' }); // ok — branded
|
|
425
|
+
* // await User.updateItem({ ...user }, { name: 'New' }); // compile error — spread drops the brand
|
|
426
|
+
* ```
|
|
427
|
+
*/
|
|
428
|
+
declare const updatableBrand: unique symbol;
|
|
429
|
+
interface Updatable {
|
|
430
|
+
readonly [updatableBrand]: true;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
declare const GSI_MARKER: unique symbol;
|
|
434
|
+
interface GsiDefinitionMarker<T extends Record<string, unknown> = Record<string, unknown>, U extends boolean = boolean> {
|
|
435
|
+
readonly [GSI_MARKER]: true;
|
|
436
|
+
readonly indexName: string;
|
|
437
|
+
readonly segmented: SegmentedKey;
|
|
438
|
+
readonly inputFieldNames: string[];
|
|
439
|
+
readonly unique: U;
|
|
440
|
+
/** @internal Phantom field for type-level input type extraction. */
|
|
441
|
+
readonly _phantom?: T;
|
|
442
|
+
}
|
|
443
|
+
interface GsiOptions {
|
|
444
|
+
unique?: boolean;
|
|
445
|
+
}
|
|
446
|
+
type GsiBuilder<T extends Record<string, unknown>> = (c: {
|
|
447
|
+
readonly [K in keyof T]-?: Column<T[K], T>;
|
|
448
|
+
}) => KeyStructure;
|
|
449
|
+
declare function gsi<T extends Record<string, unknown>>(indexName: string, builder: GsiBuilder<T>, options: GsiOptions & {
|
|
450
|
+
unique: true;
|
|
451
|
+
}): GsiDefinitionMarker<T, true>;
|
|
452
|
+
declare function gsi<T extends Record<string, unknown>>(indexName: string, builder: GsiBuilder<T>, options?: GsiOptions): GsiDefinitionMarker<T, false>;
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Extracts the Primary Key input type from a model class.
|
|
456
|
+
* Scans static properties for `KeyDefinitionMarker<T>` and returns `T`.
|
|
457
|
+
*
|
|
458
|
+
* @typeParam C - The model class constructor (`typeof UserModel`)
|
|
459
|
+
*/
|
|
460
|
+
type PrimaryKeyOf<C> = {
|
|
461
|
+
[K in keyof C]: C[K] extends KeyDefinitionMarker<infer I> ? I : never;
|
|
462
|
+
}[keyof C];
|
|
463
|
+
/**
|
|
464
|
+
* Union of all query key input types (PK + all GSIs).
|
|
465
|
+
* Used as the constraint for `list()`.
|
|
466
|
+
*
|
|
467
|
+
* @typeParam C - The model class constructor (`typeof UserModel`)
|
|
468
|
+
*/
|
|
469
|
+
type QueryKeyOf<C> = {
|
|
470
|
+
[K in keyof C]: C[K] extends KeyDefinitionMarker<infer I> ? I : C[K] extends GsiDefinitionMarker<infer I, any> ? I : never;
|
|
471
|
+
}[keyof C];
|
|
472
|
+
/**
|
|
473
|
+
* Union of unique query key input types (PK + unique GSIs only).
|
|
474
|
+
* Used as the constraint for `query()` — only keys guaranteed to
|
|
475
|
+
* return a single item are allowed.
|
|
476
|
+
*
|
|
477
|
+
* @typeParam C - The model class constructor (`typeof UserModel`)
|
|
478
|
+
*/
|
|
479
|
+
type UniqueQueryKeyOf<C> = {
|
|
480
|
+
[K in keyof C]: C[K] extends KeyDefinitionMarker<infer I> ? I : C[K] extends GsiDefinitionMarker<infer I, true> ? I : never;
|
|
481
|
+
}[keyof C];
|
|
482
|
+
/**
|
|
483
|
+
* "At least one" relaxation of a single key input type `T`: every field is
|
|
484
|
+
* known and correctly typed, but only a non-empty subset need be supplied (the
|
|
485
|
+
* remaining fields become optional).
|
|
486
|
+
*
|
|
487
|
+
* For a composite key `{ groupId: string; userId: string }` this yields
|
|
488
|
+
* `{ groupId: string; userId?: string } | { groupId?: string; userId: string }`
|
|
489
|
+
* — i.e. you may supply just the partition-key field(s) without the full
|
|
490
|
+
* sort-key tail. Unknown fields are still rejected (object-literal excess
|
|
491
|
+
* property checking), so it stays strictly typed.
|
|
492
|
+
*/
|
|
493
|
+
type AtLeastOneKey<T> = {
|
|
494
|
+
[K in keyof T]-?: {
|
|
495
|
+
[P in K]-?: T[P];
|
|
496
|
+
} & {
|
|
497
|
+
[P in Exclude<keyof T, K>]?: T[P];
|
|
498
|
+
} extends infer O ? {
|
|
499
|
+
[P in keyof O]: O[P];
|
|
500
|
+
} : never;
|
|
501
|
+
}[keyof T];
|
|
502
|
+
/**
|
|
503
|
+
* Key type accepted by `list()` / `explain()`. These issue a DynamoDB *Query*
|
|
504
|
+
* against a partition (PK or GSI), so a **partial** key — the partition-key
|
|
505
|
+
* subset of a PK/GSI input — is sufficient; the sort-key portion is optional
|
|
506
|
+
* (it narrows the range, e.g. `begins_with`). This mirrors the runtime key
|
|
507
|
+
* resolver, which accepts any field subset that produces a valid partition key
|
|
508
|
+
* (`src/planner/key-resolver.ts`) and throws otherwise.
|
|
509
|
+
*
|
|
510
|
+
* Distinct from {@link QueryKeyOf} (full key, used as the constraint label by
|
|
511
|
+
* the type tests) and {@link UniqueQueryKeyOf} (full unique key for `query()`,
|
|
512
|
+
* which must resolve to a single item). `query()` deliberately stays strict —
|
|
513
|
+
* only this `list`/`explain` boundary is relaxed.
|
|
514
|
+
*
|
|
515
|
+
* @typeParam C - The model class constructor (`typeof UserModel`)
|
|
516
|
+
*/
|
|
517
|
+
type PartialQueryKeyOf<C> = QueryKeyOf<C> extends infer U ? U extends Record<string, unknown> ? AtLeastOneKey<U> : never : never;
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* A top-level `select` may be either a plain projection (`SelectableOf<T>`) or
|
|
521
|
+
* a {@link SelectBuilder} produced by `Model.project(...)`. The builder is
|
|
522
|
+
* model-bound to `T`, so its `.filter(...)` is typed against the full entity.
|
|
523
|
+
*/
|
|
524
|
+
type SelectSpec<T> = SelectableOf<T> | SelectBuilder<T, any>;
|
|
525
|
+
/**
|
|
526
|
+
* Resolves the projection `S` from a top-level select spec (plain object or
|
|
527
|
+
* builder), so the result type is computed identically for both forms.
|
|
528
|
+
*/
|
|
529
|
+
type ProjectionOf<T, Sel> = Sel extends SelectableOf<T> ? Sel : SelectOf<Sel>;
|
|
530
|
+
/**
|
|
531
|
+
* Default model-class constraint used when a `ModelStatic` is referenced
|
|
532
|
+
* without its concrete constructor type (e.g. `ModelStatic<UserModel>`).
|
|
533
|
+
*
|
|
534
|
+
* In that case key/option types fall back to permissive shapes so existing
|
|
535
|
+
* usages keep compiling, while the precise inference is available when the
|
|
536
|
+
* constructor type is supplied (as it is from `DDBModel.asModel()`).
|
|
537
|
+
*/
|
|
538
|
+
type AnyModelClass = abstract new (...args: any[]) => DDBModel;
|
|
539
|
+
/**
|
|
540
|
+
* The accepted PK / GSI key type for `list()` / `explain()`.
|
|
541
|
+
*
|
|
542
|
+
* `list`/`explain` issue a DynamoDB *Query* against a partition, so a **partial**
|
|
543
|
+
* key (partition-key subset of a PK/GSI input) is accepted — the sort-key tail
|
|
544
|
+
* is optional. This matches the runtime key resolver. `query()` stays strict via
|
|
545
|
+
* {@link QueryKey}. Derived from the model class when available, otherwise
|
|
546
|
+
* permissive.
|
|
547
|
+
*/
|
|
548
|
+
type ListKey<C> = [C] extends [AnyModelClass] ? PartialQueryKeyOf<C> : Record<string, unknown>;
|
|
549
|
+
/**
|
|
550
|
+
* The accepted key type for `query()` — only keys guaranteed to return a
|
|
551
|
+
* single item (PK + unique GSIs). Derived from the model class when available.
|
|
552
|
+
*/
|
|
553
|
+
type QueryKey<C> = [C] extends [AnyModelClass] ? UniqueQueryKeyOf<C> : Record<string, unknown>;
|
|
554
|
+
/**
|
|
555
|
+
* The accepted key type for `delete()` (PK only). Derived from the model
|
|
556
|
+
* class when available.
|
|
557
|
+
*/
|
|
558
|
+
type DeleteKey<C> = [C] extends [AnyModelClass] ? PrimaryKeyOf<C> : Record<string, unknown>;
|
|
559
|
+
/**
|
|
560
|
+
* The accepted *explicit* key type for `update()` — the base-table primary key
|
|
561
|
+
* only. DynamoDB `UpdateItem` can target an item solely by its base-table PK
|
|
562
|
+
* (GSI-based updates are impossible), so this is `PrimaryKeyOf<C>`, not
|
|
563
|
+
* `UniqueQueryKeyOf<C>`. Derived from the model class when available.
|
|
564
|
+
*/
|
|
565
|
+
type UpdateExplicitKey<C> = [C] extends [AnyModelClass] ? PrimaryKeyOf<C> : Record<string, unknown>;
|
|
566
|
+
/**
|
|
567
|
+
* Options accepted by `list()` (third argument). The projection is supplied as
|
|
568
|
+
* the separate second `select` argument, symmetric with `query()`.
|
|
569
|
+
*
|
|
570
|
+
* - `filter` is the declarative server-side DynamoDB FilterExpression, typed
|
|
571
|
+
* against the full Entity `T` (may reference unprojected attributes). For
|
|
572
|
+
* arbitrary JS-only post-load filtering, narrow with `filter` then apply
|
|
573
|
+
* `result.items.filter(...)` (already fully typed).
|
|
574
|
+
*
|
|
575
|
+
* @typeParam T - The Entity type.
|
|
576
|
+
*/
|
|
577
|
+
type ListCallOptions<T> = {
|
|
578
|
+
limit?: number;
|
|
579
|
+
after?: string;
|
|
580
|
+
order?: 'ASC' | 'DESC';
|
|
581
|
+
filter?: FilterInput<T>;
|
|
582
|
+
/**
|
|
583
|
+
* When `true`, each returned item is updatable: it carries its resolved
|
|
584
|
+
* base-table key as a hidden, non-enumerable property and is branded
|
|
585
|
+
* `& Updatable`, so it can be passed straight back to `updateItem()`.
|
|
586
|
+
*/
|
|
587
|
+
updatable?: boolean;
|
|
588
|
+
};
|
|
589
|
+
/**
|
|
590
|
+
* Public, type-inferring API surface produced by {@link DDBModel.asModel}.
|
|
591
|
+
*
|
|
592
|
+
* @typeParam T - The Entity type of the model.
|
|
593
|
+
* @typeParam C - The model class constructor type. Supplied automatically by
|
|
594
|
+
* `asModel()`; when omitted, key/option types widen to permissive shapes so
|
|
595
|
+
* that `ModelStatic<T>` keeps compiling for callers that only name the entity.
|
|
596
|
+
*/
|
|
597
|
+
interface ModelStatic<T extends DDBModel, C = unknown> {
|
|
598
|
+
/**
|
|
599
|
+
* Type-safe column references for this model, one per scalar field. Used by
|
|
600
|
+
* the `cond` raw escape hatch (`cond\`${Model.col.age} > ${18}\``). Each
|
|
601
|
+
* column carries its declared type and is branded to this model, so passing
|
|
602
|
+
* another model's column into this model's `cond` is a compile error.
|
|
603
|
+
*/
|
|
604
|
+
readonly col: ColumnMap<T>;
|
|
605
|
+
/**
|
|
606
|
+
* Begin a model-bound, top-level select builder. The entity type `T` is fixed
|
|
607
|
+
* by this model and the projection `S` is inferred from the argument.
|
|
608
|
+
* `.filter(...)` is typed `FilterInput<T>` against the full entity.
|
|
609
|
+
*
|
|
610
|
+
* @example
|
|
611
|
+
* ```ts
|
|
612
|
+
* Order.list({ userId: 'u' },
|
|
613
|
+
* Order.project({ orderId: true, amount: true })
|
|
614
|
+
* .filter({ status: 'confirmed', amount: { gt: 100 } }),
|
|
615
|
+
* );
|
|
616
|
+
* ```
|
|
617
|
+
*/
|
|
618
|
+
project<const S extends SelectableOf<T>>(select: S): SelectBuilder<T, S>;
|
|
619
|
+
/**
|
|
620
|
+
* Begin a model-bound relation select builder for use as a relation field's
|
|
621
|
+
* value inside a parent `select`. `T` is this (relation target) model's
|
|
622
|
+
* entity type; `S` is inferred from the argument. Same model-bound inference
|
|
623
|
+
* as {@link ModelStatic.project}, plus per-relation pagination
|
|
624
|
+
* (`limit` / `after` / `order`).
|
|
625
|
+
*
|
|
626
|
+
* @example
|
|
627
|
+
* ```ts
|
|
628
|
+
* User.query({ userId: 'u' }, {
|
|
629
|
+
* name: true,
|
|
630
|
+
* orders: Order.relation({ orderId: true, amount: true })
|
|
631
|
+
* .filter({ amount: { gt: 100 } }),
|
|
632
|
+
* });
|
|
633
|
+
* ```
|
|
634
|
+
*/
|
|
635
|
+
relation<const S extends SelectableOf<T>>(select: S): RelationBuilder<T, S>;
|
|
636
|
+
/**
|
|
637
|
+
* Fetch a single item by a unique key and **hydrate** the plain result onto a
|
|
638
|
+
* host-side object (issue #53). The `hydrate` factory receives the fully-built
|
|
639
|
+
* plain object — `QueryResult<T, ProjectionOf<T, Sel>>`, typed against exactly
|
|
640
|
+
* the `select`ed fields — and its return type `R` becomes the read's return
|
|
641
|
+
* type (subsuming a `queryAs(Ctor, …)` style: `hydrate: (raw) => new Ctor(raw)`).
|
|
642
|
+
*
|
|
643
|
+
* Applied as the **final after-fetch step**, only to a present item: a missing
|
|
644
|
+
* item resolves to `null` and the factory is NOT invoked (hence `R | null`). A
|
|
645
|
+
* throw from the factory propagates verbatim as the read's rejection.
|
|
646
|
+
*
|
|
647
|
+
* Host-runtime ONLY: the factory is a host-language closure, never serialized
|
|
648
|
+
* into the SSoT / `operations.json`, so it does not impede the bridge (#48).
|
|
649
|
+
*/
|
|
650
|
+
query<Sel extends SelectSpec<T>, R>(key: QueryKey<C>, select: Sel & StrictSelectSpec<T, Sel>, options: {
|
|
651
|
+
consistentRead?: boolean;
|
|
652
|
+
maxDepth?: number;
|
|
653
|
+
updatable?: boolean;
|
|
654
|
+
hydrate: (raw: QueryResult<T, ProjectionOf<T, Sel>>) => R;
|
|
655
|
+
}): Promise<R | null>;
|
|
656
|
+
/**
|
|
657
|
+
* Fetch a single item by a unique key, requesting an **updatable** result.
|
|
658
|
+
* The returned item (when non-null) carries its resolved base-table key as a
|
|
659
|
+
* hidden, non-enumerable property and is branded `& Updatable`, so it can be
|
|
660
|
+
* passed straight back to {@link ModelStatic.updateItem} — even for a partial
|
|
661
|
+
* `select` or a GSI-based query. Do not spread / clone / JSON round-trip the
|
|
662
|
+
* result before updating: that drops both the hidden key and the brand.
|
|
663
|
+
*/
|
|
664
|
+
query<Sel extends SelectSpec<T>>(key: QueryKey<C>, select: Sel & StrictSelectSpec<T, Sel>, options: {
|
|
665
|
+
consistentRead?: boolean;
|
|
666
|
+
maxDepth?: number;
|
|
667
|
+
updatable: true;
|
|
668
|
+
}): Promise<(QueryResult<T, ProjectionOf<T, Sel>> & Updatable) | null>;
|
|
669
|
+
/**
|
|
670
|
+
* Fetch a single item by a unique key. The return type contains only the
|
|
671
|
+
* selected fields, derived from `select`.
|
|
672
|
+
*/
|
|
673
|
+
query<Sel extends SelectSpec<T>>(key: QueryKey<C>, select: Sel & StrictSelectSpec<T, Sel>, options?: {
|
|
674
|
+
consistentRead?: boolean;
|
|
675
|
+
maxDepth?: number;
|
|
676
|
+
updatable?: false;
|
|
677
|
+
}): Promise<QueryResult<T, ProjectionOf<T, Sel>> | null>;
|
|
678
|
+
/**
|
|
679
|
+
* List items for a partition, symmetric with {@link ModelStatic.query}:
|
|
680
|
+
* `list(key, select, options?)`. `select` (the second argument) may be a plain
|
|
681
|
+
* projection or a `project(...)` builder. `options.filter` is the declarative
|
|
682
|
+
* server-side FilterExpression typed against the full Entity. Returned items
|
|
683
|
+
* contain only the selected fields.
|
|
684
|
+
*/
|
|
685
|
+
list<Sel extends SelectSpec<T>>(key: ListKey<C>, select: Sel & StrictSelectSpec<T, Sel>, options: ListCallOptions<T> & {
|
|
686
|
+
updatable: true;
|
|
687
|
+
}): Promise<{
|
|
688
|
+
items: (QueryResult<T, ProjectionOf<T, Sel>> & Updatable)[];
|
|
689
|
+
cursor: string | null;
|
|
690
|
+
}>;
|
|
691
|
+
list<Sel extends SelectSpec<T>>(key: ListKey<C>, select: Sel & StrictSelectSpec<T, Sel>, options?: ListCallOptions<T>): Promise<{
|
|
692
|
+
items: QueryResult<T, ProjectionOf<T, Sel>>[];
|
|
693
|
+
cursor: string | null;
|
|
694
|
+
}>;
|
|
695
|
+
explain<Sel extends SelectSpec<T>>(key: ListKey<C>, options?: {
|
|
696
|
+
select?: Sel & StrictSelectSpec<T, Sel>;
|
|
697
|
+
limit?: number;
|
|
698
|
+
after?: string;
|
|
699
|
+
order?: 'ASC' | 'DESC';
|
|
700
|
+
consistentRead?: boolean;
|
|
701
|
+
filter?: FilterInput<T>;
|
|
702
|
+
}): ExecutionPlan;
|
|
703
|
+
putItem(item: EntityInput<T>, options?: Record<string, unknown>): Promise<void>;
|
|
704
|
+
/**
|
|
705
|
+
* Update an item identified by its **explicit base-table primary key**
|
|
706
|
+
* (`PrimaryKeyOf<C>`). This is the canonical form — it requires the key input
|
|
707
|
+
* up front, so it can never silently target the wrong item.
|
|
708
|
+
*
|
|
709
|
+
* @example `await User.updateItem({ userId: 'alice' }, { status: 'disabled' });`
|
|
710
|
+
*/
|
|
711
|
+
updateItem(key: UpdateExplicitKey<C>, changes: Partial<T>, options?: Record<string, unknown>): Promise<void>;
|
|
712
|
+
/**
|
|
713
|
+
* Update an item using an **updatable** entity obtained from
|
|
714
|
+
* `query(..., { updatable: true })` / `list(..., { updatable: true })`. The
|
|
715
|
+
* hidden base-table key carried by that entity is used directly, so this
|
|
716
|
+
* works even for a partial `select` or a GSI-based read.
|
|
717
|
+
*
|
|
718
|
+
* Pass the entity as-is. A spread / clone / JSON round-trip drops both the
|
|
719
|
+
* brand (rejected here at compile time) and the hidden key (rejected at
|
|
720
|
+
* runtime), preventing an accidental wrong-item update.
|
|
721
|
+
*/
|
|
722
|
+
updateItem(entity: Partial<T> & Updatable, changes: Partial<T>, options?: Record<string, unknown>): Promise<void>;
|
|
723
|
+
deleteItem(key: DeleteKey<C>, options?: Record<string, unknown>): Promise<void>;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
interface PutOptions {
|
|
727
|
+
condition?: Record<string, unknown>;
|
|
728
|
+
}
|
|
729
|
+
interface UpdateOptions {
|
|
730
|
+
condition?: Record<string, unknown>;
|
|
731
|
+
}
|
|
732
|
+
interface DeleteOptions {
|
|
733
|
+
condition?: Record<string, unknown>;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
interface PutInput {
|
|
737
|
+
TableName: string;
|
|
738
|
+
Item: Record<string, unknown>;
|
|
739
|
+
ConditionExpression?: string;
|
|
740
|
+
ExpressionAttributeNames?: Record<string, string>;
|
|
741
|
+
ExpressionAttributeValues?: Record<string, unknown>;
|
|
742
|
+
}
|
|
743
|
+
declare function buildPutInput(modelClass: Function, item: Record<string, unknown>, options?: PutOptions): PutInput;
|
|
744
|
+
declare function executePut(modelClass: Function, item: Record<string, unknown>, options?: PutOptions): Promise<void>;
|
|
745
|
+
|
|
746
|
+
interface UpdateInput {
|
|
747
|
+
TableName: string;
|
|
748
|
+
Key: Record<string, unknown>;
|
|
749
|
+
UpdateExpression: string;
|
|
750
|
+
ExpressionAttributeNames: Record<string, string>;
|
|
751
|
+
ExpressionAttributeValues?: Record<string, unknown>;
|
|
752
|
+
ConditionExpression?: string;
|
|
753
|
+
}
|
|
754
|
+
declare function buildUpdateInput(modelClass: Function, entity: Record<string, unknown>, changes: Record<string, unknown>, options?: UpdateOptions): UpdateInput;
|
|
755
|
+
declare function executeUpdate(modelClass: Function, entity: Record<string, unknown>, changes: Record<string, unknown>, options?: UpdateOptions): Promise<void>;
|
|
756
|
+
|
|
757
|
+
interface DeleteInput {
|
|
758
|
+
TableName: string;
|
|
759
|
+
Key: Record<string, unknown>;
|
|
760
|
+
ConditionExpression?: string;
|
|
761
|
+
ExpressionAttributeNames?: Record<string, string>;
|
|
762
|
+
ExpressionAttributeValues?: Record<string, unknown>;
|
|
763
|
+
}
|
|
764
|
+
declare function buildDeleteInput(modelClass: Function, keyObj: Record<string, unknown>, options?: DeleteOptions): DeleteInput;
|
|
765
|
+
declare function executeDelete(modelClass: Function, keyObj: Record<string, unknown>, options?: DeleteOptions): Promise<void>;
|
|
766
|
+
|
|
767
|
+
/** Result of a read operation (GetItem / Query / BatchGetItem). */
|
|
768
|
+
interface ExecutorResult {
|
|
769
|
+
items: Record<string, unknown>[];
|
|
770
|
+
lastEvaluatedKey?: Record<string, unknown>;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Result of a single structured write. `oldItem` is the pre-write image,
|
|
774
|
+
* present only when the caller requested it (`returnOldImage: true`, mapped to
|
|
775
|
+
* DynamoDB `ReturnValues: ALL_OLD`) and an item existed before the write. The
|
|
776
|
+
* write-capture seam (issue #72) reads it to derive INSERT vs MODIFY and the
|
|
777
|
+
* REMOVE OldImage.
|
|
778
|
+
*/
|
|
779
|
+
interface WriteResult {
|
|
780
|
+
oldItem?: Record<string, unknown>;
|
|
781
|
+
}
|
|
782
|
+
/** Options shared by the single-item structured write methods. */
|
|
783
|
+
interface WriteExecOptions {
|
|
784
|
+
/** Request the pre-write image (DynamoDB `ReturnValues: ALL_OLD`). */
|
|
785
|
+
returnOldImage?: boolean;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* One BatchGetItem sub-request: a single physical table plus its keys and an
|
|
789
|
+
* optional projection. Mirrors {@link import('../planner/types.js').BatchGetItemOperation}
|
|
790
|
+
* without the discriminant `type` field, so callers that already chunked keys
|
|
791
|
+
* (relation traversal) and the planner-driven read path share one entry point.
|
|
792
|
+
*/
|
|
793
|
+
interface BatchGetExecInput {
|
|
794
|
+
tableName: string;
|
|
795
|
+
keys: Record<string, unknown>[];
|
|
796
|
+
projectionExpression?: string;
|
|
797
|
+
expressionAttributeNames?: Record<string, string>;
|
|
798
|
+
consistentRead?: boolean;
|
|
799
|
+
}
|
|
800
|
+
/** A single item in a {@link Executor.batchWrite} request, grouped by table. */
|
|
801
|
+
type BatchWriteExecItem = {
|
|
802
|
+
type: 'put';
|
|
803
|
+
item: Record<string, unknown>;
|
|
804
|
+
} | {
|
|
805
|
+
type: 'delete';
|
|
806
|
+
key: Record<string, unknown>;
|
|
807
|
+
};
|
|
808
|
+
/**
|
|
809
|
+
* A read-only `ConditionCheck` entry for {@link Executor.transactWrite} (issue
|
|
810
|
+
* #81): it asserts a `ConditionExpression` holds for the keyed item without
|
|
811
|
+
* mutating it. A failed assertion cancels the **whole** `TransactWriteItems`
|
|
812
|
+
* atomically — the foundation for referential-integrity derivation.
|
|
813
|
+
*/
|
|
814
|
+
interface ConditionCheckInput {
|
|
815
|
+
TableName: string;
|
|
816
|
+
Key: Record<string, unknown>;
|
|
817
|
+
ConditionExpression: string;
|
|
818
|
+
ExpressionAttributeNames?: Record<string, string>;
|
|
819
|
+
ExpressionAttributeValues?: Record<string, unknown>;
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* A `{ Put | Update | Delete | ConditionCheck }` entry for
|
|
823
|
+
* {@link Executor.transactWrite}. The first three mutate an item; `ConditionCheck`
|
|
824
|
+
* (issue #81) is a read-only assertion on another item.
|
|
825
|
+
*/
|
|
826
|
+
type TransactWriteExecItem = {
|
|
827
|
+
Put: PutInput;
|
|
828
|
+
} | {
|
|
829
|
+
Update: UpdateInput;
|
|
830
|
+
} | {
|
|
831
|
+
Delete: DeleteInput;
|
|
832
|
+
} | {
|
|
833
|
+
ConditionCheck: ConditionCheckInput;
|
|
834
|
+
};
|
|
835
|
+
/**
|
|
836
|
+
* The unified I/O seam (issue #76). All of GraphDDB's read and write I/O is
|
|
837
|
+
* funneled through one injectable executor so the engine (planner / hydrator /
|
|
838
|
+
* relation traversal / write paths) can run unchanged against either real
|
|
839
|
+
* DynamoDB ({@link DynamoExecutor}, the default) or an in-process memory store
|
|
840
|
+
* (`MemoryExecutor`, the test adapter).
|
|
841
|
+
*
|
|
842
|
+
* Reads take a structured {@link DynamoDBOperation}; writes take the structured
|
|
843
|
+
* inputs the operation layer already builds (`PutInput` / `UpdateInput` / …),
|
|
844
|
+
* never an AWS SDK command. This keeps the seam at the *structured operation
|
|
845
|
+
* boundary* (proposal option B): no expression-string parsing is required to
|
|
846
|
+
* implement a fake.
|
|
847
|
+
*/
|
|
848
|
+
interface Executor {
|
|
849
|
+
/** Read path: GetItem / Query / BatchGetItem. */
|
|
850
|
+
execute(operation: DynamoDBOperation): Promise<ExecutorResult>;
|
|
851
|
+
/**
|
|
852
|
+
* BatchGetItem for a single table. The caller is responsible for chunking
|
|
853
|
+
* keys ≤100 (the relation traversal and the planner-driven path both do).
|
|
854
|
+
*/
|
|
855
|
+
batchGet(input: BatchGetExecInput): Promise<ExecutorResult>;
|
|
856
|
+
/** Put one item. */
|
|
857
|
+
put(input: PutInput, options?: WriteExecOptions): Promise<WriteResult>;
|
|
858
|
+
/** Update one item (applies its structured UpdateExpression). */
|
|
859
|
+
update(input: UpdateInput, options?: WriteExecOptions): Promise<WriteResult>;
|
|
860
|
+
/** Delete one item. */
|
|
861
|
+
delete(input: DeleteInput, options?: WriteExecOptions): Promise<WriteResult>;
|
|
862
|
+
/**
|
|
863
|
+
* BatchWriteItem for a single table (non-atomic). Keys/items are chunked ≤25
|
|
864
|
+
* with UnprocessedItems retry by the implementation.
|
|
865
|
+
*/
|
|
866
|
+
batchWrite(tableName: string, items: BatchWriteExecItem[]): Promise<void>;
|
|
867
|
+
/** TransactWriteItems — all-or-nothing atomic. */
|
|
868
|
+
transactWrite(items: TransactWriteExecItem[]): Promise<void>;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
type TransactWriteItemInput = {
|
|
872
|
+
Put: PutInput;
|
|
873
|
+
} | {
|
|
874
|
+
Update: UpdateInput;
|
|
875
|
+
} | {
|
|
876
|
+
Delete: DeleteInput;
|
|
877
|
+
} | {
|
|
878
|
+
ConditionCheck: ConditionCheckInput;
|
|
879
|
+
};
|
|
880
|
+
declare function attachModelClass<T extends DDBModel>(modelStatic: ModelStatic<T>, modelClass: new (...args: unknown[]) => T): ModelStatic<T>;
|
|
881
|
+
/**
|
|
882
|
+
* Per-item capture descriptor recorded alongside each transact item, so the
|
|
883
|
+
* write-capture seam (issue #72) can emit a labelled raw record after the
|
|
884
|
+
* transaction commits. `TransactWriteItems` returns no images (spec §14), so
|
|
885
|
+
* these carry the keys + the new item for Put/Update and keys only for Delete.
|
|
886
|
+
*/
|
|
887
|
+
interface TransactCaptureMeta {
|
|
888
|
+
modelName?: string;
|
|
889
|
+
op: 'put' | 'update' | 'delete';
|
|
890
|
+
table: string;
|
|
891
|
+
keys: {
|
|
892
|
+
pk: string;
|
|
893
|
+
sk: string;
|
|
894
|
+
};
|
|
895
|
+
newItem?: Record<string, unknown>;
|
|
896
|
+
}
|
|
897
|
+
declare class TransactionContext {
|
|
898
|
+
private readonly items;
|
|
899
|
+
private readonly captureMeta;
|
|
900
|
+
get itemCount(): number;
|
|
901
|
+
put(model: ModelStatic<DDBModel>, item: Record<string, unknown>, options?: PutOptions): void;
|
|
902
|
+
update(model: ModelStatic<DDBModel>, entity: Record<string, unknown>, changes: Record<string, unknown>, options?: UpdateOptions): void;
|
|
903
|
+
delete(model: ModelStatic<DDBModel>, key: Record<string, unknown>, options?: DeleteOptions): void;
|
|
904
|
+
/**
|
|
905
|
+
* Add a read-only `ConditionCheck` assertion on a keyed item (issue #81). The
|
|
906
|
+
* item is **not** mutated; the `options.condition` (e.g.
|
|
907
|
+
* `{ attributeExists: 'PK' }` to require the row exists) is asserted, and a
|
|
908
|
+
* failed assertion cancels the **whole** `TransactWriteItems` atomically. This
|
|
909
|
+
* is the foundation for referential-integrity derivation (proposal: `requires
|
|
910
|
+
* <Entity> exists`). A ConditionCheck emits no change-capture record (it writes
|
|
911
|
+
* nothing).
|
|
912
|
+
*
|
|
913
|
+
* @throws if `options.condition` is missing — a ConditionCheck without an
|
|
914
|
+
* assertion is meaningless.
|
|
915
|
+
*/
|
|
916
|
+
conditionCheck(model: ModelStatic<DDBModel>, key: Record<string, unknown>, options: {
|
|
917
|
+
condition: Record<string, unknown>;
|
|
918
|
+
}): void;
|
|
919
|
+
/** @internal */
|
|
920
|
+
getTransactItems(): TransactWriteItemInput[];
|
|
921
|
+
/** @internal — capture descriptors for the write-capture seam (issue #72). */
|
|
922
|
+
getCaptureMeta(): TransactCaptureMeta[];
|
|
923
|
+
private assertWithinLimit;
|
|
924
|
+
}
|
|
925
|
+
declare function executeTransaction(fn: (tx: TransactionContext) => void | Promise<void>): Promise<void>;
|
|
926
|
+
|
|
927
|
+
interface BatchGetRequest {
|
|
928
|
+
model: ModelStatic<DDBModel>;
|
|
929
|
+
keys: Record<string, unknown>[];
|
|
930
|
+
}
|
|
931
|
+
interface BatchPutRequest {
|
|
932
|
+
type: 'put';
|
|
933
|
+
model: ModelStatic<DDBModel>;
|
|
934
|
+
item: Record<string, unknown>;
|
|
935
|
+
options?: PutOptions;
|
|
936
|
+
}
|
|
937
|
+
interface BatchDeleteRequest {
|
|
938
|
+
type: 'delete';
|
|
939
|
+
model: ModelStatic<DDBModel>;
|
|
940
|
+
key: Record<string, unknown>;
|
|
941
|
+
options?: DeleteOptions;
|
|
942
|
+
}
|
|
943
|
+
type BatchWriteRequest = BatchPutRequest | BatchDeleteRequest;
|
|
944
|
+
declare class BatchGetResult {
|
|
945
|
+
private readonly groups;
|
|
946
|
+
constructor(groups: Map<Function, Record<string, unknown>[]>);
|
|
947
|
+
get<T extends DDBModel>(model: ModelStatic<T>): Partial<T>[];
|
|
948
|
+
}
|
|
949
|
+
declare function executeBatchGet(requests: BatchGetRequest[]): Promise<BatchGetResult>;
|
|
950
|
+
declare function executeBatchWrite(requests: BatchWriteRequest[]): Promise<void>;
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Parameter placeholders for the parameterized query / command definition DSL
|
|
954
|
+
* (issue #41, Python-bridge Phase 0a).
|
|
955
|
+
*
|
|
956
|
+
* A `Param<T>` is a **branded placeholder** standing in for a value that is not
|
|
957
|
+
* known at definition time but is supplied later (at execution, e.g. from
|
|
958
|
+
* Python). It carries:
|
|
959
|
+
*
|
|
960
|
+
* - the **TypeScript value type** `T` it represents (`string`, `number`, or a
|
|
961
|
+
* string-literal union), preserved through the IR and the inferred return
|
|
962
|
+
* types of `defineQueries` / `defineCommands`; and
|
|
963
|
+
* - a **runtime descriptor** (`kind` + optional `literals`) that the static
|
|
964
|
+
* planner (#42) and the code generator read to emit `operations.json`.
|
|
965
|
+
*
|
|
966
|
+
* Placeholders are created with `param.string()`, `param.number()`, and
|
|
967
|
+
* `param.literal(...)`. They are *only* legal at value positions inside a
|
|
968
|
+
* parameterized key / changes structure passed to the `define*` entry points —
|
|
969
|
+
* never to the live `Model.query` / `Model.putItem` runtime API, whose types remain
|
|
970
|
+
* uncontaminated by params.
|
|
971
|
+
*/
|
|
972
|
+
/** Brand tag preventing a plain object from structurally matching a {@link Param}. */
|
|
973
|
+
declare const PARAM_BRAND: unique symbol;
|
|
974
|
+
/** Runtime discriminant for a parameter placeholder. */
|
|
975
|
+
type ParamKind = 'string' | 'number' | 'literal' | 'array';
|
|
976
|
+
/**
|
|
977
|
+
* A branded parameter placeholder representing a value of TypeScript type `T`
|
|
978
|
+
* that is bound at execution time rather than definition time.
|
|
979
|
+
*
|
|
980
|
+
* The brand (`[PARAM_BRAND]: T`) keeps `Param<string>` and `Param<number>`
|
|
981
|
+
* distinct and prevents a bare value from being mistaken for a placeholder.
|
|
982
|
+
*
|
|
983
|
+
* @typeParam T - The value type this placeholder stands in for.
|
|
984
|
+
*/
|
|
985
|
+
interface Param<out T> {
|
|
986
|
+
/** @internal Type brand carrying the represented value type. */
|
|
987
|
+
readonly [PARAM_BRAND]: T;
|
|
988
|
+
/** Runtime discriminant: `'string'` | `'number'` | `'literal'` | `'array'`. */
|
|
989
|
+
readonly kind: ParamKind;
|
|
990
|
+
/**
|
|
991
|
+
* For `param.literal(...)`, the allowed literal values (preserved at runtime
|
|
992
|
+
* for the generator). `undefined` for `string` / `number`.
|
|
993
|
+
*/
|
|
994
|
+
readonly literals?: readonly T[];
|
|
995
|
+
/**
|
|
996
|
+
* For `param.array({...})`, the descriptor of each element's fields. Lets the
|
|
997
|
+
* transaction planner (#46) type `forEach` element references and emit the
|
|
998
|
+
* `{item.<field>}` templates. `undefined` for scalar params.
|
|
999
|
+
*/
|
|
1000
|
+
readonly element?: Readonly<Record<string, Param<unknown>>>;
|
|
1001
|
+
}
|
|
1002
|
+
/** A `Param` whose represented value type is `string`. */
|
|
1003
|
+
type StringParam = Param<string>;
|
|
1004
|
+
/** A `Param` whose represented value type is `number`. */
|
|
1005
|
+
type NumberParam = Param<number>;
|
|
1006
|
+
/** A `Param` whose represented value type is the literal union `L`. */
|
|
1007
|
+
type LiteralParam<L extends string | number> = Param<L>;
|
|
1008
|
+
/** The element-field descriptor shape accepted by {@link param.array}. */
|
|
1009
|
+
type ArrayElementShape = Record<string, Param<unknown>>;
|
|
1010
|
+
/**
|
|
1011
|
+
* The TypeScript element type implied by an {@link ArrayElementShape} `E`: each
|
|
1012
|
+
* field carries the value type its placeholder represents.
|
|
1013
|
+
*/
|
|
1014
|
+
type ElementOf<E extends ArrayElementShape> = {
|
|
1015
|
+
[K in keyof E]: E[K] extends Param<infer V> ? V : never;
|
|
1016
|
+
};
|
|
1017
|
+
/** A `Param` standing in for an **array** whose elements have shape `E`. */
|
|
1018
|
+
type ArrayParam<E extends ArrayElementShape> = Param<ElementOf<E>[]> & {
|
|
1019
|
+
readonly element: E;
|
|
1020
|
+
};
|
|
1021
|
+
/** Allowed scalar value types a placeholder may stand in for. */
|
|
1022
|
+
type ParamValue = string | number;
|
|
1023
|
+
/**
|
|
1024
|
+
* Placeholder factory. Each call returns a branded {@link Param} carrying both
|
|
1025
|
+
* the TypeScript value type and a runtime descriptor.
|
|
1026
|
+
*/
|
|
1027
|
+
declare const param: {
|
|
1028
|
+
/** A placeholder for a `string` value. */
|
|
1029
|
+
readonly string: () => StringParam;
|
|
1030
|
+
/** A placeholder for a `number` value. */
|
|
1031
|
+
readonly number: () => NumberParam;
|
|
1032
|
+
/**
|
|
1033
|
+
* A placeholder constrained to one of the given literal values. The value
|
|
1034
|
+
* type of the resulting {@link Param} is the **union of the literals**, so it
|
|
1035
|
+
* is preserved precisely in the IR and the inferred definition types.
|
|
1036
|
+
*
|
|
1037
|
+
* @example
|
|
1038
|
+
* ```ts
|
|
1039
|
+
* param.literal('active', 'disabled'); // Param<'active' | 'disabled'>
|
|
1040
|
+
* ```
|
|
1041
|
+
*/
|
|
1042
|
+
readonly literal: <const L extends readonly [ParamValue, ...ParamValue[]]>(...values: L) => LiteralParam<L[number]>;
|
|
1043
|
+
/**
|
|
1044
|
+
* A placeholder for an **array** parameter whose elements have the given field
|
|
1045
|
+
* shape. Used by `defineTransaction`'s `tx.forEach(p.<arrayParam>, …)` to bind
|
|
1046
|
+
* each element's fields to `{item.<field>}` templates.
|
|
1047
|
+
*
|
|
1048
|
+
* @example
|
|
1049
|
+
* ```ts
|
|
1050
|
+
* param.array({ userId: param.string(), role: param.string() });
|
|
1051
|
+
* // Param<{ userId: string; role: string }[]>
|
|
1052
|
+
* ```
|
|
1053
|
+
*/
|
|
1054
|
+
readonly array: <const E extends ArrayElementShape>(element: E) => ArrayParam<E>;
|
|
1055
|
+
};
|
|
1056
|
+
/** Runtime type guard: is `value` a parameter placeholder? */
|
|
1057
|
+
declare function isParam(value: unknown): value is Param<unknown>;
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Intermediate representation (IR) produced by the parameterized definition DSL
|
|
1061
|
+
* (issue #41). This is the **in-memory, typed** representation consumed by the
|
|
1062
|
+
* static planner (#42); JSON serialization is #42's responsibility, so nothing
|
|
1063
|
+
* here is serialized — the IR keeps full type information (param value types,
|
|
1064
|
+
* select projection) so downstream type derivation stays sound.
|
|
1065
|
+
*
|
|
1066
|
+
* Each `define*` entry point captures, for one definition:
|
|
1067
|
+
*
|
|
1068
|
+
* - `entity` — the model name + (runtime) class the operation targets;
|
|
1069
|
+
* - `operation`— the operation kind (`query` | `list` | `put` | `update`
|
|
1070
|
+
* | `delete`);
|
|
1071
|
+
* - the operation **structure** with {@link Param} placeholders left in place
|
|
1072
|
+
* (`key` / `item` / `changes`, plus `select` for reads); and
|
|
1073
|
+
* - `params` — a `name → descriptor` map collected from every placeholder in
|
|
1074
|
+
* the structure, with the descriptor preserving the param's value type.
|
|
1075
|
+
*/
|
|
1076
|
+
|
|
1077
|
+
/** The set of operation kinds a definition may describe. */
|
|
1078
|
+
type OperationKind = 'query' | 'list' | 'put' | 'update' | 'delete';
|
|
1079
|
+
/**
|
|
1080
|
+
* A declarative write condition for a parameterized command definition (issues
|
|
1081
|
+
* #46, #81). Mirrors the runtime `condition` subset
|
|
1082
|
+
* (`expression/condition-expression.ts`):
|
|
1083
|
+
*
|
|
1084
|
+
* - `{ notExists: true }` → `attribute_not_exists(PK)` (the row must not exist).
|
|
1085
|
+
* This legacy whole-row form is kept verbatim for backward compatibility.
|
|
1086
|
+
* - `{ attributeExists: '<field>' }` → `attribute_exists(<field>)` — the named
|
|
1087
|
+
* attribute (any field, including PK / SK) must be present. The foundation for
|
|
1088
|
+
* referential-integrity derivation (proposal "capability gaps": `requires X
|
|
1089
|
+
* exists`).
|
|
1090
|
+
* - `{ attributeNotExists: '<field>' }` → `attribute_not_exists(<field>)` — the
|
|
1091
|
+
* named attribute must be absent (a field-level uniqueness / first-write guard).
|
|
1092
|
+
* - `{ field: value, … }` → field equality (`#f = :v AND …`). Each value may be
|
|
1093
|
+
* a concrete literal **or** a {@link Param} placeholder (bound at execution).
|
|
1094
|
+
*
|
|
1095
|
+
* The forms are mutually exclusive: `notExists` / `attributeExists` /
|
|
1096
|
+
* `attributeNotExists` are single-primitive conditions and ignore any sibling
|
|
1097
|
+
* fields. (When more than one would be present the most specific is taken in the
|
|
1098
|
+
* order `notExists` → `attributeExists` → `attributeNotExists`.)
|
|
1099
|
+
*/
|
|
1100
|
+
type ConditionInput = {
|
|
1101
|
+
readonly notExists: true;
|
|
1102
|
+
} | {
|
|
1103
|
+
readonly attributeExists: string;
|
|
1104
|
+
} | {
|
|
1105
|
+
readonly attributeNotExists: string;
|
|
1106
|
+
} | Readonly<Record<string, Param<unknown> | string | number | boolean>>;
|
|
1107
|
+
/** Options accepted by the write `define*` entry points (issue #46). */
|
|
1108
|
+
interface WriteDefinitionOptions {
|
|
1109
|
+
/** Optional declarative write condition (subset). */
|
|
1110
|
+
readonly condition?: ConditionInput;
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Identifies the entity an operation targets. `name` is the model class name
|
|
1114
|
+
* (entity name); `modelClass` is retained so the planner can resolve metadata
|
|
1115
|
+
* (keys / GSIs / fields) from the `MetadataRegistry`.
|
|
1116
|
+
*/
|
|
1117
|
+
interface EntityRef {
|
|
1118
|
+
readonly name: string;
|
|
1119
|
+
readonly modelClass: Function;
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* A single parameter descriptor in an operation's `params` map. Carries the
|
|
1123
|
+
* runtime `kind` (and, for `literal`, the allowed values) and — purely at the
|
|
1124
|
+
* type level — the represented value type `T`.
|
|
1125
|
+
*
|
|
1126
|
+
* @typeParam T - The value type the parameter stands in for.
|
|
1127
|
+
*/
|
|
1128
|
+
interface ParamDescriptor<T = unknown> {
|
|
1129
|
+
readonly kind: ParamKind;
|
|
1130
|
+
readonly literals?: readonly T[];
|
|
1131
|
+
/**
|
|
1132
|
+
* For an `array` param (transaction `forEach` source), the per-element field
|
|
1133
|
+
* descriptors. `undefined` for scalar params.
|
|
1134
|
+
*/
|
|
1135
|
+
readonly element?: Readonly<Record<string, ParamDescriptor>>;
|
|
1136
|
+
/** Parameters are always required (no optional params in this phase). */
|
|
1137
|
+
readonly required: true;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Structure with {@link Param} placeholders left intact (key / item / changes).
|
|
1141
|
+
* A recursive record whose leaves are either concrete scalar literals (e.g. a
|
|
1142
|
+
* fixed sort-key discriminator) or `Param<…>` placeholders.
|
|
1143
|
+
*/
|
|
1144
|
+
type ParamStructure = {
|
|
1145
|
+
readonly [key: string]: ParamStructure | Param<unknown> | string | number | boolean;
|
|
1146
|
+
};
|
|
1147
|
+
/**
|
|
1148
|
+
* The typed IR for one definition. Generic over the entity, the parameterized
|
|
1149
|
+
* structure(s), the (optional) select projection, and the collected params map
|
|
1150
|
+
* so that all type information survives into the value returned by
|
|
1151
|
+
* `defineQueries` / `defineCommands`.
|
|
1152
|
+
*
|
|
1153
|
+
* Field presence by `operation`:
|
|
1154
|
+
*
|
|
1155
|
+
* | op | key | select | changes | params |
|
|
1156
|
+
* |----------|-----|--------|---------|--------|
|
|
1157
|
+
* | query | ✓ | ✓ | — | ✓ |
|
|
1158
|
+
* | list | ✓ | ✓ | — | ✓ |
|
|
1159
|
+
* | put | item (in `key`) | — | — | ✓ |
|
|
1160
|
+
* | update | ✓ | — | ✓ | ✓ |
|
|
1161
|
+
* | delete | ✓ | — | — | ✓ |
|
|
1162
|
+
*
|
|
1163
|
+
* @typeParam T - The entity type.
|
|
1164
|
+
* @typeParam Op - The operation kind.
|
|
1165
|
+
* @typeParam K - The parameterized key / item structure.
|
|
1166
|
+
* @typeParam S - The select projection (reads only; `undefined` for writes).
|
|
1167
|
+
* @typeParam Ch - The parameterized changes structure (`update` only).
|
|
1168
|
+
* @typeParam P - The collected `name → ParamDescriptor` map.
|
|
1169
|
+
*/
|
|
1170
|
+
interface OperationDefinition<T extends DDBModel, Op extends OperationKind, K, S, Ch, P extends Record<string, ParamDescriptor>> {
|
|
1171
|
+
/** @internal Marks this object as a definition IR node. */
|
|
1172
|
+
readonly __isOperationDefinition: true;
|
|
1173
|
+
readonly entity: EntityRef;
|
|
1174
|
+
readonly operation: Op;
|
|
1175
|
+
/**
|
|
1176
|
+
* The parameterized key for `query` / `list` / `update` / `delete`, or the
|
|
1177
|
+
* parameterized item for `put`. Placeholders are preserved in place.
|
|
1178
|
+
*/
|
|
1179
|
+
readonly key: K;
|
|
1180
|
+
/** The select projection for `query` / `list`; `undefined` otherwise. */
|
|
1181
|
+
readonly select: S;
|
|
1182
|
+
/** The parameterized changes for `update`; `undefined` otherwise. */
|
|
1183
|
+
readonly changes: Ch;
|
|
1184
|
+
/**
|
|
1185
|
+
* The declarative write condition for `put` / `update` / `delete`; `undefined`
|
|
1186
|
+
* when no condition is attached or for reads (issue #46).
|
|
1187
|
+
*/
|
|
1188
|
+
readonly condition?: ConditionInput;
|
|
1189
|
+
/** Collected parameters: name → descriptor (value type preserved in `P`). */
|
|
1190
|
+
readonly params: P;
|
|
1191
|
+
/** @internal Phantom carrier so the entity type `T` is retained on the node. */
|
|
1192
|
+
readonly __entityType?: (value: T) => void;
|
|
1193
|
+
}
|
|
1194
|
+
/** Any operation definition node, regardless of its type parameters. */
|
|
1195
|
+
type AnyOperationDefinition = OperationDefinition<any, OperationKind, unknown, unknown, unknown, Record<string, ParamDescriptor>>;
|
|
1196
|
+
/**
|
|
1197
|
+
* A map of definition name → {@link OperationDefinition}, as accepted by
|
|
1198
|
+
* `defineQueries` / `defineCommands` and returned (unchanged in type) from them.
|
|
1199
|
+
*/
|
|
1200
|
+
type DefinitionMap = Record<string, AnyOperationDefinition>;
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Model write-semantics — the reusable *save contract* `entityWrites` (issue #83,
|
|
1204
|
+
* Epic #80; spec `docs/mutation-command-derivation.md` §2).
|
|
1205
|
+
*
|
|
1206
|
+
* `Model.writes` declares the **write-side invariants / effects required for this
|
|
1207
|
+
* entity to be consistent on DynamoDB** — a *reusable save contract*, NOT a
|
|
1208
|
+
* mutation and NOT business logic. It is a per-lifecycle map
|
|
1209
|
+
* (`{ create?, update?, remove? }`) whose values are {@link LifecycleContract}s
|
|
1210
|
+
* built with `w.lifecycle({...})`; each carries the §2 effect arrays
|
|
1211
|
+
* (`requires` / `unique` / `edges` / `derive` / `emits` / `idempotency`).
|
|
1212
|
+
*
|
|
1213
|
+
* ## What #83 does — and deliberately does NOT — with the effects
|
|
1214
|
+
*
|
|
1215
|
+
* The mutation compiler (#83) resolves a fragment's lifecycle from this save
|
|
1216
|
+
* contract (the fragment's intent — `m.create` / `m.update` / `m.remove` — picks
|
|
1217
|
+
* `create` / `update` / `remove`) and emits the **base write op** for that
|
|
1218
|
+
* lifecycle (create → `PutItem` with `attribute_not_exists(PK)`, update →
|
|
1219
|
+
* `UpdateItem`, remove → `DeleteItem`). The §2 effect arrays are **stored
|
|
1220
|
+
* opaquely** here and consumed by NONE of #83: deriving `ConditionCheck`
|
|
1221
|
+
* (requires, #84), uniqueness guards (#86), adjacency edge `Put` / `Delete` from
|
|
1222
|
+
* `edges` (#85), derived counter `UpdateItem`s (#85), outbox events (#87), and
|
|
1223
|
+
* the idempotency guard (#87) are each their own follow-up issue. #83 only shapes
|
|
1224
|
+
* the declaration and exposes the {@link LifecycleContract.effects} a later issue
|
|
1225
|
+
* reads — the **per-fragment hook** the compiler leaves empty (see
|
|
1226
|
+
* `compileFragment` in `src/spec/mutation-command.ts`).
|
|
1227
|
+
*
|
|
1228
|
+
* ## Coexistence with edge writes (`edgeWrites`, #82)
|
|
1229
|
+
*
|
|
1230
|
+
* This is a **distinct** construct from {@link edgeWrites} (#82, the adjacency
|
|
1231
|
+
* edge-only `writes` member). `edgeWrites` declares *only* the edge write side of
|
|
1232
|
+
* an adjacency entity; `entityWrites` is the full per-lifecycle save contract a
|
|
1233
|
+
* mutation fragment adopts. Both are recognized by their own brand, so a model
|
|
1234
|
+
* may carry either form on its static `writes` member without collision; the
|
|
1235
|
+
* mutation compiler reads {@link getEntityWrites} (this marker), the edge
|
|
1236
|
+
* derivation reads its own.
|
|
1237
|
+
*
|
|
1238
|
+
* ## Trivial base op when a model declares no `writes`
|
|
1239
|
+
*
|
|
1240
|
+
* A target model that declares **no** `entityWrites` save contract has no
|
|
1241
|
+
* lifecycle to resolve; the fragment then compiles the **trivial base op** for
|
|
1242
|
+
* its intent (create → `Put` `attribute_not_exists(PK)`, update → `Update`,
|
|
1243
|
+
* remove → `Delete`) directly against the plain model. So
|
|
1244
|
+
* `{ create: () => PostModel, key, input }` works on a bare model — see
|
|
1245
|
+
* {@link resolveLifecycle} in `src/spec/mutation-command.ts`.
|
|
1246
|
+
*/
|
|
1247
|
+
/**
|
|
1248
|
+
* The lifecycle phase a save contract entry declares. The mutation fragment's
|
|
1249
|
+
* intent (`m.create` / `m.update` / `m.remove`) selects the matching entry.
|
|
1250
|
+
*/
|
|
1251
|
+
type WriteLifecyclePhase = 'create' | 'update' | 'remove';
|
|
1252
|
+
/**
|
|
1253
|
+
* A path-rooted leaf value in a save-contract effect (proposal §2): every value
|
|
1254
|
+
* binds to an explicit path root so its source is unambiguous —
|
|
1255
|
+
* `$.input.*` (the mutation input) or `$.entity.*` (the written entity). #83
|
|
1256
|
+
* stores these verbatim (it consumes none); a follow-up issue (#84–#87) reads
|
|
1257
|
+
* them when deriving the effect's DynamoDB realization.
|
|
1258
|
+
*/
|
|
1259
|
+
type EffectPath = string;
|
|
1260
|
+
/**
|
|
1261
|
+
* A referential-integrity requirement (proposal §2 `requires`): the named target
|
|
1262
|
+
* entity must exist for the keys bound from `$.input.*` / `$.entity.*`. Derived
|
|
1263
|
+
* to a `ConditionCheck` (`attribute_exists`) by #84 — **not** by #83, which only
|
|
1264
|
+
* stores it.
|
|
1265
|
+
*/
|
|
1266
|
+
interface RequiresEffect {
|
|
1267
|
+
readonly kind: 'requires';
|
|
1268
|
+
/** Resolves the entity whose existence is asserted. */
|
|
1269
|
+
readonly targetFactory: () => new (...args: unknown[]) => unknown;
|
|
1270
|
+
/** The key binding: target key field → a path-rooted source value. */
|
|
1271
|
+
readonly keys: Readonly<Record<string, EffectPath>>;
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* A uniqueness guard (proposal §2 `unique`): `{ name, scope, fields }` — a named
|
|
1275
|
+
* composite-scope uniqueness invariant. Derived to a `UNIQUE#…` guard `Put` with
|
|
1276
|
+
* `attribute_not_exists` by #86 — stored opaquely here.
|
|
1277
|
+
*/
|
|
1278
|
+
interface UniqueEffect {
|
|
1279
|
+
readonly kind: 'unique';
|
|
1280
|
+
/** The guard name (its key-shape discriminator on DynamoDB). */
|
|
1281
|
+
readonly name: string;
|
|
1282
|
+
/** The scope the uniqueness is partitioned by (path-rooted values). */
|
|
1283
|
+
readonly scope: readonly EffectPath[];
|
|
1284
|
+
/** The fields whose combined value must be unique within the scope. */
|
|
1285
|
+
readonly fields: readonly EffectPath[];
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* An edge write effect (proposal §2 `edges`): create / delete the adjacency row
|
|
1289
|
+
* that materializes a target relation. Derived to an adjacency `Put` / `Delete`
|
|
1290
|
+
* by #82/#85 (the `src/relation/edge-write.ts` primitive) — stored opaquely here.
|
|
1291
|
+
*/
|
|
1292
|
+
interface EdgeEffect {
|
|
1293
|
+
readonly kind: 'putEdge' | 'deleteEdge';
|
|
1294
|
+
/** Resolves the target model whose relation this edge feeds. */
|
|
1295
|
+
readonly targetFactory: () => new (...args: unknown[]) => unknown;
|
|
1296
|
+
/** The relation property on the target that reads this edge. */
|
|
1297
|
+
readonly relationProperty: string;
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* A derived (cascading) update (proposal §2 `derive`): e.g. `User.postCount +=
|
|
1301
|
+
* 1`. Derived to an `UpdateItem` `ADD` / `SET` in the transaction by #85 —
|
|
1302
|
+
* stored opaquely here.
|
|
1303
|
+
*/
|
|
1304
|
+
interface DeriveEffect {
|
|
1305
|
+
readonly kind: 'derive';
|
|
1306
|
+
/** Resolves the entity whose attribute is derived-updated. */
|
|
1307
|
+
readonly targetFactory: () => new (...args: unknown[]) => unknown;
|
|
1308
|
+
/** The key binding of the updated row (path-rooted values). */
|
|
1309
|
+
readonly keys: Readonly<Record<string, EffectPath>>;
|
|
1310
|
+
/** The attribute to increment / set. */
|
|
1311
|
+
readonly attribute: string;
|
|
1312
|
+
/** The amount to add (for an increment). */
|
|
1313
|
+
readonly amount: number;
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* A domain event (proposal §2 `emits`): an outbox `Put` drained to Streams /
|
|
1317
|
+
* `src/cdc/`. Derived by #87 — stored opaquely here.
|
|
1318
|
+
*/
|
|
1319
|
+
interface EmitEffect {
|
|
1320
|
+
readonly kind: 'event';
|
|
1321
|
+
/** The event name (e.g. `PostCreated`). */
|
|
1322
|
+
readonly name: string;
|
|
1323
|
+
/** The event payload, each value path-rooted. */
|
|
1324
|
+
readonly payload: Readonly<Record<string, EffectPath>>;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* An idempotency guard (proposal §2 `idempotency`): a client-token guard item
|
|
1328
|
+
* (`attribute_not_exists`). Derived by #87 — stored opaquely here.
|
|
1329
|
+
*/
|
|
1330
|
+
interface IdempotencyEffect {
|
|
1331
|
+
readonly kind: 'idempotency';
|
|
1332
|
+
/** The path-rooted token the guard keys on (e.g. `$.input.requestId`). */
|
|
1333
|
+
readonly token: EffectPath;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* The full §2 effect set a {@link LifecycleContract} carries. Every field is
|
|
1337
|
+
* optional; an omitted field is the empty effect set. #83 stores this verbatim
|
|
1338
|
+
* and consumes none — the per-fragment hook a later issue (#84–#87) reads.
|
|
1339
|
+
*/
|
|
1340
|
+
interface LifecycleEffects {
|
|
1341
|
+
/** Referential-integrity requirements (→ `ConditionCheck`, #84). */
|
|
1342
|
+
readonly requires?: readonly RequiresEffect[];
|
|
1343
|
+
/** Uniqueness guards (→ `UNIQUE#…` guard `Put`, #86). */
|
|
1344
|
+
readonly unique?: readonly UniqueEffect[];
|
|
1345
|
+
/** Edge write effects (→ adjacency `Put` / `Delete`, #82/#85). */
|
|
1346
|
+
readonly edges?: readonly EdgeEffect[];
|
|
1347
|
+
/** Derived / cascading updates (→ `UpdateItem` `ADD`/`SET`, #85). */
|
|
1348
|
+
readonly derive?: readonly DeriveEffect[];
|
|
1349
|
+
/** Domain events (→ outbox `Put`, #87). */
|
|
1350
|
+
readonly emits?: readonly EmitEffect[];
|
|
1351
|
+
/** Idempotency guard (→ client-token guard `Put`, #87). */
|
|
1352
|
+
readonly idempotency?: IdempotencyEffect;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Marker carried by a {@link LifecycleContract} so it is recognized regardless of
|
|
1356
|
+
* declaration shape. Built by {@link WriteRecorder.lifecycle}.
|
|
1357
|
+
*/
|
|
1358
|
+
declare const LIFECYCLE_CONTRACT_MARKER: unique symbol;
|
|
1359
|
+
/**
|
|
1360
|
+
* One lifecycle's save contract — the §2 effect set for a single phase
|
|
1361
|
+
* (`create` / `update` / `remove`). #83 emits the lifecycle's **base write op**
|
|
1362
|
+
* (selected from the fragment intent) and leaves the {@link effects} as the
|
|
1363
|
+
* untouched per-fragment hook a later issue derives.
|
|
1364
|
+
*/
|
|
1365
|
+
interface LifecycleContract {
|
|
1366
|
+
readonly [LIFECYCLE_CONTRACT_MARKER]: true;
|
|
1367
|
+
/** The §2 effect arrays, stored opaquely by #83. */
|
|
1368
|
+
readonly effects: LifecycleEffects;
|
|
1369
|
+
}
|
|
1370
|
+
/** Runtime guard: is `value` a {@link LifecycleContract} (a `w.lifecycle(...)`)? */
|
|
1371
|
+
declare function isLifecycleContract(value: unknown): value is LifecycleContract;
|
|
1372
|
+
/**
|
|
1373
|
+
* Marker carried by a model's static `writes` member when it is an
|
|
1374
|
+
* {@link EntityWritesDefinition} (an `entityWrites(...)` save contract), so it is
|
|
1375
|
+
* told apart from an {@link edgeWrites} member (#82) on the same conventional
|
|
1376
|
+
* property name.
|
|
1377
|
+
*/
|
|
1378
|
+
declare const ENTITY_WRITES_MARKER: unique symbol;
|
|
1379
|
+
/**
|
|
1380
|
+
* The per-lifecycle save contract produced by {@link entityWrites}: a map from
|
|
1381
|
+
* {@link WriteLifecyclePhase} to its {@link LifecycleContract}. Every phase is
|
|
1382
|
+
* optional — a model may declare only the lifecycles it needs (e.g. a `create` +
|
|
1383
|
+
* `remove` save contract with no `update`).
|
|
1384
|
+
*/
|
|
1385
|
+
interface EntityWritesDefinition {
|
|
1386
|
+
readonly [ENTITY_WRITES_MARKER]: true;
|
|
1387
|
+
/** The create-lifecycle save contract, if declared. */
|
|
1388
|
+
readonly create?: LifecycleContract;
|
|
1389
|
+
/** The update-lifecycle save contract, if declared. */
|
|
1390
|
+
readonly update?: LifecycleContract;
|
|
1391
|
+
/** The remove-lifecycle save contract, if declared. */
|
|
1392
|
+
readonly remove?: LifecycleContract;
|
|
1393
|
+
}
|
|
1394
|
+
/** Runtime guard: is `value` an {@link EntityWritesDefinition}? */
|
|
1395
|
+
declare function isEntityWritesDefinition(value: unknown): value is EntityWritesDefinition;
|
|
1396
|
+
/**
|
|
1397
|
+
* The recorder handed to the {@link entityWrites} builder. `w.lifecycle({...})`
|
|
1398
|
+
* accepts the §2 effect arrays and brands them into a {@link LifecycleContract}.
|
|
1399
|
+
* #83 stores the effects opaquely; #84–#87 read them.
|
|
1400
|
+
*/
|
|
1401
|
+
interface WriteRecorder {
|
|
1402
|
+
/**
|
|
1403
|
+
* Build one lifecycle's save contract from its §2 effect arrays. Every effect
|
|
1404
|
+
* field is optional; an omitted field is an empty effect set. The returned
|
|
1405
|
+
* contract is branded so the compiler can resolve it from the model's `writes`
|
|
1406
|
+
* map by lifecycle phase.
|
|
1407
|
+
*/
|
|
1408
|
+
lifecycle(effects?: LifecycleEffects): LifecycleContract;
|
|
1409
|
+
/** Declare a referential-integrity requirement (§2 `requires`; derived #84). */
|
|
1410
|
+
exists(target: () => new (...args: unknown[]) => unknown, keys: Readonly<Record<string, EffectPath>>): RequiresEffect;
|
|
1411
|
+
/** Declare a uniqueness guard (§2 `unique`; derived #86). */
|
|
1412
|
+
unique(spec: {
|
|
1413
|
+
readonly name: string;
|
|
1414
|
+
readonly scope: readonly EffectPath[];
|
|
1415
|
+
readonly fields: readonly EffectPath[];
|
|
1416
|
+
}): UniqueEffect;
|
|
1417
|
+
/** Declare an edge create effect (§2 `edges`; derived #82/#85). */
|
|
1418
|
+
putEdge(target: () => new (...args: unknown[]) => unknown, relationProperty: string): EdgeEffect;
|
|
1419
|
+
/** Declare an edge delete effect (§2 `edges`; derived #82/#85). */
|
|
1420
|
+
deleteEdge(target: () => new (...args: unknown[]) => unknown, relationProperty: string): EdgeEffect;
|
|
1421
|
+
/** Declare a derived / cascading update (§2 `derive`; derived #85). */
|
|
1422
|
+
increment(target: () => new (...args: unknown[]) => unknown, keys: Readonly<Record<string, EffectPath>>, attribute: string, amount: number): DeriveEffect;
|
|
1423
|
+
/** Declare a domain event (§2 `emits`; derived #87). */
|
|
1424
|
+
event(name: string, payload: Readonly<Record<string, EffectPath>>): EmitEffect;
|
|
1425
|
+
/** Declare an idempotency guard (§2 `idempotency`; derived #87). */
|
|
1426
|
+
idempotentBy(token: EffectPath): IdempotencyEffect;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* The lifecycle map a {@link entityWrites} builder returns: each phase is either
|
|
1430
|
+
* a built {@link LifecycleContract} (`w.lifecycle({...})`) or omitted.
|
|
1431
|
+
*/
|
|
1432
|
+
interface EntityWritesShape {
|
|
1433
|
+
readonly create?: LifecycleContract;
|
|
1434
|
+
readonly update?: LifecycleContract;
|
|
1435
|
+
readonly remove?: LifecycleContract;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Declare a model's reusable **save contract** (issue #83, proposal §2). The
|
|
1439
|
+
* builder receives a {@link WriteRecorder} and returns the per-lifecycle map
|
|
1440
|
+
* (`{ create?, update?, remove? }`), each value a `w.lifecycle({...})` carrying
|
|
1441
|
+
* the §2 effect arrays. The result is stored on the model as
|
|
1442
|
+
* `static readonly writes = entityWrites<Model>((w) => ({ … }))`.
|
|
1443
|
+
*
|
|
1444
|
+
* #83 reads only **which** lifecycles are declared (to resolve a fragment's base
|
|
1445
|
+
* write op by intent) and stores the effect arrays opaquely; the effect
|
|
1446
|
+
* derivation is #84–#87.
|
|
1447
|
+
*
|
|
1448
|
+
* @typeParam M The model type the save contract belongs to (documentary; the
|
|
1449
|
+
* recorder is structural).
|
|
1450
|
+
* @example
|
|
1451
|
+
* ```ts
|
|
1452
|
+
* static readonly writes = entityWrites<PostModel>((w) => ({
|
|
1453
|
+
* create: w.lifecycle({
|
|
1454
|
+
* requires: [w.exists(() => UserModel, { userId: '$.input.userId' })],
|
|
1455
|
+
* edges: [w.putEdge(() => UserModel, 'posts')],
|
|
1456
|
+
* }),
|
|
1457
|
+
* remove: w.lifecycle({ edges: [w.deleteEdge(() => UserModel, 'posts')] }),
|
|
1458
|
+
* }));
|
|
1459
|
+
* ```
|
|
1460
|
+
*/
|
|
1461
|
+
declare function entityWrites<M = unknown>(builder: (w: WriteRecorder) => EntityWritesShape): EntityWritesDefinition;
|
|
1462
|
+
/**
|
|
1463
|
+
* Read the {@link EntityWritesDefinition} declared on a model class as its static
|
|
1464
|
+
* `writes` member, or `undefined` when the model declares no `entityWrites` save
|
|
1465
|
+
* contract. Scans the class's own static members for the {@link
|
|
1466
|
+
* ENTITY_WRITES_MARKER}, exactly as {@link getEdgeWrites} (#82) scans for the
|
|
1467
|
+
* edge marker — so the two `writes` forms coexist without a fixed property name
|
|
1468
|
+
* collision (a model carries one or the other; the marker disambiguates).
|
|
1469
|
+
*/
|
|
1470
|
+
declare function getEntityWrites(modelClass: new (...args: unknown[]) => unknown): EntityWritesDefinition | undefined;
|
|
1471
|
+
/** The lifecycle phase a mutation fragment intent selects. */
|
|
1472
|
+
declare function lifecyclePhaseForIntent(intent: 'create' | 'update' | 'remove'): WriteLifecyclePhase;
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Internal write-plan composition DSL — `mutation` / `definePlan` (issue #83,
|
|
1476
|
+
* Epic #80; spec `docs/mutation-command-derivation.md` §1, §3).
|
|
1477
|
+
*
|
|
1478
|
+
* A **mutation is INTERNAL** — it is *not* a public API. It is a *write-plan
|
|
1479
|
+
* composition language* used as the **implementation** of a Command: it declares
|
|
1480
|
+
* a set of write **fragments** that must execute atomically. The only externally
|
|
1481
|
+
* exposed surface is the fixed Command IF (`publicCommandModel(...).plan(...)`,
|
|
1482
|
+
* `src/define/contract.ts`); the mutation document is never exposed.
|
|
1483
|
+
*
|
|
1484
|
+
* The authoring form (issue #108) is a **descriptor map**: the body returns an
|
|
1485
|
+
* **alias → write-descriptor** object, the declarative twin of an in-process
|
|
1486
|
+
* `DDBModel.mutate(...)` call (the only difference is the values are `$.field`
|
|
1487
|
+
* placeholders rather than concrete values):
|
|
1488
|
+
*
|
|
1489
|
+
* ```ts
|
|
1490
|
+
* const JoinGroup = mutation($ => ({
|
|
1491
|
+
* membership: { create: GroupMembership, key: { groupId: $.groupId, userId: $.userId }, input: { role: $.role } },
|
|
1492
|
+
* permission: { create: () => Permission, key: { id: $.permId }, input: { groupId: $.membership.groupId } },
|
|
1493
|
+
* }));
|
|
1494
|
+
* ```
|
|
1495
|
+
*
|
|
1496
|
+
* ## Descriptors are DECLARATIONS, not executed calls
|
|
1497
|
+
*
|
|
1498
|
+
* Each descriptor is a write **fragment** declaration, never an executed write.
|
|
1499
|
+
* The intent key (`create` / `update` / `remove`) is the **discriminator** and
|
|
1500
|
+
* carries the target model (directly, or as a `() => Model` thunk — the
|
|
1501
|
+
* forward/circular-reference escape hatch); `key` / `input` bind model fields to
|
|
1502
|
+
* `$.field` placeholders (or literals). The compiler
|
|
1503
|
+
* (`src/spec/mutation-command.ts`) expands the captured {@link MutationFragment}
|
|
1504
|
+
* list at compile time into a {@link CommandSpec} / `TransactionSpec`. The map's
|
|
1505
|
+
* **insertion order** is the fragments' declaration (execution) order, so the IR
|
|
1506
|
+
* is the SAME ordered `{ name, fragments }` shape the legacy recorder produced —
|
|
1507
|
+
* the single-fragment fast path (#83) and the N-fragment atomic merge (#90) are
|
|
1508
|
+
* unchanged.
|
|
1509
|
+
*
|
|
1510
|
+
* ## Intent selects the lifecycle; `use:` is optional
|
|
1511
|
+
*
|
|
1512
|
+
* Each fragment's **intent** (`create` / `update` / `remove`) selects the
|
|
1513
|
+
* lifecycle from the target's `writes` save contract (`entityWrites`, §2):
|
|
1514
|
+
* `create` → `writes.create`, etc. `use:` is **optional** — it defaults to the
|
|
1515
|
+
* target model's own `writes`; supply it only to adopt a *custom* write contract,
|
|
1516
|
+
* and it takes the **whole** `writes` set (an {@link EntityWritesDefinition}),
|
|
1517
|
+
* never `writes.create` (the intent already picks the lifecycle, so naming the
|
|
1518
|
+
* lifecycle would double-specify it). When the target declares **no** `writes`,
|
|
1519
|
+
* the fragment compiles the **trivial base op** for its intent (create → `Put`
|
|
1520
|
+
* `attribute_not_exists(PK)`, update → `Update`, remove → `Delete`).
|
|
1521
|
+
*
|
|
1522
|
+
* The `$` argument is the **placeholder proxy**: `$.userId` at a `key` / `input`
|
|
1523
|
+
* leaf mints a faithful {@link MutationInputRef} naming an input field, and
|
|
1524
|
+
* `$.alias.field` names a value an **earlier** fragment (by alias) writes — a
|
|
1525
|
+
* cross-fragment reference resolved at compile time (the legacy `$.entity[i].field`
|
|
1526
|
+
* positional form, now alias-based). No arbitrary JS may reach a binding, exactly
|
|
1527
|
+
* as the contract `params` sentinel enforces.
|
|
1528
|
+
*/
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* The intent of a write fragment (proposal §3): `create` / `update` / `remove`
|
|
1532
|
+
* (`remove`, **not** `del`). The intent selects the lifecycle from the target's
|
|
1533
|
+
* save contract, and the base write op (create → `PutItem`, update →
|
|
1534
|
+
* `UpdateItem`, remove → `DeleteItem`).
|
|
1535
|
+
*/
|
|
1536
|
+
type MutationIntent = 'create' | 'update' | 'remove';
|
|
1537
|
+
declare const INPUT_REF_BRAND: unique symbol;
|
|
1538
|
+
/**
|
|
1539
|
+
* A faithful reference to a mutation **input** field, minted by reading `$.<field>`
|
|
1540
|
+
* in a mutation body (e.g. `$.title`). Branded so the compiler tells a faithful
|
|
1541
|
+
* input reference from a literal and renders its `{token}` (`{title}`) into the
|
|
1542
|
+
* derived write template — the declarative binding the proposal's `$.input.*`
|
|
1543
|
+
* path root names (here the root is implied: a fragment `input` value is always
|
|
1544
|
+
* an input field reference or a concrete literal).
|
|
1545
|
+
*/
|
|
1546
|
+
interface MutationInputRef {
|
|
1547
|
+
readonly [INPUT_REF_BRAND]: true;
|
|
1548
|
+
/** The input field name, e.g. `title`. */
|
|
1549
|
+
readonly field: string;
|
|
1550
|
+
/** The template token this reference renders to, e.g. `{title}`. */
|
|
1551
|
+
readonly token: string;
|
|
1552
|
+
}
|
|
1553
|
+
/** Runtime guard: is `value` a {@link MutationInputRef} (a `$.<field>` read)? */
|
|
1554
|
+
declare function isMutationInputRef(value: unknown): value is MutationInputRef;
|
|
1555
|
+
declare const ENTITY_REF_BRAND: unique symbol;
|
|
1556
|
+
/**
|
|
1557
|
+
* A faithful **cross-fragment reference** (`$.entity[i].field`): the value a later
|
|
1558
|
+
* fragment binds from an **earlier** fragment's written field (proposal §3). The
|
|
1559
|
+
* compiler ({@link compileMutationPlan}) resolves it at **compile time** to the
|
|
1560
|
+
* earlier fragment's binding for that field — a `TransactWriteItems` cannot read
|
|
1561
|
+
* one item's value mid-transaction, so the dependency is resolved declaratively
|
|
1562
|
+
* from the shared input (the referenced field must itself trace to an input
|
|
1563
|
+
* reference or a literal), keeping the composed transaction render-time-resolvable
|
|
1564
|
+
* and atomic. A **forward / self** reference (to a same-or-later fragment) is a
|
|
1565
|
+
* circular dependency, rejected at build time.
|
|
1566
|
+
*/
|
|
1567
|
+
interface MutationEntityRef {
|
|
1568
|
+
readonly [ENTITY_REF_BRAND]: true;
|
|
1569
|
+
/** The 0-based index of the producing fragment (declaration order). */
|
|
1570
|
+
readonly fragmentIndex: number;
|
|
1571
|
+
/** The produced field name on that fragment's written entity. */
|
|
1572
|
+
readonly field: string;
|
|
1573
|
+
/** The source path, e.g. `$.entity[0].postId` (documentary; surfaces in errors). */
|
|
1574
|
+
readonly path: string;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* The placeholder proxy handed to a mutation body as `$`. In the descriptor-map
|
|
1578
|
+
* authoring form (issue #108) a top-level read `$.<x>` is **ambiguous** until used:
|
|
1579
|
+
*
|
|
1580
|
+
* - `$.role` placed at a `key` / `input` leaf is an **input-field reference**
|
|
1581
|
+
* (a {@link MutationInputRef}, token `{role}`) — bound from the command input;
|
|
1582
|
+
* - `$.membership.role` is a **cross-fragment reference** — the `role` field
|
|
1583
|
+
* written by the fragment named `membership` (an alias). It mints an
|
|
1584
|
+
* {@link AliasFieldRef}, which the {@link mutation} assembler rewrites into the
|
|
1585
|
+
* legacy `$.entity[<index>].<field>` string the compiler resolves (the alias →
|
|
1586
|
+
* declaration-order index is known once every alias key is seen).
|
|
1587
|
+
*
|
|
1588
|
+
* So each top-level read returns a value that is **both** a faithful
|
|
1589
|
+
* {@link MutationInputRef} (when used directly as a leaf) and supports one further
|
|
1590
|
+
* `.<field>` access (when used as an alias root). Any other access / coercion
|
|
1591
|
+
* throws — the binding stays declarative (only a direct `$.field` / `$.alias.field`
|
|
1592
|
+
* reference or a literal is representable, never a transform).
|
|
1593
|
+
*/
|
|
1594
|
+
type MutationInputProxy = Record<string, MutationInputRef>;
|
|
1595
|
+
declare const FRAGMENT_BRAND: unique symbol;
|
|
1596
|
+
/**
|
|
1597
|
+
* The per-field input binding of a fragment: model field name → a faithful
|
|
1598
|
+
* {@link MutationInputRef} (a `$.<field>` reference) or a concrete scalar literal
|
|
1599
|
+
* (e.g. a fixed discriminator). One declarative level — the values bind the model
|
|
1600
|
+
* field they key, never a transform.
|
|
1601
|
+
*/
|
|
1602
|
+
type FragmentInput = Readonly<Record<string, MutationInputRef | string | number | boolean | Date | null>>;
|
|
1603
|
+
/**
|
|
1604
|
+
* A descriptor `key` / `input` value as authored in the descriptor-map form
|
|
1605
|
+
* (issue #108): a `$.field` input reference, a `$.alias.field` cross-fragment
|
|
1606
|
+
* reference (an opaque alias-field ref the assembler rewrites), or a literal.
|
|
1607
|
+
* Structurally `unknown` at the leaf because the alias-field ref is internal; the
|
|
1608
|
+
* assembler validates every leaf via {@link assertFaithfulInput}.
|
|
1609
|
+
*/
|
|
1610
|
+
type DescriptorBinding = Readonly<Record<string, unknown>>;
|
|
1611
|
+
/**
|
|
1612
|
+
* One recorded write fragment (proposal §3): an **intent** + a target entity + an
|
|
1613
|
+
* optional adopted save contract (`use:`) + the input binding. It is a
|
|
1614
|
+
* *declaration* the compiler expands — never an executed write.
|
|
1615
|
+
*
|
|
1616
|
+
* - `intent` — `create` / `update` / `remove`; selects the lifecycle + base op.
|
|
1617
|
+
* - `entity` — the target entity (resolved at definition time).
|
|
1618
|
+
* - `input` — the model-field → input-field / literal binding (declarative).
|
|
1619
|
+
* - `use` — the adopted {@link EntityWritesDefinition} (whole set), or `undefined`
|
|
1620
|
+
* to default to the target model's own `writes` (and, absent that, the trivial
|
|
1621
|
+
* base op). Per the `use:` rule it is **never** a single lifecycle.
|
|
1622
|
+
*/
|
|
1623
|
+
interface MutationFragment {
|
|
1624
|
+
readonly [FRAGMENT_BRAND]: true;
|
|
1625
|
+
readonly intent: MutationIntent;
|
|
1626
|
+
readonly entity: EntityRef;
|
|
1627
|
+
readonly input: FragmentInput;
|
|
1628
|
+
readonly use?: EntityWritesDefinition;
|
|
1629
|
+
}
|
|
1630
|
+
/** Runtime guard: is `value` a {@link MutationFragment}? */
|
|
1631
|
+
declare function isMutationFragment(value: unknown): value is MutationFragment;
|
|
1632
|
+
/**
|
|
1633
|
+
* A model reference in a write descriptor (issue #108): either the model class /
|
|
1634
|
+
* `.asModel()` value **directly** (`{ create: GroupMembership }`) or a **thunk**
|
|
1635
|
+
* (`{ create: () => GroupMembership }`). The thunk form is the escape hatch that
|
|
1636
|
+
* preserves forward / circular references — a model not yet defined at the
|
|
1637
|
+
* descriptor's evaluation point is named through a closure, exactly as the legacy
|
|
1638
|
+
* `m.create(() => Model, …)` recorder required.
|
|
1639
|
+
*/
|
|
1640
|
+
type ModelRef = ModelStatic<DDBModel> | (new (...args: unknown[]) => unknown) | (() => ModelStatic<DDBModel> | (new (...args: unknown[]) => unknown));
|
|
1641
|
+
/**
|
|
1642
|
+
* One **write descriptor** in the descriptor-map authoring form (issue #108) —
|
|
1643
|
+
* the declarative twin of an in-process `DDBModel.mutate` entry (#101). The intent
|
|
1644
|
+
* key (`create` / `update` / `remove`) is the **discriminator** and carries the
|
|
1645
|
+
* target {@link ModelRef} (direct or thunk). The remaining fields are declarative
|
|
1646
|
+
* bindings (`$.field` references or literals):
|
|
1647
|
+
*
|
|
1648
|
+
* - `key` — the target row's primary-key binding (identifies the row);
|
|
1649
|
+
* - `input` — the non-key field binding (the body / changed fields). Optional for
|
|
1650
|
+
* a `remove` (a delete writes no fields);
|
|
1651
|
+
* - `condition` — an optional declarative write gate (accepted for authoring
|
|
1652
|
+
* parity with the #101 descriptor; threaded by #101's in-process executor — the
|
|
1653
|
+
* declaration-DSL compiler does not consume it, so the serialized IR is
|
|
1654
|
+
* unchanged);
|
|
1655
|
+
* - `result` — an optional read-back projection `{ select, options? }` (likewise
|
|
1656
|
+
* accepted for authoring parity; the public read-back projection of a
|
|
1657
|
+
* `command(...).plan(...)` method is still declared on `command({ select })`).
|
|
1658
|
+
*
|
|
1659
|
+
* Exactly one intent key must be present.
|
|
1660
|
+
*/
|
|
1661
|
+
interface WriteDescriptor {
|
|
1662
|
+
readonly create?: ModelRef;
|
|
1663
|
+
readonly update?: ModelRef;
|
|
1664
|
+
readonly remove?: ModelRef;
|
|
1665
|
+
/** The target row's primary-key binding (`{ field: $.field | literal }`). */
|
|
1666
|
+
readonly key: DescriptorBinding;
|
|
1667
|
+
/** The non-key field binding; optional (a `remove` writes no fields). */
|
|
1668
|
+
readonly input?: DescriptorBinding;
|
|
1669
|
+
/** Optional declarative write gate (accepted for #101 parity; see above). */
|
|
1670
|
+
readonly condition?: DescriptorBinding;
|
|
1671
|
+
/** Optional read-back projection `{ select, options? }` (accepted for #101 parity). */
|
|
1672
|
+
readonly result?: {
|
|
1673
|
+
readonly select?: unknown;
|
|
1674
|
+
readonly options?: unknown;
|
|
1675
|
+
};
|
|
1676
|
+
/**
|
|
1677
|
+
* Optional custom save contract to adopt (the **whole** {@link
|
|
1678
|
+
* EntityWritesDefinition}). Omit to default to the target model's own `writes`.
|
|
1679
|
+
* Per the `use:` rule this is never a single lifecycle — the intent picks it.
|
|
1680
|
+
*/
|
|
1681
|
+
readonly use?: EntityWritesDefinition;
|
|
1682
|
+
}
|
|
1683
|
+
/** The descriptor map a {@link mutation} body returns: alias → {@link WriteDescriptor}. */
|
|
1684
|
+
type MutationDescriptorMap = Record<string, WriteDescriptor>;
|
|
1685
|
+
declare const COMMAND_PLAN_BRAND: unique symbol;
|
|
1686
|
+
/**
|
|
1687
|
+
* The recorded **CommandPlan** — a mutation's IR (proposal §3). It carries the
|
|
1688
|
+
* mutation `name` and the **list** of {@link MutationFragment}s the body declared
|
|
1689
|
+
* (1..N; #83 compiles the 1-fragment case end-to-end, the list shape is ready for
|
|
1690
|
+
* the #90 N-fragment merge). The body closure is discarded — only the declarative
|
|
1691
|
+
* fragment list is kept, exactly as the contract IR discards its method closures.
|
|
1692
|
+
*/
|
|
1693
|
+
interface CommandPlan {
|
|
1694
|
+
readonly [COMMAND_PLAN_BRAND]: true;
|
|
1695
|
+
/** The mutation name (documentary; surfaces in compiler error messages). */
|
|
1696
|
+
readonly name: string;
|
|
1697
|
+
/** The declared write fragments, in declaration order (the mutation IR). */
|
|
1698
|
+
readonly fragments: readonly MutationFragment[];
|
|
1699
|
+
}
|
|
1700
|
+
/** Runtime guard: is `value` a {@link CommandPlan} (a `mutation(...)` result)? */
|
|
1701
|
+
declare function isCommandPlan(value: unknown): value is CommandPlan;
|
|
1702
|
+
/**
|
|
1703
|
+
* The body of a {@link mutation} in the descriptor-map authoring form (issue #108):
|
|
1704
|
+
* `$ => ({ alias: descriptor, … })`. It receives the placeholder proxy `$` and
|
|
1705
|
+
* returns an **alias → {@link WriteDescriptor}** map. The map's **insertion order**
|
|
1706
|
+
* is the fragments' declaration (execution) order, and each alias is the name a
|
|
1707
|
+
* later descriptor's `$.alias.field` cross-fragment reference resolves against.
|
|
1708
|
+
*/
|
|
1709
|
+
type MutationBody = ($: MutationInputProxy) => MutationDescriptorMap;
|
|
1710
|
+
/**
|
|
1711
|
+
* Declare an internal **mutation** — a write-plan composition in the descriptor-map
|
|
1712
|
+
* authoring form (issue #108). The body returns an **alias → {@link WriteDescriptor}**
|
|
1713
|
+
* map; each descriptor names its target via the intent key (`create` / `update` /
|
|
1714
|
+
* `remove`: a model or a `() => Model` thunk) and binds `key` / `input` with `$.field`
|
|
1715
|
+
* placeholders. Insertion order is execution order, and a later descriptor may read
|
|
1716
|
+
* an earlier one's written field via `$.alias.field`.
|
|
1717
|
+
*
|
|
1718
|
+
* ```ts
|
|
1719
|
+
* const JoinGroup = mutation($ => ({
|
|
1720
|
+
* membership: { create: GroupMembership, key: { groupId: $.groupId, userId: $.userId }, input: { role: $.role } },
|
|
1721
|
+
* permission: { create: Permission, key: { id: $.permissionId }, input: { groupId: $.membership.groupId } },
|
|
1722
|
+
* }));
|
|
1723
|
+
* ```
|
|
1724
|
+
*
|
|
1725
|
+
* A leading **name** is optional (it surfaces only in compiler error messages — the
|
|
1726
|
+
* alias map already names each fragment): `mutation('JoinGroup', $ => ({ … }))`.
|
|
1727
|
+
*
|
|
1728
|
+
* The result is a branded {@link CommandPlan} (the mutation IR) — *not* public; it
|
|
1729
|
+
* implements a Command IF via `publicCommandModel(...).plan(mutation)`. The IR shape
|
|
1730
|
+
* (`{ name, fragments }`) is unchanged from the legacy recorder form, so the compiler,
|
|
1731
|
+
* serializer, and every runtime consume it identically.
|
|
1732
|
+
*
|
|
1733
|
+
* @throws if the body returns a non-object / empty map, a descriptor has no (or
|
|
1734
|
+
* multiple) intent key(s), a binding is non-declarative, or a `$.alias.field`
|
|
1735
|
+
* reference names an unknown alias.
|
|
1736
|
+
*/
|
|
1737
|
+
declare function mutation(body: MutationBody): CommandPlan;
|
|
1738
|
+
declare function mutation(name: string, body: MutationBody): CommandPlan;
|
|
1739
|
+
/**
|
|
1740
|
+
* Alias of {@link mutation} (proposal naming: the internal DSL is `mutation` /
|
|
1741
|
+
* `definePlan`). Identical behavior; provided so a producer may name the plan
|
|
1742
|
+
* declaration `definePlan(…)` when that reads more clearly at the call site.
|
|
1743
|
+
*/
|
|
1744
|
+
declare const definePlan: typeof mutation;
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* Serializable static operation specs and manifest (issue #42, Python-bridge
|
|
1748
|
+
* Phase 0b).
|
|
1749
|
+
*
|
|
1750
|
+
* Everything in this module is **JSON-serializable** by construction (only
|
|
1751
|
+
* strings / numbers / booleans / arrays / plain objects — no functions, classes,
|
|
1752
|
+
* `Date`, `Set`, or `undefined`-valued fields are emitted). These types mirror
|
|
1753
|
+
* the `graphddb_manifest.json` / `graphddb_operations.json` shapes sketched in
|
|
1754
|
+
* `docs/python-bridge.md`. The generator (#43) writes them to disk; the
|
|
1755
|
+
* Python runtime (#44+) interprets them.
|
|
1756
|
+
*
|
|
1757
|
+
* ## Templates and placeholders
|
|
1758
|
+
*
|
|
1759
|
+
* Key-condition / range / item / change *values* are **template strings** with
|
|
1760
|
+
* two placeholder forms:
|
|
1761
|
+
*
|
|
1762
|
+
* - `{paramName}` — bound from the caller-supplied params at execution time.
|
|
1763
|
+
* - `{result.sourceField}` — bound from a field of the **prior** operation's
|
|
1764
|
+
* result item(s), used to chain relation operations.
|
|
1765
|
+
*
|
|
1766
|
+
* A value with no `{...}` is a literal (e.g. a fixed sort-key discriminator
|
|
1767
|
+
* `PROFILE`).
|
|
1768
|
+
*/
|
|
1769
|
+
|
|
1770
|
+
/** The schema version emitted in every manifest / operations document. */
|
|
1771
|
+
declare const SPEC_VERSION: "1.0";
|
|
1772
|
+
/** Field type as carried in the manifest (derived from the DynamoDB type). */
|
|
1773
|
+
type ManifestFieldType = 'string' | 'number' | 'boolean' | 'binary' | 'stringSet' | 'numberSet' | 'list' | 'map';
|
|
1774
|
+
interface ManifestField {
|
|
1775
|
+
readonly type: ManifestFieldType;
|
|
1776
|
+
/**
|
|
1777
|
+
* Semantic format for `string` fields that carry a serialized temporal value.
|
|
1778
|
+
* `datetime` → ISO 8601 instant, `date` → `YYYY-MM-DD`. Absent for plain
|
|
1779
|
+
* fields. The Python runtime uses this to restore `datetime` on hydration,
|
|
1780
|
+
* mirroring the TS hydrator's `format`-driven `Date` reconstruction.
|
|
1781
|
+
*/
|
|
1782
|
+
readonly format?: 'datetime' | 'date';
|
|
1783
|
+
}
|
|
1784
|
+
/** A key (PK or GSI) template descriptor in the manifest. */
|
|
1785
|
+
interface ManifestKey {
|
|
1786
|
+
/** Input field names the key is composed from (in declaration order). */
|
|
1787
|
+
readonly inputFields: readonly string[];
|
|
1788
|
+
/** Template for the partition-key value, e.g. `EMAIL#{email}`. */
|
|
1789
|
+
readonly pkTemplate: string;
|
|
1790
|
+
/** Template for the sort-key value, or `null` when the key has no sort key. */
|
|
1791
|
+
readonly skTemplate: string | null;
|
|
1792
|
+
}
|
|
1793
|
+
interface ManifestGsi extends ManifestKey {
|
|
1794
|
+
readonly indexName: string;
|
|
1795
|
+
readonly unique: boolean;
|
|
1796
|
+
}
|
|
1797
|
+
interface ManifestRelation {
|
|
1798
|
+
readonly type: 'hasMany' | 'hasOne' | 'belongsTo';
|
|
1799
|
+
/** Target entity (model class) name. */
|
|
1800
|
+
readonly target: string;
|
|
1801
|
+
/** target field → source field (on this entity). */
|
|
1802
|
+
readonly keyBinding: Readonly<Record<string, string>>;
|
|
1803
|
+
}
|
|
1804
|
+
interface ManifestEntity {
|
|
1805
|
+
/** Declared (logical) table name. */
|
|
1806
|
+
readonly table: string;
|
|
1807
|
+
/** Physical table name (after `TableMapping` resolution). */
|
|
1808
|
+
readonly physicalName: string;
|
|
1809
|
+
/** PK prefix, e.g. `USER#`. */
|
|
1810
|
+
readonly prefix: string;
|
|
1811
|
+
readonly fields: Readonly<Record<string, ManifestField>>;
|
|
1812
|
+
/** Primary key, or `null` when the entity has none. */
|
|
1813
|
+
readonly key: ManifestKey | null;
|
|
1814
|
+
readonly gsis: readonly ManifestGsi[];
|
|
1815
|
+
readonly relations: Readonly<Record<string, ManifestRelation>>;
|
|
1816
|
+
}
|
|
1817
|
+
interface ManifestTable {
|
|
1818
|
+
readonly physicalName: string;
|
|
1819
|
+
}
|
|
1820
|
+
interface Manifest {
|
|
1821
|
+
readonly version: typeof SPEC_VERSION;
|
|
1822
|
+
readonly tables: Readonly<Record<string, ManifestTable>>;
|
|
1823
|
+
readonly entities: Readonly<Record<string, ManifestEntity>>;
|
|
1824
|
+
}
|
|
1825
|
+
interface ParamSpec {
|
|
1826
|
+
readonly type: ParamKind;
|
|
1827
|
+
readonly required: boolean;
|
|
1828
|
+
/** Allowed literal values for `literal` params; omitted otherwise. */
|
|
1829
|
+
readonly literals?: readonly (string | number)[];
|
|
1830
|
+
/**
|
|
1831
|
+
* For an `array` param (transaction `forEach` source), the per-element field
|
|
1832
|
+
* descriptors (field name → element param spec). Omitted for scalar params.
|
|
1833
|
+
*/
|
|
1834
|
+
readonly element?: Readonly<Record<string, ParamSpec>>;
|
|
1835
|
+
}
|
|
1836
|
+
/** A `begins_with` range condition on a sort key (templated value). */
|
|
1837
|
+
interface RangeConditionSpec {
|
|
1838
|
+
readonly operator: 'begins_with';
|
|
1839
|
+
readonly key: string;
|
|
1840
|
+
/** Template value, e.g. `GROUP#` or `GROUP#{result.groupId}`. */
|
|
1841
|
+
readonly value: string;
|
|
1842
|
+
}
|
|
1843
|
+
/** Server-side declarative filter, carried verbatim for the runtime to compile. */
|
|
1844
|
+
interface FilterSpec {
|
|
1845
|
+
/** The declarative {@link FilterInput} tree (JSON-safe). */
|
|
1846
|
+
readonly declarative: unknown;
|
|
1847
|
+
}
|
|
1848
|
+
type ReadOperationType = 'Query' | 'GetItem' | 'BatchGetItem';
|
|
1849
|
+
/**
|
|
1850
|
+
* A single static read operation. Relation chains are expressed as multiple
|
|
1851
|
+
* operations whose `resultPath` / templated key conditions wire them together.
|
|
1852
|
+
*/
|
|
1853
|
+
interface OperationSpec {
|
|
1854
|
+
readonly type: ReadOperationType;
|
|
1855
|
+
readonly tableName: string;
|
|
1856
|
+
readonly indexName?: string;
|
|
1857
|
+
/**
|
|
1858
|
+
* Key condition: attribute name → template value. For a `BatchGetItem` this is
|
|
1859
|
+
* the per-key shape (one key per resolved source item at runtime).
|
|
1860
|
+
*/
|
|
1861
|
+
readonly keyCondition: Readonly<Record<string, string>>;
|
|
1862
|
+
readonly rangeCondition?: RangeConditionSpec;
|
|
1863
|
+
/** Projected fields (entity field names). */
|
|
1864
|
+
readonly projection: readonly string[];
|
|
1865
|
+
readonly limit?: number;
|
|
1866
|
+
readonly filter?: FilterSpec;
|
|
1867
|
+
/**
|
|
1868
|
+
* Where the result of this operation is placed in the assembled result.
|
|
1869
|
+
* `$` is the root; `$.groups.items` a relation connection's items, etc.
|
|
1870
|
+
*/
|
|
1871
|
+
readonly resultPath: string;
|
|
1872
|
+
/**
|
|
1873
|
+
* For relation operations: the source field on the prior result whose value(s)
|
|
1874
|
+
* drive this operation's `{result.*}` placeholders. Absent on the root op.
|
|
1875
|
+
*/
|
|
1876
|
+
readonly sourceField?: string;
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* The **execution plan** for a multi-operation read (issue #70a). It is the
|
|
1880
|
+
* Single Source of Truth for *how* a query's {@link OperationSpec}[] is staged:
|
|
1881
|
+
* which operations are independent (may run concurrently) and which must wait for
|
|
1882
|
+
* a prior operation's result. Every runtime that honors it produces identical
|
|
1883
|
+
* effects with identical concurrency discipline — the staging is **serialized,
|
|
1884
|
+
* not re-derived** (cf. the contract decided-facts discipline).
|
|
1885
|
+
*
|
|
1886
|
+
* - `groups` is an ordered list of **stages**; each stage is a list of indices
|
|
1887
|
+
* into the query's `operations[]`. Operations **within** a stage are mutually
|
|
1888
|
+
* independent and may be issued concurrently; stages run **in order** because a
|
|
1889
|
+
* later stage reads a `{result.*}` value produced by an earlier one. Stage 0 is
|
|
1890
|
+
* always `[0]` (the root). Every operation index appears in exactly one stage,
|
|
1891
|
+
* and the stages are a topological layering of the result-dependency graph.
|
|
1892
|
+
* - `concurrency` is the declared in-flight bound a runtime applies **within**
|
|
1893
|
+
* each stage (the shared {@link RELATION_TRAVERSAL_CONCURRENCY} = 16): at most
|
|
1894
|
+
* this many operations of a stage are issued at once.
|
|
1895
|
+
*
|
|
1896
|
+
* ## Derivation (see `buildReadOperations` / `deriveExecutionPlan`)
|
|
1897
|
+
*
|
|
1898
|
+
* The planner emits `operations[]` in a deterministic pre-order: the root at
|
|
1899
|
+
* index 0, then each relation child immediately followed by its own descendants.
|
|
1900
|
+
* A child's key templates reference `{result.<sourceField>}` of the **operation
|
|
1901
|
+
* whose `resultPath` is the child's parent path** — that producer is the child's
|
|
1902
|
+
* sole dependency. The stage of an operation is therefore `1 + stage(parent)`:
|
|
1903
|
+
* the root is stage 0; sibling relations that share a parent path land in the
|
|
1904
|
+
* **same** stage (no inter-`{result.*}` dependency between siblings, so they are
|
|
1905
|
+
* independent and concurrency-eligible); a grandchild that reads its parent's
|
|
1906
|
+
* `{result.*}` lands one stage later. This mirrors exactly the level-by-level
|
|
1907
|
+
* fan-out the TS relation runtime performs at runtime (`resolveRelations` issues
|
|
1908
|
+
* sibling relations together under the same bound) — #70a only *records* it.
|
|
1909
|
+
*
|
|
1910
|
+
* Absent on a single-operation spec and on specs produced before #70a; a consumer
|
|
1911
|
+
* without a plan falls back to one-operation-per-stage **sequential** execution
|
|
1912
|
+
* (the pre-#70 behavior), so an absent plan never changes results.
|
|
1913
|
+
*/
|
|
1914
|
+
interface ExecutionPlanSpec {
|
|
1915
|
+
/** Ordered stages; each stage is indices into `operations[]` (stage 0 = `[0]`). */
|
|
1916
|
+
readonly groups: readonly (readonly number[])[];
|
|
1917
|
+
/** The declared in-flight bound applied within each stage (16). */
|
|
1918
|
+
readonly concurrency: number;
|
|
1919
|
+
}
|
|
1920
|
+
/** A query (read) definition's full execution spec. */
|
|
1921
|
+
interface QuerySpec {
|
|
1922
|
+
readonly params: Readonly<Record<string, ParamSpec>>;
|
|
1923
|
+
readonly operations: readonly OperationSpec[];
|
|
1924
|
+
/**
|
|
1925
|
+
* Result cardinality of the **root** (`$`) operation, derived from the
|
|
1926
|
+
* definition kind: `'one'` for `defineQuery` (a single entity object, with any
|
|
1927
|
+
* relations attached directly), `'many'` for `defineList` (a `{ items, cursor }`
|
|
1928
|
+
* connection). The relation runtime (#45) uses this to shape the root result:
|
|
1929
|
+
* a `'one'` Query root takes the first matched item rather than returning a
|
|
1930
|
+
* connection. Absent in specs produced before #45; consumers should treat an
|
|
1931
|
+
* absent value as `'many'` for a `Query`/`BatchGetItem` root and `'one'` for a
|
|
1932
|
+
* `GetItem` root (the pre-#45 behavior).
|
|
1933
|
+
*/
|
|
1934
|
+
readonly cardinality?: 'one' | 'many';
|
|
1935
|
+
/**
|
|
1936
|
+
* The staged execution plan (issue #70a): which {@link operations} are
|
|
1937
|
+
* independent (concurrency-eligible) vs. result-dependent. Present only when the
|
|
1938
|
+
* query has more than one operation (a relation chain); a single-operation read
|
|
1939
|
+
* needs no plan. See {@link ExecutionPlanSpec} for the derivation and the
|
|
1940
|
+
* backward-compatible (plan-absent → sequential) fallback.
|
|
1941
|
+
*/
|
|
1942
|
+
readonly executionPlan?: ExecutionPlanSpec;
|
|
1943
|
+
}
|
|
1944
|
+
type WriteOperationType = 'PutItem' | 'UpdateItem' | 'DeleteItem';
|
|
1945
|
+
/**
|
|
1946
|
+
* A condition expression on a write, as a JSON-serializable spec (issues #46,
|
|
1947
|
+
* #81). The supported subset:
|
|
1948
|
+
*
|
|
1949
|
+
* - `{ kind: 'notExists' }` → `attribute_not_exists(PK)` (legacy whole-row guard).
|
|
1950
|
+
* - `{ kind: 'attributeExists'; field }` → `attribute_exists(<field>)` — the named
|
|
1951
|
+
* attribute (any field, incl. PK/SK) must be present. The foundation for
|
|
1952
|
+
* referential-integrity derivation.
|
|
1953
|
+
* - `{ kind: 'attributeNotExists'; field }` → `attribute_not_exists(<field>)` — the
|
|
1954
|
+
* named attribute must be absent (field-level uniqueness / first-write guard).
|
|
1955
|
+
* - `{ kind: 'equals'; fields }` → `#f = :v AND …` field equality (each value a
|
|
1956
|
+
* `{param}` / literal template).
|
|
1957
|
+
*/
|
|
1958
|
+
type ConditionSpec = {
|
|
1959
|
+
readonly kind: 'notExists';
|
|
1960
|
+
} | {
|
|
1961
|
+
readonly kind: 'attributeExists';
|
|
1962
|
+
readonly field: string;
|
|
1963
|
+
} | {
|
|
1964
|
+
readonly kind: 'attributeNotExists';
|
|
1965
|
+
readonly field: string;
|
|
1966
|
+
} | {
|
|
1967
|
+
readonly kind: 'equals';
|
|
1968
|
+
readonly fields: Readonly<Record<string, string>>;
|
|
1969
|
+
};
|
|
1970
|
+
interface CommandSpec {
|
|
1971
|
+
readonly type: WriteOperationType;
|
|
1972
|
+
readonly tableName: string;
|
|
1973
|
+
readonly entity: string;
|
|
1974
|
+
readonly params: Readonly<Record<string, ParamSpec>>;
|
|
1975
|
+
/**
|
|
1976
|
+
* Key condition for `UpdateItem` / `DeleteItem` (attribute name → template).
|
|
1977
|
+
* Absent for `PutItem` (the whole item is built from `item`).
|
|
1978
|
+
*/
|
|
1979
|
+
readonly keyCondition?: Readonly<Record<string, string>>;
|
|
1980
|
+
/** The full item template for `PutItem` (field name → template / literal). */
|
|
1981
|
+
readonly item?: Readonly<Record<string, string>>;
|
|
1982
|
+
/** Field-level changes for `UpdateItem` (field name → template / literal). */
|
|
1983
|
+
readonly changes?: Readonly<Record<string, string>>;
|
|
1984
|
+
/** Optional write condition (subset). */
|
|
1985
|
+
readonly condition?: ConditionSpec;
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* A declarative `when` guard on a transaction item (issue #46). Compares a
|
|
1989
|
+
* templated left-hand value against a templated right-hand value; the item is
|
|
1990
|
+
* skipped when the comparison does not hold. Both values are template strings
|
|
1991
|
+
* (`{param}` / `{item.<field>}` / literal), resolved by the runtime before the
|
|
1992
|
+
* comparison. The runtime compares the resolved **string** values.
|
|
1993
|
+
*/
|
|
1994
|
+
interface WhenSpec {
|
|
1995
|
+
readonly op: 'eq' | 'ne';
|
|
1996
|
+
/** Templated left-hand value (typically an `{item.<field>}` or `{param}`). */
|
|
1997
|
+
readonly left: string;
|
|
1998
|
+
/** Templated right-hand value (literal or another reference). */
|
|
1999
|
+
readonly right: string;
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* The kind of a transaction item. The three write kinds (`Put` / `Update` /
|
|
2003
|
+
* `Delete`) mutate an item; `ConditionCheck` (issue #81) is a **read-only
|
|
2004
|
+
* assertion** on another item — it asserts a {@link ConditionSpec} holds for the
|
|
2005
|
+
* keyed row without modifying it, and its failure cancels the **whole**
|
|
2006
|
+
* `TransactWriteItems` atomically. It is the foundation for referential-integrity
|
|
2007
|
+
* derivation (proposal: `requires <Entity> exists` → a `ConditionCheck` with
|
|
2008
|
+
* `attribute_exists`).
|
|
2009
|
+
*/
|
|
2010
|
+
type TransactionItemType = 'Put' | 'Update' | 'Delete' | 'ConditionCheck';
|
|
2011
|
+
/**
|
|
2012
|
+
* A single templated item in a transaction. When `forEach` is present, the item
|
|
2013
|
+
* is expanded **once per element** of the named array param, with each element's
|
|
2014
|
+
* fields bound to the item's `{item.<field>}` placeholders.
|
|
2015
|
+
*
|
|
2016
|
+
* Field presence by `type`:
|
|
2017
|
+
*
|
|
2018
|
+
* | type | item | keyCondition | changes | add | condition |
|
|
2019
|
+
* |----------------|------|--------------|---------|-----|--------------------------|
|
|
2020
|
+
* | `Put` | ✓ | — | — | — | optional |
|
|
2021
|
+
* | `Update` | — | ✓ | ✓ / — | ✓ / — | optional |
|
|
2022
|
+
* | `Delete` | — | ✓ | — | — | optional |
|
|
2023
|
+
* | `ConditionCheck` | — | ✓ | — | — | **required** (the assert) |
|
|
2024
|
+
*
|
|
2025
|
+
* An `Update` carries `changes` (a `SET` of named fields) and/or `add` (an atomic
|
|
2026
|
+
* `ADD` of named numeric fields, issue #85) — at least one of the two. The two are
|
|
2027
|
+
* distinct DynamoDB update actions: `SET #f = :v` **overwrites**, while
|
|
2028
|
+
* `ADD #f :delta` **atomically increments** without a read (concurrency-safe), so
|
|
2029
|
+
* a derived counter (`User.postCount += 1`) MUST be an `add`, never a `changes`
|
|
2030
|
+
* `SET` (which would clobber a concurrent increment).
|
|
2031
|
+
*/
|
|
2032
|
+
interface TransactionItemSpec {
|
|
2033
|
+
readonly type: TransactionItemType;
|
|
2034
|
+
readonly tableName: string;
|
|
2035
|
+
readonly entity: string;
|
|
2036
|
+
/** Put: the full item template (field → template / literal). */
|
|
2037
|
+
readonly item?: Readonly<Record<string, string>>;
|
|
2038
|
+
/** Update / Delete / ConditionCheck: the key template (attribute → template). */
|
|
2039
|
+
readonly keyCondition?: Readonly<Record<string, string>>;
|
|
2040
|
+
/** Update: field-level `SET` change templates (overwrite). */
|
|
2041
|
+
readonly changes?: Readonly<Record<string, string>>;
|
|
2042
|
+
/**
|
|
2043
|
+
* Update: field-level atomic `ADD` deltas (issue #85, derived counters). Each
|
|
2044
|
+
* value is a numeric template (`{param}` / a literal number string); the runtime
|
|
2045
|
+
* applies `ADD #field :delta`, an atomic increment that needs no prior read and
|
|
2046
|
+
* is safe under concurrency. A negative delta decrements (e.g. `-1` on a remove).
|
|
2047
|
+
*/
|
|
2048
|
+
readonly add?: Readonly<Record<string, string>>;
|
|
2049
|
+
/**
|
|
2050
|
+
* The write / assertion condition (subset). Optional on `Put` / `Update` /
|
|
2051
|
+
* `Delete`; **required** on a `ConditionCheck` (the assertion it makes).
|
|
2052
|
+
*/
|
|
2053
|
+
readonly condition?: ConditionSpec;
|
|
2054
|
+
/** Optional declarative guard; the item is skipped when it does not hold. */
|
|
2055
|
+
readonly when?: WhenSpec;
|
|
2056
|
+
/** When present, expand this item once per element of the named array param. */
|
|
2057
|
+
readonly forEach?: {
|
|
2058
|
+
readonly source: string;
|
|
2059
|
+
};
|
|
2060
|
+
/**
|
|
2061
|
+
* Marks a **raw marker-row** write (issue #86 uniqueness guard): the row is NOT a
|
|
2062
|
+
* modeled entity, so its primary key is carried **literally** rather than derived
|
|
2063
|
+
* from manifest metadata. When `true`:
|
|
2064
|
+
*
|
|
2065
|
+
* - a `Put` writes its `item` record **verbatim** as the stored item — the `item`
|
|
2066
|
+
* already carries the synthetic `PK` / `SK` templates (`UNIQUE#…`), so the
|
|
2067
|
+
* runtimes do NOT prepend a model prefix or compute GSI attributes;
|
|
2068
|
+
* - a `Delete` uses its `keyCondition` (the `PK` / `SK` templates) as the row Key
|
|
2069
|
+
* verbatim.
|
|
2070
|
+
*
|
|
2071
|
+
* The companion {@link entity} is the {@link MARKER_ROW_ENTITY} sentinel (there is
|
|
2072
|
+
* no manifest entity to resolve). Absent / `false` on every modeled write (a base
|
|
2073
|
+
* entity / edge / counter / ConditionCheck item), so those paths are unchanged.
|
|
2074
|
+
*/
|
|
2075
|
+
readonly literalKey?: boolean;
|
|
2076
|
+
}
|
|
2077
|
+
/** A declarative transaction definition's full execution spec. */
|
|
2078
|
+
interface TransactionSpec {
|
|
2079
|
+
readonly params: Readonly<Record<string, ParamSpec>>;
|
|
2080
|
+
readonly items: readonly TransactionItemSpec[];
|
|
2081
|
+
/**
|
|
2082
|
+
* A static upper bound on the expanded item count when computable (no `forEach`
|
|
2083
|
+
* present → the exact count; with `forEach` it is left absent because the
|
|
2084
|
+
* element count is only known at execution — the runtime then enforces the
|
|
2085
|
+
* DynamoDB ≤25 limit after expansion).
|
|
2086
|
+
*/
|
|
2087
|
+
readonly maxItems?: number;
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Whether a contract is a public **read** (`'query'`) or **write** (`'command'`)
|
|
2091
|
+
* interface. Mirrors {@link QueryModelContract} / {@link CommandModelContract}'s
|
|
2092
|
+
* `kind` discriminant from the Contract IR (#58).
|
|
2093
|
+
*/
|
|
2094
|
+
type ContractKind = 'query' | 'command';
|
|
2095
|
+
/**
|
|
2096
|
+
* The resolution kind of a contract query method, **derived** by the planner from
|
|
2097
|
+
* the internal op (never hand-written; see proposal "N+1 Safety"):
|
|
2098
|
+
*
|
|
2099
|
+
* - `'point'` — target keys are known (unique-key `query` / `GetItem`); a key
|
|
2100
|
+
* array coalesces to one `BatchGetItem`.
|
|
2101
|
+
* - `'range'` — target key set is unknown (partition `list` / `Query`); one
|
|
2102
|
+
* request per partition key, so it is only ever safe for a single key.
|
|
2103
|
+
*/
|
|
2104
|
+
type ContractResolution = 'point' | 'range';
|
|
2105
|
+
/**
|
|
2106
|
+
* What input arity a contract method accepts, **derived** from its resolution:
|
|
2107
|
+
*
|
|
2108
|
+
* - `'either'` — a single key **or** an array (a `point` read / a known-key
|
|
2109
|
+
* write; the array form is one `BatchGetItem` / batched write).
|
|
2110
|
+
* - `'single'` — a single key only (a `range` read; an array would be an N+1
|
|
2111
|
+
* fan-out and is rejected by construction).
|
|
2112
|
+
* - `'array'` — an array only (reserved; not produced by the current resolvers).
|
|
2113
|
+
*/
|
|
2114
|
+
type ContractInputArity = 'single' | 'array' | 'either';
|
|
2115
|
+
/**
|
|
2116
|
+
* The per-key result cardinality of a contract query method, **derived** from the
|
|
2117
|
+
* internal op kind (proposal "Cardinality matrix"):
|
|
2118
|
+
*
|
|
2119
|
+
* - `'one'` — at most one item per key (a `point` read).
|
|
2120
|
+
* - `'many'` — a connection (`{ items, cursor }`) per key (a `range` read).
|
|
2121
|
+
*
|
|
2122
|
+
* This is the per-key shape; the input arity (single vs. array) then decides
|
|
2123
|
+
* whether the overall result is bare or keyed.
|
|
2124
|
+
*/
|
|
2125
|
+
type ContractCardinality = 'one' | 'many';
|
|
2126
|
+
/**
|
|
2127
|
+
* The category of a contract command method's declared result, part of the
|
|
2128
|
+
* contract (it surfaces in OpenAPI and every binding; proposal "Return values"):
|
|
2129
|
+
*
|
|
2130
|
+
* - `'void'` — fire-and-forget write (no body).
|
|
2131
|
+
* - `'result'` — an outcome / status object (e.g. `{ ok, version }`).
|
|
2132
|
+
* - `'entity'` — the updated entity (the post-write projection).
|
|
2133
|
+
*/
|
|
2134
|
+
type ContractCommandResult = 'void' | 'result' | 'entity';
|
|
2135
|
+
/** The Key of a contract: the field names that compose its access / join key. */
|
|
2136
|
+
interface ContractKeySpec {
|
|
2137
|
+
/** The key field names, in declaration order (e.g. `["articleId"]`). */
|
|
2138
|
+
readonly fields: readonly string[];
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* One External Query (Query Composition) binding on a contract query method
|
|
2142
|
+
* (proposal "External Query"). It is a **build-time, in-process** read dependency
|
|
2143
|
+
* on another contract referenced by name in the same SSoT — there is no protocol
|
|
2144
|
+
* or transport. The runtime collects every parent key produced at this level and
|
|
2145
|
+
* resolves the referenced contract **once, batched**, for all of them.
|
|
2146
|
+
*
|
|
2147
|
+
* A composed child MUST be `point` (proposal "N+1 Safety"): the parent step may
|
|
2148
|
+
* yield N records, so a `range` child would be an N+1 fan-out.
|
|
2149
|
+
*/
|
|
2150
|
+
interface ComposeSpec {
|
|
2151
|
+
/** The result property the composed value is attached to (e.g. `billing`). */
|
|
2152
|
+
readonly as: string;
|
|
2153
|
+
/** The referenced contract name (a symbol in the same SSoT). */
|
|
2154
|
+
readonly contract: string;
|
|
2155
|
+
/** The referenced method on that contract (e.g. `get`). */
|
|
2156
|
+
readonly method: string;
|
|
2157
|
+
/**
|
|
2158
|
+
* An optional **logical** owner label (the bounded context that owns the
|
|
2159
|
+
* referenced contract) — not a network address. Omitted when unlabeled.
|
|
2160
|
+
*/
|
|
2161
|
+
readonly context?: string;
|
|
2162
|
+
/**
|
|
2163
|
+
* The child-key binding: child key field → a `from` source path on the parent
|
|
2164
|
+
* result, e.g. `{ accountId: "$.billingAccountId" }`.
|
|
2165
|
+
*/
|
|
2166
|
+
readonly bind: Readonly<Record<string, string>>;
|
|
2167
|
+
/**
|
|
2168
|
+
* The composed child's resolution. It MUST be `'point'` in a valid contract — a
|
|
2169
|
+
* composed child coalesces to one `BatchGetItem` across all parent keys, so a
|
|
2170
|
+
* `range` child (a partition `Query` that cannot be batched across the parent's
|
|
2171
|
+
* keys) is an N+1 fan-out. The N+1 static checker (#60,
|
|
2172
|
+
* {@link assertContractN1Safe}) rejects a `'range'` value on the build path; the
|
|
2173
|
+
* field is widened to {@link ContractResolution} so the offending node can be
|
|
2174
|
+
* carried into the assembled {@link ContractSpec} for the checker to inspect and
|
|
2175
|
+
* reject with a clear message, rather than being silently unrepresentable.
|
|
2176
|
+
*/
|
|
2177
|
+
readonly resolution: ContractResolution;
|
|
2178
|
+
/** The composed child's per-key cardinality (`'one'` for a valid `point`). */
|
|
2179
|
+
readonly cardinality: ContractCardinality;
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* The **composition execution plan** for one query contract method (issue #70a),
|
|
2183
|
+
* the contract-layer mirror of {@link ExecutionPlanSpec}. It records how a method's
|
|
2184
|
+
* External Query compositions (`compose[]`) are staged relative to the parent read
|
|
2185
|
+
* and to one another, and the batching discipline each composed child obeys:
|
|
2186
|
+
*
|
|
2187
|
+
* - `stages` is an ordered list whose entries are indices into the method's
|
|
2188
|
+
* `compose[]`. The parent read (the referenced `operation`) is the implicit
|
|
2189
|
+
* stage before all of these. Every compose child binds its child key purely from
|
|
2190
|
+
* the **parent** result (`from('$.…')` paths), so sibling compositions are
|
|
2191
|
+
* mutually independent: they form a single stage `[0, 1, …]` and may be resolved
|
|
2192
|
+
* concurrently. (A composed child cannot, today, bind from another composed
|
|
2193
|
+
* child — the #63 DSL only exposes parent `from` paths — so there is never a
|
|
2194
|
+
* cross-composition dependency; the field is a list of stages so the shape can
|
|
2195
|
+
* carry one if a future primitive introduces it.)
|
|
2196
|
+
* - `concurrency` is the declared in-flight bound applied **within** a stage (the
|
|
2197
|
+
* shared {@link RELATION_TRAVERSAL_CONCURRENCY} = 16).
|
|
2198
|
+
* - `batchChunkSize` is the per-request key cap for a composed child read (100,
|
|
2199
|
+
* the DynamoDB `BatchGetItem` limit): a composition resolves **one batched
|
|
2200
|
+
* call** across all parent records (chunked at this size), never an N+1 fan-out
|
|
2201
|
+
* (the composed child is `point`, enforced by the #60 N+1 checker).
|
|
2202
|
+
*
|
|
2203
|
+
* Emitted only when the method declares at least one composition. A method without
|
|
2204
|
+
* compositions carries no plan, and a runtime without one resolves any
|
|
2205
|
+
* compositions sequentially (the pre-#70 behavior) — so an absent plan never
|
|
2206
|
+
* changes results.
|
|
2207
|
+
*/
|
|
2208
|
+
interface CompositionPlanSpec {
|
|
2209
|
+
/** Ordered stages of indices into `compose[]` (independent siblings share a stage). */
|
|
2210
|
+
readonly stages: readonly (readonly number[])[];
|
|
2211
|
+
/** The declared in-flight bound applied within each composition stage (16). */
|
|
2212
|
+
readonly concurrency: number;
|
|
2213
|
+
/** Per-request key cap for a composed child's batched read (100, BatchGetItem limit). */
|
|
2214
|
+
readonly batchChunkSize: number;
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* The serialized spec of one **query** contract method. Carries the decided facts
|
|
2218
|
+
* (`resolution` / `inputArity` / `cardinality`) the planner derived in #58, a
|
|
2219
|
+
* reference into `queries` by name (the internal op), and any External Query
|
|
2220
|
+
* compositions. The decided facts are **serialized, not re-derived** — every
|
|
2221
|
+
* runtime honors them.
|
|
2222
|
+
*/
|
|
2223
|
+
interface QueryContractMethodSpec {
|
|
2224
|
+
/** Derived: `'point'` (unique-key query) or `'range'` (partition list). */
|
|
2225
|
+
readonly resolution: ContractResolution;
|
|
2226
|
+
/** Derived: `'either'` for `point`, `'single'` for `range`. */
|
|
2227
|
+
readonly inputArity: ContractInputArity;
|
|
2228
|
+
/** Derived per-key result shape: `'one'` (point) or `'many'` (range). */
|
|
2229
|
+
readonly cardinality: ContractCardinality;
|
|
2230
|
+
/** The referenced read op name in `queries` (e.g. `ArticleById__get`). */
|
|
2231
|
+
readonly operation: string;
|
|
2232
|
+
/** External Query compositions on this method; omitted when there are none. */
|
|
2233
|
+
readonly compose?: readonly ComposeSpec[];
|
|
2234
|
+
/**
|
|
2235
|
+
* The composition staging + batch plan (issue #70a); present only when `compose`
|
|
2236
|
+
* is. See {@link CompositionPlanSpec}: independent compositions form one
|
|
2237
|
+
* concurrency-eligible stage after the parent read, each resolved as one batched
|
|
2238
|
+
* call (chunk ≤100). Absent → resolve compositions sequentially (pre-#70).
|
|
2239
|
+
*/
|
|
2240
|
+
readonly compositionPlan?: CompositionPlanSpec;
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* How a contract command method resolves a single key vs. an array of keys to the
|
|
2244
|
+
* underlying write surface (proposal "Consistency with the existing write
|
|
2245
|
+
* surface"). A single key → one write op (or one transaction); an array → a
|
|
2246
|
+
* batched write — a `TransactWriteItems` when atomicity / per-item conditions are
|
|
2247
|
+
* required, or a `BatchWriteItem` when neither is.
|
|
2248
|
+
*/
|
|
2249
|
+
type CommandResolutionTarget =
|
|
2250
|
+
/** One write op, referenced by name in `commands`. */
|
|
2251
|
+
{
|
|
2252
|
+
readonly mode: 'op';
|
|
2253
|
+
readonly operation: string;
|
|
2254
|
+
}
|
|
2255
|
+
/** One transaction, referenced by name in `transactions`. */
|
|
2256
|
+
| {
|
|
2257
|
+
readonly mode: 'transaction';
|
|
2258
|
+
readonly transaction: string;
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* A non-atomic **parallel** fan-out over the per-key write op (#101). The op
|
|
2262
|
+
* MAY carry a condition (guarded create / conditioned update). Unconditioned
|
|
2263
|
+
* `put`/`delete` ops coalesce into a `BatchWriteItem` (with `UnprocessedItems`
|
|
2264
|
+
* retry); conditioned/update ops issue individual conditional writes
|
|
2265
|
+
* concurrently. Per-op success/failure is reported (partial success); a
|
|
2266
|
+
* per-op failure never aborts the others. References the per-key write op in
|
|
2267
|
+
* `commands`.
|
|
2268
|
+
*/
|
|
2269
|
+
| {
|
|
2270
|
+
readonly mode: 'parallel';
|
|
2271
|
+
readonly operation: string;
|
|
2272
|
+
};
|
|
2273
|
+
/**
|
|
2274
|
+
* The serialized spec of one **command** contract method. Symmetric to
|
|
2275
|
+
* {@link QueryContractMethodSpec}: it carries the declared `result` type and the
|
|
2276
|
+
* `single` / `batch` resolution targets into the existing write specs.
|
|
2277
|
+
*/
|
|
2278
|
+
interface CommandContractMethodSpec {
|
|
2279
|
+
/** Derived input arity: writes accept `'either'` (single key or key array). */
|
|
2280
|
+
readonly inputArity: ContractInputArity;
|
|
2281
|
+
/** The declared result type (`void` | a `Result` | the updated entity). */
|
|
2282
|
+
readonly result: ContractCommandResult;
|
|
2283
|
+
/** How a single key resolves (one op / one transaction). */
|
|
2284
|
+
readonly single: CommandResolutionTarget;
|
|
2285
|
+
/**
|
|
2286
|
+
* How an array of keys resolves (a transaction / `BatchWriteItem`); omitted
|
|
2287
|
+
* when the contract does not declare a batched write form.
|
|
2288
|
+
*/
|
|
2289
|
+
readonly batch?: CommandResolutionTarget;
|
|
2290
|
+
/**
|
|
2291
|
+
* The **return projection** of a `mutation`-derived command method (issue #83;
|
|
2292
|
+
* proposal §3, "return selection as a read projection"). A JSON-safe boolean
|
|
2293
|
+
* field map: after the write commits, the runtime issues a **`GetItem` with
|
|
2294
|
+
* `ConsistentRead`** of the written entity's primary key and applies this
|
|
2295
|
+
* projection via the existing read-projection machinery, returning the projected
|
|
2296
|
+
* item. This is the **uniform** return mechanism for both a single-op command
|
|
2297
|
+
* and a future transaction (a `TransactWriteItems` cannot return item images),
|
|
2298
|
+
* so TS and Python return an **identical** projected item.
|
|
2299
|
+
*
|
|
2300
|
+
* The consistent read-back is issued against the read op named
|
|
2301
|
+
* `<Contract>__<method>__readback` (a synthesized {@link QuerySpec}: a `GetItem`
|
|
2302
|
+
* on the written entity's primary key projecting these fields). Present only on
|
|
2303
|
+
* a `.plan(mutation)` method; a hand-written #64 command omits it (its
|
|
2304
|
+
* {@link result} stays `'void'` / `'entity'` and it returns no projected item).
|
|
2305
|
+
*/
|
|
2306
|
+
readonly returnSelection?: Readonly<Record<string, boolean>>;
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* A serialized contract — a keyed public read (`'query'`) or write (`'command'`)
|
|
2310
|
+
* interface holding one or more named methods. Each method references an existing
|
|
2311
|
+
* operation spec by name and adds the decided contract facts. The existing
|
|
2312
|
+
* operation-spec shapes are unchanged, so the current runtime keeps working.
|
|
2313
|
+
*/
|
|
2314
|
+
interface ContractSpec {
|
|
2315
|
+
/** Whether this is a read (`'query'`) or write (`'command'`) contract. */
|
|
2316
|
+
readonly kind: ContractKind;
|
|
2317
|
+
/** The contract Key (the access pattern / join / batch key). */
|
|
2318
|
+
readonly key: ContractKeySpec;
|
|
2319
|
+
/**
|
|
2320
|
+
* The named methods (use cases over the same Key). A query contract's methods
|
|
2321
|
+
* are {@link QueryContractMethodSpec}; a command contract's are
|
|
2322
|
+
* {@link CommandContractMethodSpec}. The map is keyed by contract `kind`.
|
|
2323
|
+
*/
|
|
2324
|
+
readonly methods: Readonly<Record<string, QueryContractMethodSpec>> | Readonly<Record<string, CommandContractMethodSpec>>;
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* One bounded context's membership (issue #59, "context ownership"). Lists the
|
|
2328
|
+
* Models and Contracts that belong to the same context, so the future boundary
|
|
2329
|
+
* lint (#61) can tell **own** from **foreign**: a contract may resolve its own
|
|
2330
|
+
* context's Models directly, but may only reach another context through that
|
|
2331
|
+
* context's published Contract (direct use of a foreign Model is a build error).
|
|
2332
|
+
*
|
|
2333
|
+
* This issue only **serializes / emits** this declaration; it does not enforce
|
|
2334
|
+
* it. Both lists are entity / contract *names* (matching `manifest.entities` keys
|
|
2335
|
+
* and `contracts` keys respectively).
|
|
2336
|
+
*/
|
|
2337
|
+
interface ContextSpec {
|
|
2338
|
+
/** Model (entity) names owned by this context (sorted; manifest entity keys). */
|
|
2339
|
+
readonly models: readonly string[];
|
|
2340
|
+
/** Contract names owned by this context (sorted; `contracts` map keys). */
|
|
2341
|
+
readonly contracts: readonly string[];
|
|
2342
|
+
}
|
|
2343
|
+
interface OperationsDocument {
|
|
2344
|
+
readonly version: typeof SPEC_VERSION;
|
|
2345
|
+
readonly queries: Readonly<Record<string, QuerySpec>>;
|
|
2346
|
+
readonly commands: Readonly<Record<string, CommandSpec>>;
|
|
2347
|
+
/** Declarative transactions (issue #46). Absent in pre-#46 specs. */
|
|
2348
|
+
readonly transactions?: Readonly<Record<string, TransactionSpec>>;
|
|
2349
|
+
/**
|
|
2350
|
+
* The CQRS Contract layer (issue #59): contract name → {@link ContractSpec}.
|
|
2351
|
+
* Layered **on top of** the existing `queries` / `commands` / `transactions`
|
|
2352
|
+
* (each method references one of them by name). **Absent** when the input
|
|
2353
|
+
* defines no contracts — so a contract-free input produces a byte-identical
|
|
2354
|
+
* pre-#59 operations document (backward compatibility).
|
|
2355
|
+
*/
|
|
2356
|
+
readonly contracts?: Readonly<Record<string, ContractSpec>>;
|
|
2357
|
+
/**
|
|
2358
|
+
* Context-ownership declarations (issue #59): context name →
|
|
2359
|
+
* {@link ContextSpec}. Used by the boundary lint (#61) to tell own from
|
|
2360
|
+
* foreign. **Absent** when no contexts are declared (backward compatibility).
|
|
2361
|
+
*/
|
|
2362
|
+
readonly contexts?: Readonly<Record<string, ContextSpec>>;
|
|
2363
|
+
}
|
|
2364
|
+
/** The full `{ manifest, operations }` bundle produced by the static planner. */
|
|
2365
|
+
interface BridgeBundle {
|
|
2366
|
+
readonly manifest: Manifest;
|
|
2367
|
+
readonly operations: OperationsDocument;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/**
|
|
2371
|
+
* Single-fragment mutation → command-op compiler (issue #83, Epic #80; proposal
|
|
2372
|
+
* `docs/mutation-command-derivation.md` §3).
|
|
2373
|
+
*
|
|
2374
|
+
* A {@link CommandPlan} (a `mutation(...)` result) is the internal IR: a **list**
|
|
2375
|
+
* of write fragments to compose atomically. This module compiles the
|
|
2376
|
+
* **single-fragment** case end-to-end into the **same** {@link ContractMethodOp}
|
|
2377
|
+
* the #58 contract recorder produces, so the existing serializer
|
|
2378
|
+
* (`opToDefinition` → `buildCommandSpec`, `src/spec/contracts.ts` /
|
|
2379
|
+
* `src/spec/operations.ts`) and the existing runtimes consume it with no new
|
|
2380
|
+
* write substrate. The IR is already a list, so the **N-fragment atomic merge is
|
|
2381
|
+
* #90** — this module rejects N>1 with a clear pointer rather than half-merging.
|
|
2382
|
+
*
|
|
2383
|
+
* ## Intent → base op (proposal §3, "single fragment compile")
|
|
2384
|
+
*
|
|
2385
|
+
* | intent | base op |
|
|
2386
|
+
* |----------|-------------------------------------------|
|
|
2387
|
+
* | `create` | `PutItem` (`attribute_not_exists(PK)`) |
|
|
2388
|
+
* | `update` | `UpdateItem` (key + changed fields) |
|
|
2389
|
+
* | `remove` | `DeleteItem` (key) |
|
|
2390
|
+
*
|
|
2391
|
+
* ## `use:` resolution (the resolved fork — proposal §3, pinned spec correction)
|
|
2392
|
+
*
|
|
2393
|
+
* The fragment's **intent** selects the lifecycle; `use:` is **optional**:
|
|
2394
|
+
*
|
|
2395
|
+
* - `use:` present → adopt that {@link EntityWritesDefinition} (the **whole**
|
|
2396
|
+
* `writes` set; never a single lifecycle).
|
|
2397
|
+
* - `use:` omitted → default to the **target model's own** `entityWrites` save
|
|
2398
|
+
* contract ({@link getEntityWrites}).
|
|
2399
|
+
* - the resolved save contract (custom or default) has **no** entry for the
|
|
2400
|
+
* intent's lifecycle, **or the target declares no `writes` at all** → the
|
|
2401
|
+
* **trivial base op** for the intent (create → `Put` `attribute_not_exists(PK)`,
|
|
2402
|
+
* update → `Update`, remove → `Delete`) against the plain model. So
|
|
2403
|
+
* `{ create: () => PostModel, key, input }` works on a bare model.
|
|
2404
|
+
*
|
|
2405
|
+
* Either way #83 emits the **same** base op — the resolved
|
|
2406
|
+
* {@link LifecycleContract} only matters as the **per-fragment hook** the
|
|
2407
|
+
* effect-derivation issues (#84–#87) read: the resolved lifecycle's
|
|
2408
|
+
* {@link LifecycleContract.effects} are exposed on {@link CompiledFragment} and
|
|
2409
|
+
* consumed by NONE of #83 (the empty hook).
|
|
2410
|
+
*/
|
|
2411
|
+
|
|
2412
|
+
/**
|
|
2413
|
+
* A **referential-integrity assertion** derived from a `requires`
|
|
2414
|
+
* ({@link RequiresEffect}) effect (issue #84; proposal §2/§3). It is the
|
|
2415
|
+
* realization of `w.exists(() => Entity, { <entityKeyField>: '$.input.<field>' })`:
|
|
2416
|
+
* the referenced entity must already exist for the bound keys, asserted by a
|
|
2417
|
+
* read-only `ConditionCheck` (`attribute_exists(PK)`) item composed into the
|
|
2418
|
+
* mutation's atomic `TransactWriteItems`. A failed assertion cancels the WHOLE
|
|
2419
|
+
* transaction, so the entity / edges / derived updates / events that the same
|
|
2420
|
+
* transaction would write never persist (referential integrity is enforced
|
|
2421
|
+
* atomically, not by a pre-read).
|
|
2422
|
+
*
|
|
2423
|
+
* The shape is **runtime-neutral** — it carries the structural binding (target
|
|
2424
|
+
* entity + the target key fields resolved from the mutation input) that BOTH the
|
|
2425
|
+
* spec serializer (→ a {@link import('./types.js').TransactionItemSpec} with
|
|
2426
|
+
* templated `keyCondition`, for the Python bridge) and the in-process TS runtime
|
|
2427
|
+
* (→ a `ConditionCheck` `TransactWriteItems` entry, keyed from `(key, params)`)
|
|
2428
|
+
* derive their concrete form from. The compiler resolves the key binding once,
|
|
2429
|
+
* here, so both runtimes assert the **same** keyed row from the same input.
|
|
2430
|
+
*/
|
|
2431
|
+
interface DerivedConditionCheck {
|
|
2432
|
+
/** The referenced entity whose existence is asserted (resolved target model). */
|
|
2433
|
+
readonly entity: EntityRef;
|
|
2434
|
+
/**
|
|
2435
|
+
* The asserted row's key binding: target primary-key **input field name** → the
|
|
2436
|
+
* **mutation input field** that supplies it (parsed from the effect's
|
|
2437
|
+
* `$.input.<field>` value). Every target primary-key field is bound (a partial
|
|
2438
|
+
* key cannot identify a row to assert on), so the runtimes build the referenced
|
|
2439
|
+
* entity's full primary key from the mutation params.
|
|
2440
|
+
*/
|
|
2441
|
+
readonly keyBinding: Readonly<Record<string, string>>;
|
|
2442
|
+
}
|
|
2443
|
+
/**
|
|
2444
|
+
* A **derived edge write** — the adjacency `Put` / `Delete` {@link TransactionItemSpec}
|
|
2445
|
+
* set that materializes a relation edge on DynamoDB (issue #85; proposal §2 `edges`),
|
|
2446
|
+
* derived from one `w.putEdge(() => Target, 'relation')` / `w.deleteEdge(...)` effect.
|
|
2447
|
+
*
|
|
2448
|
+
* The effect names a **relation** (`Target.relationProperty`) whose rows form the
|
|
2449
|
+
* edge; the **adjacency entity** — the entity whose row IS that edge — is the
|
|
2450
|
+
* relation's *target model* ({@link RelationMetadata.targetFactory}), resolved here
|
|
2451
|
+
* and round-trip-verified by the #82 primitive ({@link deriveEdgeWriteItemsFor}, with
|
|
2452
|
+
* the #85-tightened PK-partition guard). The resolved item set is lifecycle-distinguished
|
|
2453
|
+
* by the fragment's intent ({@link INTENT_EDGE_LIFECYCLE}):
|
|
2454
|
+
*
|
|
2455
|
+
* - `create` → `onCreate`: a single adjacency `Put` (the new edge row);
|
|
2456
|
+
* - `remove` → `onDelete`: a single adjacency `Delete` (the edge row keyed by PK/SK);
|
|
2457
|
+
* - `update` → `onUpdateKeyChange`: `Delete`(old edge, keyed in the `{old.*}`
|
|
2458
|
+
* namespace) + `Put`(new edge) — the edge **moves** atomically (spec correction).
|
|
2459
|
+
*
|
|
2460
|
+
* The items carry templated key/item fields (`{field}` / `{old.field}`); the
|
|
2461
|
+
* serializer composes them into the atomic `TransactWriteItems` and the in-process
|
|
2462
|
+
* runtime renders the same items from the shared `(key, params)`, so both runtimes
|
|
2463
|
+
* write the **same** adjacency row(s) from the same input.
|
|
2464
|
+
*/
|
|
2465
|
+
interface DerivedEdgeWrite {
|
|
2466
|
+
/** The adjacency entity (the relation's target — whose row is the edge). */
|
|
2467
|
+
readonly entity: EntityRef;
|
|
2468
|
+
/** The relation (`Target.relationProperty`) the edge materializes (documentary). */
|
|
2469
|
+
readonly relation: {
|
|
2470
|
+
readonly target: string;
|
|
2471
|
+
readonly property: string;
|
|
2472
|
+
};
|
|
2473
|
+
/** The lifecycle-distinguished adjacency item set (`Put` / `Delete`). */
|
|
2474
|
+
readonly items: readonly TransactionItemSpec[];
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* A **derived update** — an atomic `ADD` (`UpdateItem`) on a target row, derived
|
|
2478
|
+
* from one `w.increment(() => Entity, { key }, 'field', delta)` effect (issue #85;
|
|
2479
|
+
* proposal §2 `derive`). It realizes a cascading counter such as
|
|
2480
|
+
* `User.postCount += 1`: an atomic `ADD #field :delta` that needs no prior read and
|
|
2481
|
+
* is safe under concurrency (a `SET` would clobber a concurrent increment).
|
|
2482
|
+
*
|
|
2483
|
+
* The lifecycle decides the sign convention the *declaration* uses (the author
|
|
2484
|
+
* declares `+1` on `create`, `-1` on `remove`, and a `-1`-on-old + `+1`-on-new pair
|
|
2485
|
+
* on `update` / onUpdateKeyChange); the compiler stores the declared delta verbatim.
|
|
2486
|
+
* The target row's key binds from the mutation input — `$.input.<field>` (the new /
|
|
2487
|
+
* current value) or `$.old.<field>` (the pre-mutation value, for decrementing the
|
|
2488
|
+
* **old** parent on a key change; the {@link OLD_VALUE_NAMESPACE} convention).
|
|
2489
|
+
*/
|
|
2490
|
+
interface DerivedUpdate {
|
|
2491
|
+
/** The entity whose attribute is derive-updated (resolved target model). */
|
|
2492
|
+
readonly entity: EntityRef;
|
|
2493
|
+
/**
|
|
2494
|
+
* The target row's key binding: target primary-key **input field name** → the
|
|
2495
|
+
* source param that supplies it. A `$.input.<field>` source binds the param
|
|
2496
|
+
* `<field>`; a `$.old.<field>` source binds the param `old.<field>` (the
|
|
2497
|
+
* pre-mutation value). Every target primary-key field is bound.
|
|
2498
|
+
*/
|
|
2499
|
+
readonly keyBinding: Readonly<Record<string, string>>;
|
|
2500
|
+
/** The attribute to atomically `ADD` to. */
|
|
2501
|
+
readonly attribute: string;
|
|
2502
|
+
/** The signed delta to add (e.g. `+1` create, `-1` remove). */
|
|
2503
|
+
readonly amount: number;
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* A **derived uniqueness guard** — the marker-row `Put` (and, on a unique-field
|
|
2507
|
+
* change, the old-guard `Delete` + new-guard `Put` swap) that enforces an
|
|
2508
|
+
* application-level uniqueness invariant declared by `w.unique({ name, scope,
|
|
2509
|
+
* fields })` (issue #86; proposal §2 `unique`). It is the realization of a named,
|
|
2510
|
+
* composite-scope uniqueness constraint that a unique GSI cannot express
|
|
2511
|
+
* (e.g. *unique title per user*).
|
|
2512
|
+
*
|
|
2513
|
+
* ## The guard-row key (deterministic, built from `name` / `scope` / `fields`)
|
|
2514
|
+
*
|
|
2515
|
+
* The guard occupies a synthetic marker row whose primary key is built **purely**
|
|
2516
|
+
* from the constraint:
|
|
2517
|
+
*
|
|
2518
|
+
* - `PK` = `UNIQUE#<name>` followed by one `#{<scopeField>}` segment per `scope`
|
|
2519
|
+
* path (a composite scope yields several segments) — the **partition** the
|
|
2520
|
+
* uniqueness applies within, e.g. `UNIQUE#postTitlePerUser#{userId}`;
|
|
2521
|
+
* - `SK` = `VALUE#` followed by one `#{<field>}` segment per `fields` path (the
|
|
2522
|
+
* first carries no leading separator) — the **unique value** within that scope,
|
|
2523
|
+
* e.g. `VALUE#{title}`.
|
|
2524
|
+
*
|
|
2525
|
+
* Each `$.input.<field>` path contributes a `{<field>}` template token (rendered
|
|
2526
|
+
* from the mutation input). The shape is **deterministic** (same constraint →
|
|
2527
|
+
* same key), so a second write of the same scoped value targets the SAME row, and
|
|
2528
|
+
* the `attribute_not_exists` guard on the `Put` then fails — rolling back the
|
|
2529
|
+
* WHOLE transaction so the entity is never created.
|
|
2530
|
+
*
|
|
2531
|
+
* ## create vs. update-swap
|
|
2532
|
+
*
|
|
2533
|
+
* - **create / remove** — a single guard item: `create` → `Put` (claim the value)
|
|
2534
|
+
* guarded by `attribute_not_exists(PK)`; `remove` → `Delete` (release the value);
|
|
2535
|
+
* - **update changing a unique field** — `Delete`(OLD guard) + `Put`(NEW guard):
|
|
2536
|
+
* the old marker row (keyed in the `{old.*}` namespace, the {@link
|
|
2537
|
+
* OLD_VALUE_NAMESPACE} convention shared with the edge `onUpdateKeyChange` and
|
|
2538
|
+
* the derived-counter old-parent decrement) is released and the new value claimed
|
|
2539
|
+
* atomically, so the uniqueness moves with the field. The new `Put` keeps the
|
|
2540
|
+
* `attribute_not_exists` guard so the moved-to value cannot collide.
|
|
2541
|
+
*
|
|
2542
|
+
* The items are **runtime-neutral** {@link TransactionItemSpec}s carrying the
|
|
2543
|
+
* literal-key marker ({@link TransactionItemSpec.literalKey}) and the
|
|
2544
|
+
* {@link MARKER_ROW_ENTITY} sentinel (there is no manifest entity), so BOTH runtimes
|
|
2545
|
+
* write the SAME marker row(s) from the same input and the guard composes into the
|
|
2546
|
+
* entity's atomic `TransactWriteItems` (#90).
|
|
2547
|
+
*/
|
|
2548
|
+
interface DerivedUniqueGuard {
|
|
2549
|
+
/** The constraint name (its key-shape discriminator on DynamoDB). */
|
|
2550
|
+
readonly name: string;
|
|
2551
|
+
/** The guard's marker-row item set (`Put` / `Delete`), lifecycle-distinguished. */
|
|
2552
|
+
readonly items: readonly TransactionItemSpec[];
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* A **derived outbox event** — the transactional-outbox `Put` ({@link
|
|
2556
|
+
* TransactionItemSpec}) that records a domain event ATOMICALLY with the entity
|
|
2557
|
+
* write, derived from one `w.event(name, payload)` effect (issue #87; proposal §2
|
|
2558
|
+
* `emits`, §4 "delivery is outside"). The event row is written in the **same**
|
|
2559
|
+
* `TransactWriteItems` as the entity, so the event and the state it announces can
|
|
2560
|
+
* never diverge (no "wrote the row, lost the event" gap).
|
|
2561
|
+
*
|
|
2562
|
+
* ## The outbox-row key + shape (deterministic; drainable via `src/cdc/`)
|
|
2563
|
+
*
|
|
2564
|
+
* The row occupies a synthetic marker row in its own `OUTBOX#…` key-space:
|
|
2565
|
+
*
|
|
2566
|
+
* - `PK` = `OUTBOX#<eventName>` followed by one `#{<keyField>}` segment per
|
|
2567
|
+
* primary-key input field of the **writing entity** — so each entity emission has
|
|
2568
|
+
* its own deterministic outbox row, e.g. `OUTBOX#PostCreated#{postId}`;
|
|
2569
|
+
* - `SK` = `EVENT#` followed by the same per-key-field segments — a stable,
|
|
2570
|
+
* single-row-per-emission sort key;
|
|
2571
|
+
* - the item also stores `eventName` plus every declared `payload` field (each a
|
|
2572
|
+
* `{field}` template rendered from the shared method params), so the **`newImage`**
|
|
2573
|
+
* of the row carries the complete, typed event body.
|
|
2574
|
+
*
|
|
2575
|
+
* Because the row is a normal table item, the `src/cdc/` Streams substrate captures
|
|
2576
|
+
* its write as a {@link import('../cdc/types.js').ChangeEvent} (`eventName:
|
|
2577
|
+
* 'INSERT'`, `newImage` = this item). A consumer drains the outbox by filtering the
|
|
2578
|
+
* stream on `PK begins_with 'OUTBOX#'` and reading the event body off `newImage` —
|
|
2579
|
+
* **delivery / handlers / business reactions live OUTSIDE the mutation** (proposal
|
|
2580
|
+
* §4); #87 only writes the drainable RECORD.
|
|
2581
|
+
*/
|
|
2582
|
+
interface DerivedOutboxEvent {
|
|
2583
|
+
/** The event name (e.g. `PostCreated`). */
|
|
2584
|
+
readonly name: string;
|
|
2585
|
+
/** The outbox marker-row `Put` (one item; `literalKey`, `MARKER_ROW_ENTITY`). */
|
|
2586
|
+
readonly items: readonly TransactionItemSpec[];
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* A **derived idempotency guard** — the client-token guard `Put` ({@link
|
|
2590
|
+
* TransactionItemSpec}) that makes a same-token re-execution a NO-OP, derived from
|
|
2591
|
+
* the lifecycle's `w.idempotentBy(token)` effect (issue #87; proposal §2
|
|
2592
|
+
* `idempotency`). It is the transactional realization of "process this command at
|
|
2593
|
+
* most once for this client token".
|
|
2594
|
+
*
|
|
2595
|
+
* ## The guard-row key (deterministic, built from the token)
|
|
2596
|
+
*
|
|
2597
|
+
* The guard occupies a synthetic marker row keyed **purely** by the client token:
|
|
2598
|
+
*
|
|
2599
|
+
* - `PK` = `IDEMP#{<tokenField>}` — the `$.input.<field>` token (e.g.
|
|
2600
|
+
* `IDEMP#{requestId}`), rendered from the shared method params;
|
|
2601
|
+
* - `SK` = `TOKEN` (a fixed discriminator — one guard row per token).
|
|
2602
|
+
*
|
|
2603
|
+
* The `Put` is guarded by `attribute_not_exists(PK)`: the FIRST execution claims the
|
|
2604
|
+
* token row; a SECOND execution with the same token targets the SAME row and the
|
|
2605
|
+
* guard fails, cancelling the WHOLE `TransactWriteItems` — so the entity write, the
|
|
2606
|
+
* outbox event, and every other effect roll back and **no effect is double-applied**.
|
|
2607
|
+
* The shape is identical to the uniqueness guard's claim `Put` (the same `literalKey`
|
|
2608
|
+
* marker mechanism), so BOTH runtimes write / assert the SAME row from the same input.
|
|
2609
|
+
*/
|
|
2610
|
+
interface DerivedIdempotencyGuard {
|
|
2611
|
+
/** The mutation-input field the token binds from (documentary; surfaces in errors). */
|
|
2612
|
+
readonly tokenField: string;
|
|
2613
|
+
/** The guard marker-row `Put` (one item; `attribute_not_exists`, `literalKey`). */
|
|
2614
|
+
readonly items: readonly TransactionItemSpec[];
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* The result of compiling one fragment: the {@link ContractMethodOp} the existing
|
|
2618
|
+
* serializer / runtime consume, the resolved contract Key fields (the model's
|
|
2619
|
+
* primary-key input fields, also the consistent read-back key), and the **opaque
|
|
2620
|
+
* per-fragment hook** — the resolved lifecycle's §2 effect set (empty-consumed by
|
|
2621
|
+
* #83; read by #84–#87).
|
|
2622
|
+
*/
|
|
2623
|
+
interface CompiledFragment {
|
|
2624
|
+
/** The resolved write op (`put` / `update` / `delete`), serializer-ready. */
|
|
2625
|
+
readonly op: ContractMethodOp;
|
|
2626
|
+
/** The contract Key fields (the model primary-key input fields). */
|
|
2627
|
+
readonly keyFields: readonly string[];
|
|
2628
|
+
/**
|
|
2629
|
+
* The resolved lifecycle's §2 effect set, or `undefined` when the fragment
|
|
2630
|
+
* resolved to the trivial base op (no `writes` entry). **The per-fragment hook
|
|
2631
|
+
* for #84–#87** — #83 derives NO items from it. #84 reads the `requires` arm to
|
|
2632
|
+
* derive {@link conditionChecks}; the remaining arms (unique / edges / derive /
|
|
2633
|
+
* emits / idempotency) are still stored opaquely for #85–#87.
|
|
2634
|
+
*/
|
|
2635
|
+
readonly effects?: LifecycleEffects;
|
|
2636
|
+
/**
|
|
2637
|
+
* The referential-integrity assertions derived from the resolved lifecycle's
|
|
2638
|
+
* `requires` effects (issue #84): one {@link DerivedConditionCheck} per
|
|
2639
|
+
* `w.exists(...)`. Present (non-empty) only when the fragment's lifecycle
|
|
2640
|
+
* declares `requires`; absent otherwise (no regression — a fragment with no
|
|
2641
|
+
* `requires` compiles to exactly the #83/#90 base op). Each becomes a read-only
|
|
2642
|
+
* `ConditionCheck` (`attribute_exists`) item in the mutation's atomic
|
|
2643
|
+
* `TransactWriteItems`, and their presence **promotes** an otherwise single-op
|
|
2644
|
+
* mutation to a transaction (a `ConditionCheck` only exists inside a
|
|
2645
|
+
* `TransactWriteItems`).
|
|
2646
|
+
*/
|
|
2647
|
+
readonly conditionChecks?: readonly DerivedConditionCheck[];
|
|
2648
|
+
/**
|
|
2649
|
+
* The adjacency edge writes derived from the resolved lifecycle's `edges` effects
|
|
2650
|
+
* (issue #85): one {@link DerivedEdgeWrite} per `w.putEdge(...)` / `w.deleteEdge(...)`,
|
|
2651
|
+
* lifecycle-distinguished by the fragment intent (`create` → `Put`, `remove` →
|
|
2652
|
+
* `Delete`, `update` → `Delete(old) + Put(new)`). Present (non-empty) only when the
|
|
2653
|
+
* lifecycle declares `edges`; absent otherwise (no regression). Each derived item is
|
|
2654
|
+
* composed into the method's atomic `TransactWriteItems`.
|
|
2655
|
+
*/
|
|
2656
|
+
readonly edgeWrites?: readonly DerivedEdgeWrite[];
|
|
2657
|
+
/**
|
|
2658
|
+
* The derived (cascading) updates from the resolved lifecycle's `derive` effects
|
|
2659
|
+
* (issue #85): one {@link DerivedUpdate} per `w.increment(...)`, each an atomic
|
|
2660
|
+
* `ADD` on a target counter. Present (non-empty) only when the lifecycle declares
|
|
2661
|
+
* `derive`; absent otherwise (no regression). Each becomes an `UpdateItem` (`ADD`)
|
|
2662
|
+
* in the method's atomic `TransactWriteItems`.
|
|
2663
|
+
*/
|
|
2664
|
+
readonly derivedUpdates?: readonly DerivedUpdate[];
|
|
2665
|
+
/**
|
|
2666
|
+
* The uniqueness guards derived from the resolved lifecycle's `unique` effects
|
|
2667
|
+
* (issue #86): one {@link DerivedUniqueGuard} per `w.unique({ name, scope,
|
|
2668
|
+
* fields })`, lifecycle-distinguished by the fragment intent (`create` → a guarded
|
|
2669
|
+
* marker `Put`, `remove` → a marker `Delete`, `update` → `Delete(old) + Put(new)`
|
|
2670
|
+
* — the value swap). Present (non-empty) only when the lifecycle declares
|
|
2671
|
+
* `unique`; absent otherwise (no regression — a fragment with no `unique` compiles
|
|
2672
|
+
* to exactly the #83/#90 base op). Each guard's items compose into the method's
|
|
2673
|
+
* atomic `TransactWriteItems`; the guard `Put`'s `attribute_not_exists` makes a
|
|
2674
|
+
* duplicate roll the WHOLE transaction back, so the entity is never created.
|
|
2675
|
+
*/
|
|
2676
|
+
readonly uniqueGuards?: readonly DerivedUniqueGuard[];
|
|
2677
|
+
/**
|
|
2678
|
+
* The outbox events derived from the resolved lifecycle's `emits` effects (issue
|
|
2679
|
+
* #87): one {@link DerivedOutboxEvent} per `w.event(name, payload)`, each a
|
|
2680
|
+
* transactional-outbox `Put` recording the event ATOMICALLY with the entity write
|
|
2681
|
+
* (so event and state cannot diverge). Present (non-empty) only when the lifecycle
|
|
2682
|
+
* declares `emits`; absent otherwise (no regression — a fragment with no `emits`
|
|
2683
|
+
* compiles to exactly the #83/#90 base op). Each event's `Put` composes into the
|
|
2684
|
+
* method's atomic `TransactWriteItems`; its row is drainable via `src/cdc/`
|
|
2685
|
+
* (`PK begins_with 'OUTBOX#'`). Delivery / handlers are OUT of scope (proposal §4).
|
|
2686
|
+
*/
|
|
2687
|
+
readonly outboxEvents?: readonly DerivedOutboxEvent[];
|
|
2688
|
+
/**
|
|
2689
|
+
* The idempotency guard derived from the resolved lifecycle's `idempotency` effect
|
|
2690
|
+
* (issue #87): a single {@link DerivedIdempotencyGuard} (the `w.idempotentBy(token)`
|
|
2691
|
+
* client-token guard `Put`, `attribute_not_exists`). Present only when the lifecycle
|
|
2692
|
+
* declares `idempotency`; absent otherwise (no regression). Its `Put` composes into
|
|
2693
|
+
* the method's atomic `TransactWriteItems`; a same-token re-execution fails the
|
|
2694
|
+
* guard and rolls the WHOLE transaction back — so no effect is double-applied.
|
|
2695
|
+
*/
|
|
2696
|
+
readonly idempotencyGuard?: DerivedIdempotencyGuard;
|
|
2697
|
+
}
|
|
2698
|
+
/**
|
|
2699
|
+
* Resolve the {@link LifecycleContract} a fragment adopts for its intent:
|
|
2700
|
+
* `use:` (the whole save contract) when present, else the target model's own
|
|
2701
|
+
* `entityWrites`, then the lifecycle entry for the intent's phase. Returns
|
|
2702
|
+
* `undefined` (→ the trivial base op) when no save contract declares the phase or
|
|
2703
|
+
* the target declares no `writes`.
|
|
2704
|
+
*/
|
|
2705
|
+
declare function resolveLifecycle(fragment: MutationFragment): LifecycleContract | undefined;
|
|
2706
|
+
/**
|
|
2707
|
+
* A resolver for a {@link MutationEntityRef} (`$.entity[i].field`) — supplied by
|
|
2708
|
+
* the multi-fragment compiler ({@link compileMutationPlan}) so a later fragment's
|
|
2709
|
+
* leaf can be resolved to the **earlier** fragment's binding for that field. It
|
|
2710
|
+
* returns the already-rendered value the producer fragment stores at that field
|
|
2711
|
+
* (a {@link ContractParamRef} / {@link ContractKeyFieldRef} / literal), so the
|
|
2712
|
+
* cross-fragment value renders from the **same** shared input at execution time
|
|
2713
|
+
* (a `TransactWriteItems` cannot read another item mid-transaction). `null` for a
|
|
2714
|
+
* single-fragment compile, where a cross-fragment reference is meaningless.
|
|
2715
|
+
*/
|
|
2716
|
+
type EntityRefResolver = (ref: MutationEntityRef, consumerIndex: number, consumerField: string) => unknown;
|
|
2717
|
+
/**
|
|
2718
|
+
* Compile one fragment into a {@link CompiledFragment}. Builds the intent's base
|
|
2719
|
+
* op as a {@link ContractMethodOp}:
|
|
2720
|
+
*
|
|
2721
|
+
* - `create` → a `put` whose `item` is the fragment input (primary-key fields as
|
|
2722
|
+
* {@link ContractKeyFieldRef}, the rest as {@link ContractParamRef}), guarded by
|
|
2723
|
+
* `attribute_not_exists(PK)` (`{ notExists: true }`);
|
|
2724
|
+
* - `update` → an `update` whose `keys` is the whole-keys sentinel with
|
|
2725
|
+
* `keyFields` = the primary-key fields (bound from `$.input.*`), and whose
|
|
2726
|
+
* `changes` is the **non-key** fragment input;
|
|
2727
|
+
* - `remove` → a `delete` whose `keys` is the whole-keys sentinel + `keyFields`.
|
|
2728
|
+
*
|
|
2729
|
+
* The resolved lifecycle's effect set is attached as the empty per-fragment hook.
|
|
2730
|
+
*
|
|
2731
|
+
* @param fragment The fragment to compile.
|
|
2732
|
+
* @param index Its 0-based position in the mutation's fragment list (the
|
|
2733
|
+
* cross-fragment reference index). `0` for a single-fragment compile.
|
|
2734
|
+
* @param resolveEntityRef Resolver for a `$.entity[i].field` cross-fragment
|
|
2735
|
+
* reference (multi-fragment compile), or `null` for a single-fragment compile
|
|
2736
|
+
* (where such a reference is a hard error).
|
|
2737
|
+
*/
|
|
2738
|
+
declare function compileFragment(fragment: MutationFragment, index?: number, resolveEntityRef?: EntityRefResolver | null): CompiledFragment;
|
|
2739
|
+
/**
|
|
2740
|
+
* Compile a single-fragment {@link CommandPlan} into its {@link CompiledFragment}.
|
|
2741
|
+
* The N-fragment atomic merge is **#90** ({@link compileMutationPlan}); this entry
|
|
2742
|
+
* point is the single-fragment fast path used by #83 and by the multi-fragment
|
|
2743
|
+
* compiler for `N === 1` (no behavioral change). A cross-fragment reference is a
|
|
2744
|
+
* hard error here — there is no earlier fragment to read from.
|
|
2745
|
+
*
|
|
2746
|
+
* @throws if `plan` is not a {@link CommandPlan}, or declares more than one fragment.
|
|
2747
|
+
*/
|
|
2748
|
+
declare function compileSingleFragmentPlan(plan: CommandPlan): CompiledFragment;
|
|
2749
|
+
/**
|
|
2750
|
+
* The result of compiling a (1..N)-fragment {@link CommandPlan} (#90): the ordered
|
|
2751
|
+
* per-fragment {@link CompiledFragment}s, merged so they form **one atomic
|
|
2752
|
+
* `TransactWriteItems`**. For a single fragment this is exactly the #83 result with
|
|
2753
|
+
* `fragments.length === 1` (no regression); for N≥2 the fragments are validated
|
|
2754
|
+
* together — same-item conflicts rejected, cross-fragment `$.entity[i].field`
|
|
2755
|
+
* dependencies resolved (circular deps rejected), and the composed item count
|
|
2756
|
+
* capped at {@link MAX_TRANSACT_COMPOSE_ITEMS}.
|
|
2757
|
+
*
|
|
2758
|
+
* The **primary fragment** is fragment 0 — the entity the public Command IF is
|
|
2759
|
+
* keyed by and read back from (`returnSelection`); the proposal's `CreatePost`
|
|
2760
|
+
* keys on `Post` (fragment 0), with `AuditLog` (fragment 1) composed atomically.
|
|
2761
|
+
*/
|
|
2762
|
+
interface CompiledMutationPlan {
|
|
2763
|
+
/** The mutation name (documentary; surfaces in compiler errors). */
|
|
2764
|
+
readonly name: string;
|
|
2765
|
+
/** Each fragment compiled to its base op, in declaration order. */
|
|
2766
|
+
readonly fragments: readonly CompiledFragment[];
|
|
2767
|
+
}
|
|
2768
|
+
/**
|
|
2769
|
+
* Compile a (1..N)-fragment {@link CommandPlan} into one **atomically composable**
|
|
2770
|
+
* {@link CompiledMutationPlan} (issue #90; proposal §3, "CROSS-FRAGMENT composition
|
|
2771
|
+
* + validation"). Each fragment is compiled with #83's per-fragment compiler; the
|
|
2772
|
+
* resulting items are **merged into one `TransactWriteItems`**, with every MUST AC
|
|
2773
|
+
* enforced at build time:
|
|
2774
|
+
*
|
|
2775
|
+
* 1. **Cross-fragment data dependencies** — a fragment's `$.entity[i].field` leaf
|
|
2776
|
+
* is resolved to fragment `i`'s binding for `field` (a build-time alias, so the
|
|
2777
|
+
* value renders from the same shared input at execution; a `TransactWriteItems`
|
|
2778
|
+
* cannot read another item mid-transaction). A reference to a **same-or-later**
|
|
2779
|
+
* fragment (`i ≥ consumer`) is a **circular** dependency and is rejected; an
|
|
2780
|
+
* out-of-range index, or a field the producer does not write, is rejected.
|
|
2781
|
+
* 2. **Same-item conflict** — two fragments that write the **same primary key**
|
|
2782
|
+
* ({@link keySignature}) in one transaction are rejected (DynamoDB rejects a
|
|
2783
|
+
* transaction touching one item twice), naming the conflicting fragments.
|
|
2784
|
+
* 3. **≤25 items** — a composed plan over {@link MAX_TRANSACT_COMPOSE_ITEMS} base
|
|
2785
|
+
* items is a hard error (an atomic transaction is NEVER split — atomicity, #64).
|
|
2786
|
+
*
|
|
2787
|
+
* Because fragment `i` is fully compiled before fragment `i+1`, and a cross-fragment
|
|
2788
|
+
* reference may only point **backward** (to an already-compiled fragment), resolving
|
|
2789
|
+
* dependencies in declaration order is itself the topological order — a forward /
|
|
2790
|
+
* self reference is the only cycle shape and is rejected directly, so no separate
|
|
2791
|
+
* cycle search is needed.
|
|
2792
|
+
*
|
|
2793
|
+
* @throws if `plan` is not a {@link CommandPlan}; on a malformed / circular / dangling
|
|
2794
|
+
* cross-fragment reference; on a same-item conflict; or on >25 composed items.
|
|
2795
|
+
*/
|
|
2796
|
+
declare function compileMutationPlan(plan: CommandPlan): CompiledMutationPlan;
|
|
2797
|
+
|
|
2798
|
+
/**
|
|
2799
|
+
* Contract DSL — `publicQueryModel` / `publicCommandModel` (issue #58, CQRS
|
|
2800
|
+
* Contract layer, Epic #57; spec `docs/cqrs-contract.md`).
|
|
2801
|
+
*
|
|
2802
|
+
* A **QueryModel / CommandModel** is the public, storage-independent interface
|
|
2803
|
+
* of a CQRS (logical) service. It is **keyed** — its Key *is* the access pattern
|
|
2804
|
+
* — and holds **one or more named Methods**, each a distinct **use case** (e.g.
|
|
2805
|
+
* `get` + `summary` over the same key, differing only in projection). The Key is
|
|
2806
|
+
* the access pattern; the Method is the use case.
|
|
2807
|
+
*
|
|
2808
|
+
* This module sits **on top of** the existing definition DSL
|
|
2809
|
+
* (`defineQuery` / `defineList` / `definePut` / `defineUpdate` / `defineDelete`,
|
|
2810
|
+
* `src/define/define.ts`) and shares its internal representation
|
|
2811
|
+
* ({@link OperationDefinition}). A contract method body resolves to a single
|
|
2812
|
+
* declarative operation on the underlying model; the closure is **never**
|
|
2813
|
+
* serialized — only the resolved declaration is captured into the IR.
|
|
2814
|
+
*
|
|
2815
|
+
* ```ts
|
|
2816
|
+
* const ArticleById = publicQueryModel<ArticleIdKey>()({
|
|
2817
|
+
* get: (keys, params) =>
|
|
2818
|
+
* Article.query(keys, { articleId: true, title: true, body: true }, params),
|
|
2819
|
+
* });
|
|
2820
|
+
* ```
|
|
2821
|
+
*
|
|
2822
|
+
* ## Method signatures (the generation source)
|
|
2823
|
+
*
|
|
2824
|
+
* Every method has one uniform signature, accepting **either one key or an array
|
|
2825
|
+
* of keys** (the array form is what makes a contract uniformly batch-resolvable):
|
|
2826
|
+
*
|
|
2827
|
+
* ```ts
|
|
2828
|
+
* type QueryMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
|
|
2829
|
+
* type CommandMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
|
|
2830
|
+
* ```
|
|
2831
|
+
*
|
|
2832
|
+
* These two types are the **generation source** for every binding target
|
|
2833
|
+
* (OpenAPI / Python / PHP / Go / Rust) — see the proposal.
|
|
2834
|
+
*
|
|
2835
|
+
* ## Method names are use cases, not operations
|
|
2836
|
+
*
|
|
2837
|
+
* A method is named for its **use case** (`get` / `summary` / `recent` /
|
|
2838
|
+
* `disable`), never for the storage op it runs. Whether it resolves internally
|
|
2839
|
+
* to a `query` (point lookup) or a `list` (partition read) is the **Runtime's**
|
|
2840
|
+
* concern and is invisible to callers and to generated bindings. The internal op
|
|
2841
|
+
* still determines the **derived** `resolution` / `inputArity` facts (below), but
|
|
2842
|
+
* those are bookkeeping, not part of the method's public name.
|
|
2843
|
+
*
|
|
2844
|
+
* ## Derived facts: `resolution` and `inputArity`
|
|
2845
|
+
*
|
|
2846
|
+
* Per the proposal's N+1 Safety section these are **derived** from the internal
|
|
2847
|
+
* op kind, never hand-written:
|
|
2848
|
+
*
|
|
2849
|
+
* - a unique-key `query` / `GetItem` → `resolution: 'point'`, `inputArity:
|
|
2850
|
+
* 'either'` (a key array becomes one `BatchGetItem`);
|
|
2851
|
+
* - a partition `list` / `Query` → `resolution: 'range'`, `inputArity: 'single'`
|
|
2852
|
+
* (DynamoDB cannot coalesce N partition `Query`s — feeding an array would be an
|
|
2853
|
+
* N+1 fan-out, so the range method only accepts a single key).
|
|
2854
|
+
*
|
|
2855
|
+
* Command methods additionally carry an explicit **result type** (`void` | a
|
|
2856
|
+
* `Result` | the updated entity) as part of the contract; see
|
|
2857
|
+
* {@link CommandResultKind}.
|
|
2858
|
+
*
|
|
2859
|
+
* ## Build-time hardening (reused from `defineTransaction`)
|
|
2860
|
+
*
|
|
2861
|
+
* The method body is evaluated at definition time with **throwing sentinels** in
|
|
2862
|
+
* place of `keys` / `params`, recorded through a model recorder, and subjected to
|
|
2863
|
+
* the **same** hardening `defineTransaction` (`src/define/transaction.ts`) applies
|
|
2864
|
+
* to value positions:
|
|
2865
|
+
*
|
|
2866
|
+
* 1. **Throwing sentinel Proxy** — every property / method access other than the
|
|
2867
|
+
* internal brand reads and primitive coercion throws at the access site.
|
|
2868
|
+
* 2. **Per-access coercion ledger (primary boundary)** — a faithful reference is
|
|
2869
|
+
* stored as the Proxy object and is *never* coerced, so any coercion of a
|
|
2870
|
+
* `params` field is positive proof the body consumed it into a transform /
|
|
2871
|
+
* branch / interpolation and the build is rejected.
|
|
2872
|
+
* 3. **Differential evaluation (secondary)** — the body runs twice with disjoint
|
|
2873
|
+
* per-field markers; every recorded leaf must be a faithful reference or a
|
|
2874
|
+
* byte-identical literal across passes.
|
|
2875
|
+
* 4. **Consumed-field check** — a `params` field the body accessed but that never
|
|
2876
|
+
* reached the recorded operation (a silent value branch) is rejected.
|
|
2877
|
+
*
|
|
2878
|
+
* So any non-declarative body (a transform, a value branch, a coercion, arbitrary
|
|
2879
|
+
* JS over `keys` / `params`) is rejected at build time — never silently emitted
|
|
2880
|
+
* as a wrong spec. The #58 issue was the **DSL + IR + type-level surface only**:
|
|
2881
|
+
* serialization to the JSON SSoT (#59) and execution against DynamoDB (#62) were
|
|
2882
|
+
* out of scope there.
|
|
2883
|
+
*
|
|
2884
|
+
* ## External Query composition (`query` / `from`, issue #63)
|
|
2885
|
+
*
|
|
2886
|
+
* The {@link query} / {@link from} primitive (added by #63) extends a read method
|
|
2887
|
+
* body with a **build-time-resolved, in-process** reference to **another query
|
|
2888
|
+
* contract's** method — relation chaining where the link is a contract reference
|
|
2889
|
+
* instead of a model relation (there is no protocol / transport). Placed at a
|
|
2890
|
+
* `select` property (`as: query(OtherContract.method, { childKey: from("$.field") })`),
|
|
2891
|
+
* it is recorded as a declarative {@link RecordedCompose} edge on the method op
|
|
2892
|
+
* (the property name becomes `as`, and it is removed from the read projection).
|
|
2893
|
+
* The binding stays declarative — `from` admits only a literal `$`-rooted path —
|
|
2894
|
+
* so no arbitrary JS reaches a key binding. The serializer (#59) recovers the
|
|
2895
|
+
* referenced contract name by identity and emits the proposal's `compose` shape;
|
|
2896
|
+
* the N+1 checker (#60) enforces the `point`-child rule; the runtimes (#62 infra
|
|
2897
|
+
* reused) resolve the child once, batched, across all parent records.
|
|
2898
|
+
*/
|
|
2899
|
+
|
|
2900
|
+
/**
|
|
2901
|
+
* The **resolution kind** of a method, derived from its internal op:
|
|
2902
|
+
*
|
|
2903
|
+
* - `'point'` — target keys are known (unique-key `query` / `GetItem`, fixed key
|
|
2904
|
+
* sets); coalesces to a `BatchGetItem` for a key array.
|
|
2905
|
+
* - `'range'` — target key set is unknown (partition `list` / `Query`); one
|
|
2906
|
+
* request per partition key, so it is only ever safe for a single key.
|
|
2907
|
+
*/
|
|
2908
|
+
type Resolution = 'point' | 'range';
|
|
2909
|
+
/**
|
|
2910
|
+
* What input arity a method accepts, derived from its `resolution`:
|
|
2911
|
+
*
|
|
2912
|
+
* - `'either'` — a single key **or** an array (a `point` read; the array form is
|
|
2913
|
+
* one `BatchGetItem`).
|
|
2914
|
+
* - `'single'` — a single key only (a `range` read; an array would be an N+1
|
|
2915
|
+
* fan-out and is rejected by construction).
|
|
2916
|
+
* - `'array'` — an array only (reserved; not produced by the current resolvers).
|
|
2917
|
+
*/
|
|
2918
|
+
type InputArity = 'single' | 'array' | 'either';
|
|
2919
|
+
/**
|
|
2920
|
+
* The category of a {@link CommandMethod}'s declared result, part of the
|
|
2921
|
+
* contract (it surfaces in OpenAPI and every binding):
|
|
2922
|
+
*
|
|
2923
|
+
* - `'void'` — fire-and-forget write (no body);
|
|
2924
|
+
* - `'result'` — an outcome/status object (e.g. `{ ok, version }`);
|
|
2925
|
+
* - `'entity'` — the updated entity (the post-write projection).
|
|
2926
|
+
*
|
|
2927
|
+
* The concrete `TResult` type is preserved at the type level on
|
|
2928
|
+
* {@link CommandMethodSpec}; this discriminant is the runtime/SSoT-facing label.
|
|
2929
|
+
*/
|
|
2930
|
+
type CommandResultKind = 'void' | 'result' | 'entity';
|
|
2931
|
+
/**
|
|
2932
|
+
* The formal **query** method signature (proposal: "Query Method — Common
|
|
2933
|
+
* Interface"). Accepts a single key **or** an array of keys and returns
|
|
2934
|
+
* `TResult`. This one type is the generation source for every binding target.
|
|
2935
|
+
*
|
|
2936
|
+
* @typeParam TKey - The contract Key (the access pattern / join key).
|
|
2937
|
+
* @typeParam TParams - The retrieval options (pagination / cursor / consistency).
|
|
2938
|
+
* @typeParam TResult - The method's result type.
|
|
2939
|
+
*/
|
|
2940
|
+
type QueryMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
|
|
2941
|
+
/**
|
|
2942
|
+
* The formal **command** method signature (proposal: "Command Definition").
|
|
2943
|
+
* Symmetric to {@link QueryMethod}, with an explicit result type. A single key
|
|
2944
|
+
* maps to one write op; an array maps to a batched write.
|
|
2945
|
+
*
|
|
2946
|
+
* @typeParam TKey - The contract Key (the target identity).
|
|
2947
|
+
* @typeParam TParams - The call params.
|
|
2948
|
+
* @typeParam TResult - The declared result type (`void` | a `Result` | entity).
|
|
2949
|
+
*/
|
|
2950
|
+
type CommandMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
|
|
2951
|
+
declare const KEY_REF_BRAND: unique symbol;
|
|
2952
|
+
declare const KEY_FIELD_REF_BRAND: unique symbol;
|
|
2953
|
+
/**
|
|
2954
|
+
* The captured `keys` argument of a contract method body. A branded sentinel that
|
|
2955
|
+
* supports the two faithful uses the proposal's examples need:
|
|
2956
|
+
*
|
|
2957
|
+
* - **passed whole** into a model op's key position
|
|
2958
|
+
* (`Model.query(keys, …)` / `Model.updateItem(keys, …)`) — the point / write form;
|
|
2959
|
+
* - **destructured per field** to rebuild a partition key
|
|
2960
|
+
* (`Model.list({ categoryId: keys.categoryId }, …)`) — the range form. Each
|
|
2961
|
+
* `keys.<field>` mints a faithful {@link ContractKeyFieldRef}.
|
|
2962
|
+
*
|
|
2963
|
+
* Both forms are declarative: the recorded key slot is either this sentinel
|
|
2964
|
+
* itself (whole) or a `{ field: ContractKeyFieldRef }` record. Any non-faithful
|
|
2965
|
+
* use — coercing `keys`, or transforming a `keys.<field>` ref — is rejected by the
|
|
2966
|
+
* same coercion ledger / differential checks as `params`.
|
|
2967
|
+
*/
|
|
2968
|
+
interface ContractKeyRef {
|
|
2969
|
+
readonly [KEY_REF_BRAND]: true;
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* A captured reference to a single field of the method's `keys` argument
|
|
2973
|
+
* (`keys.<field>`), minted when a range body rebuilds a partition key. Branded so
|
|
2974
|
+
* the verifier can distinguish a faithful key-field reference from a literal, and
|
|
2975
|
+
* so it cannot be silently coerced.
|
|
2976
|
+
*/
|
|
2977
|
+
interface ContractKeyFieldRef {
|
|
2978
|
+
readonly [KEY_FIELD_REF_BRAND]: true;
|
|
2979
|
+
/** The key field name, e.g. `categoryId`. */
|
|
2980
|
+
readonly field: string;
|
|
2981
|
+
/** The template token this reference renders to, e.g. `{key.categoryId}`. */
|
|
2982
|
+
readonly token: string;
|
|
2983
|
+
}
|
|
2984
|
+
/** Runtime guard: is `value` a {@link ContractKeyRef} (the whole key sentinel)? */
|
|
2985
|
+
declare function isContractKeyRef(value: unknown): value is ContractKeyRef;
|
|
2986
|
+
/**
|
|
2987
|
+
* Mint a faithful {@link ContractKeyFieldRef} for a named key field — a plain
|
|
2988
|
+
* (non-Proxy) reference rendering the `{key.field}` differential token but, at a
|
|
2989
|
+
* **value** position (a `put` item field), serialized as `{field}` (see
|
|
2990
|
+
* `templateLeaf`, `src/spec/operations.ts`). Used by the **mutation compiler**
|
|
2991
|
+
* (#83) to mark which of a `create` fragment's item fields are the model's
|
|
2992
|
+
* primary-key (contract Key) fields, so the existing serializer recovers the
|
|
2993
|
+
* contract Key from the put item (`keyFieldsOf`) exactly as it does for a
|
|
2994
|
+
* hand-written #64 `put` that binds `keys.<field>` into the item. Mirrors
|
|
2995
|
+
* {@link mintContractParamRef}.
|
|
2996
|
+
*
|
|
2997
|
+
* @param field The key field name (rendered `{field}` at a value position).
|
|
2998
|
+
*/
|
|
2999
|
+
declare function mintContractKeyFieldRef(field: string): ContractKeyFieldRef;
|
|
3000
|
+
/**
|
|
3001
|
+
* The whole-`keys` sentinel value an `update` / `delete` op records in its key
|
|
3002
|
+
* slot when the body passed `keys` whole into the op's key position. Exposed so
|
|
3003
|
+
* the **mutation compiler** (#83) can build an equivalent planned `update` /
|
|
3004
|
+
* `delete` op without re-entering the hardening machinery (a mutation has no
|
|
3005
|
+
* `keys` argument — its key fields are bound from `$.input.*`, captured via
|
|
3006
|
+
* {@link ContractMethodOp.keyFields}).
|
|
3007
|
+
*/
|
|
3008
|
+
declare function wholeKeysSentinel(): ContractKeyRef;
|
|
3009
|
+
/** Runtime guard: is `value` a {@link ContractKeyFieldRef}? */
|
|
3010
|
+
declare function isContractKeyFieldRef(value: unknown): value is ContractKeyFieldRef;
|
|
3011
|
+
declare const PARAM_REF_BRAND: unique symbol;
|
|
3012
|
+
/**
|
|
3013
|
+
* A captured reference to a field of the method's `params` argument (`{<field>}`).
|
|
3014
|
+
* Branded so the recorder can distinguish a faithful reference from a concrete
|
|
3015
|
+
* literal, and so a stray reference cannot be silently coerced.
|
|
3016
|
+
*/
|
|
3017
|
+
interface ContractParamRef {
|
|
3018
|
+
readonly [PARAM_REF_BRAND]: true;
|
|
3019
|
+
/** The template token this reference renders to, e.g. `{limit}`. */
|
|
3020
|
+
readonly token: string;
|
|
3021
|
+
/** The field name on `params`, e.g. `limit`. */
|
|
3022
|
+
readonly field: string;
|
|
3023
|
+
}
|
|
3024
|
+
/** Runtime guard: is `value` a {@link ContractParamRef}? */
|
|
3025
|
+
declare function isContractParamRef(value: unknown): value is ContractParamRef;
|
|
3026
|
+
/**
|
|
3027
|
+
* Mint a faithful {@link ContractParamRef} for a named field — a concrete
|
|
3028
|
+
* (non-Proxy) reference rendering the `{field}` token. Unlike the hardening
|
|
3029
|
+
* sentinels {@link makeParamFieldRef} builds during a contract method body (which
|
|
3030
|
+
* throw on any transform to detect non-declarative use), this is a *plain* ref
|
|
3031
|
+
* used by the **mutation compiler** (#83, `src/spec/mutation-command.ts`): a
|
|
3032
|
+
* mutation fragment's `input` binding is captured as a {@link MutationInputRef}
|
|
3033
|
+
* (its own declarative sentinel), and the compiler translates each one into this
|
|
3034
|
+
* `ContractParamRef` so the **existing** contract serializer
|
|
3035
|
+
* ({@link opToDefinition} → {@link buildCommandSpec}) and TS runtime
|
|
3036
|
+
* ({@link renderLeaf}) consume it with **zero** new template / param machinery —
|
|
3037
|
+
* the param name and `{token}` are the input field name, exactly as a hand-written
|
|
3038
|
+
* `params.field` reference would render.
|
|
3039
|
+
*
|
|
3040
|
+
* @param field The param / input field name (the rendered token is `{field}`).
|
|
3041
|
+
*/
|
|
3042
|
+
declare function mintContractParamRef(field: string): ContractParamRef;
|
|
3043
|
+
declare const FROM_REF_BRAND: unique symbol;
|
|
3044
|
+
declare const COMPOSE_NODE_BRAND: unique symbol;
|
|
3045
|
+
/**
|
|
3046
|
+
* A parent-result source path produced by {@link from} (proposal "External
|
|
3047
|
+
* Query"). It names the field of the **parent** method's result that supplies a
|
|
3048
|
+
* composed child's key, e.g. `from("$.billingAccountId")`. The path is a plain
|
|
3049
|
+
* `"$.field"` string — the binding is **declarative** (no arbitrary JS): the
|
|
3050
|
+
* value is read from the resolved parent record at execution time, never computed.
|
|
3051
|
+
*
|
|
3052
|
+
* The grammar is intentionally minimal — a single dotted field path rooted at
|
|
3053
|
+
* `$` (the parent record). `$.a.b` reaches a nested field; anything else (a bare
|
|
3054
|
+
* field with no `$`, an index, a wildcard, an empty path) is rejected by
|
|
3055
|
+
* {@link from} at build time so a malformed binding can never reach the SSoT.
|
|
3056
|
+
*/
|
|
3057
|
+
interface ContractFromRef {
|
|
3058
|
+
readonly [FROM_REF_BRAND]: true;
|
|
3059
|
+
/** The `$`-rooted source path on the parent result, e.g. `$.billingAccountId`. */
|
|
3060
|
+
readonly path: string;
|
|
3061
|
+
}
|
|
3062
|
+
/** Runtime guard: is `value` a {@link ContractFromRef} (a `from(...)` binding)? */
|
|
3063
|
+
declare function isContractFromRef(value: unknown): value is ContractFromRef;
|
|
3064
|
+
/**
|
|
3065
|
+
* A resolved External Query composition node produced by {@link query} and placed
|
|
3066
|
+
* at a `select` property of a contract method body (proposal "External Query"):
|
|
3067
|
+
*
|
|
3068
|
+
* ```ts
|
|
3069
|
+
* Account.query(keys, {
|
|
3070
|
+
* accountId: true,
|
|
3071
|
+
* billing: query(BillingPlanQueries.get, { accountId: from("$.billingAccountId") }),
|
|
3072
|
+
* })
|
|
3073
|
+
* ```
|
|
3074
|
+
*
|
|
3075
|
+
* It carries a faithful reference to the **referenced method spec** (so the
|
|
3076
|
+
* serializer can recover the referenced contract's *name* by identity against the
|
|
3077
|
+
* contract map, and its derived `resolution` / `cardinality` facts) plus the
|
|
3078
|
+
* declarative child-key binding (child key field → a {@link ContractFromRef}).
|
|
3079
|
+
* The owning `select` key becomes the composition's `as` property at record time
|
|
3080
|
+
* (it is not known to {@link query} itself).
|
|
3081
|
+
*/
|
|
3082
|
+
interface ContractComposeNode {
|
|
3083
|
+
readonly [COMPOSE_NODE_BRAND]: true;
|
|
3084
|
+
/** The referenced query method spec (`OtherContract.method`). */
|
|
3085
|
+
readonly method: QueryMethodSpec<unknown, unknown, unknown>;
|
|
3086
|
+
/** The child-key binding: child key field → parent-result `from` path. */
|
|
3087
|
+
readonly bind: Readonly<Record<string, ContractFromRef>>;
|
|
3088
|
+
}
|
|
3089
|
+
/** Runtime guard: is `value` a {@link ContractComposeNode} (a `query(...)` node)? */
|
|
3090
|
+
declare function isContractComposeNode(value: unknown): value is ContractComposeNode;
|
|
3091
|
+
/**
|
|
3092
|
+
* Declare a parent-result source path for an External Query child key binding
|
|
3093
|
+
* (proposal "External Query"). Used **only** inside a {@link query} binding:
|
|
3094
|
+
*
|
|
3095
|
+
* ```ts
|
|
3096
|
+
* query(BillingPlanQueries.get, { accountId: from("$.billingAccountId") })
|
|
3097
|
+
* ```
|
|
3098
|
+
*
|
|
3099
|
+
* The path is `$`-rooted (the parent record) and dotted; the bound child-key
|
|
3100
|
+
* field is read from that path of each resolved parent record at execution time.
|
|
3101
|
+
* The binding is **declarative** — `from` accepts only a literal path string and
|
|
3102
|
+
* rejects any other shape, so no arbitrary JS can sneak into a key binding.
|
|
3103
|
+
*
|
|
3104
|
+
* @param path A `$`-rooted dotted path, e.g. `"$.billingAccountId"` or `"$.a.b"`.
|
|
3105
|
+
* @throws if `path` is not a non-empty `$`-rooted dotted field path.
|
|
3106
|
+
*/
|
|
3107
|
+
declare function from(path: string): ContractFromRef;
|
|
3108
|
+
/**
|
|
3109
|
+
* Reference another query contract's method as an **External Query** composition
|
|
3110
|
+
* child, bound to the parent result by a declarative `from` mapping (proposal
|
|
3111
|
+
* "Query Composition" / "External Query"). Place the result at a `select`
|
|
3112
|
+
* property of a contract method body; the property name becomes the composed
|
|
3113
|
+
* value's `as`:
|
|
3114
|
+
*
|
|
3115
|
+
* ```ts
|
|
3116
|
+
* export const AccountAccess = publicQueryModel<AccountKey>()({
|
|
3117
|
+
* get: (keys, params) => Account.query(keys, {
|
|
3118
|
+
* accountId: true,
|
|
3119
|
+
* billing: query(BillingPlanQueries.get, { accountId: from("$.billingAccountId") }),
|
|
3120
|
+
* }, params),
|
|
3121
|
+
* });
|
|
3122
|
+
* ```
|
|
3123
|
+
*
|
|
3124
|
+
* This is **build-time-resolved, in-process** contract chaining — the relation
|
|
3125
|
+
* primitive extended to point at another contract's method instead of a model
|
|
3126
|
+
* relation. There is no protocol / transport: the runtime collects every bound
|
|
3127
|
+
* key produced by the parent step and resolves the referenced contract **once,
|
|
3128
|
+
* batched** (proposal "External Query"). The child MUST be `point` (the parent
|
|
3129
|
+
* may yield N records, so a `range` child would be an N+1 fan-out) — that rule is
|
|
3130
|
+
* owned by the N+1 checker (#60), which rejects a `range` child at build time.
|
|
3131
|
+
*
|
|
3132
|
+
* @param method The referenced query method (`OtherContract.method`), a resolved
|
|
3133
|
+
* {@link QueryMethodSpec}.
|
|
3134
|
+
* @param bind The child-key binding: child key field → a {@link from} path on
|
|
3135
|
+
* the parent result. Must be non-empty and every value a `from(...)` ref.
|
|
3136
|
+
* @throws if `method` is not a query method spec, or `bind` is empty / carries a
|
|
3137
|
+
* non-`from` value.
|
|
3138
|
+
*/
|
|
3139
|
+
declare function query(method: QueryMethodSpec<any, any, any>, bind: Record<string, ContractFromRef>): ContractComposeNode;
|
|
3140
|
+
/**
|
|
3141
|
+
* The {@link QueryModelContract} that owns a resolved query method spec, or
|
|
3142
|
+
* `undefined` if the spec was not produced by {@link publicQueryModel} (so it has
|
|
3143
|
+
* no registered owner). Used by the serializer to resolve a composition's
|
|
3144
|
+
* referenced contract by identity.
|
|
3145
|
+
*/
|
|
3146
|
+
declare function contractOfMethodSpec(method: QueryMethodSpec<unknown, unknown, unknown>): QueryModelContract<unknown, Record<string, QueryMethodSpec<unknown, unknown, unknown>>> | undefined;
|
|
3147
|
+
/**
|
|
3148
|
+
* The declarative internal operation a contract method body resolves to — the
|
|
3149
|
+
* resolved declaration captured into the IR (the closure itself is discarded).
|
|
3150
|
+
* Mirrors {@link OperationDefinition} but is produced by the model recorder
|
|
3151
|
+
* handed to a contract method body rather than a `define*` entry point.
|
|
3152
|
+
*
|
|
3153
|
+
* @typeParam Op - The internal op kind (`query` | `list` | `put` | `update` |
|
|
3154
|
+
* `delete`).
|
|
3155
|
+
*/
|
|
3156
|
+
interface ContractMethodOp<Op extends OperationKind = OperationKind> {
|
|
3157
|
+
/** @internal Marks this object as a recorded contract method op. */
|
|
3158
|
+
readonly __isContractMethodOp: true;
|
|
3159
|
+
/** The model the op targets (name + runtime class). */
|
|
3160
|
+
readonly entity: EntityRef;
|
|
3161
|
+
/** The internal op kind. Determines the derived `resolution` / `inputArity`. */
|
|
3162
|
+
readonly operation: Op;
|
|
3163
|
+
/**
|
|
3164
|
+
* The captured key slot. Either the whole {@link ContractKeyRef} sentinel
|
|
3165
|
+
* (passed faithfully into the op's key position — the point / write form) or a
|
|
3166
|
+
* `{ field: ContractKeyFieldRef }` record rebuilt from `keys.<field>` (the range
|
|
3167
|
+
* form). For `put` it is the inert {@link PUT_HAS_NO_KEY} marker. Stored as
|
|
3168
|
+
* evidence of faithfulness; never serialized.
|
|
3169
|
+
*/
|
|
3170
|
+
readonly keys: ContractKeyRef | Readonly<Record<string, unknown>>;
|
|
3171
|
+
/**
|
|
3172
|
+
* The **explicit contract Key field names** captured from the factory's
|
|
3173
|
+
* key-field argument (`publicQueryModel<EmailKey>(['email'])(...)`, issue #71).
|
|
3174
|
+
* Present only for a **whole-`keys`** op (the {@link keys} slot is the whole
|
|
3175
|
+
* {@link ContractKeyRef} sentinel) when the author supplied a field list, or
|
|
3176
|
+
* when the model-derived factory form defaulted it to the model's primary-key
|
|
3177
|
+
* input fields. It is the SSoT for "which fields the Key is composed of" — fed
|
|
3178
|
+
* through {@link resolveKey} so the serializer emits the correct base-table PK
|
|
3179
|
+
* **or** GSI (`indexName` + GSI key template) key, and so the planner can tell a
|
|
3180
|
+
* coalescible base-table point (`GetItem` / `BatchGetItem`) from a non-coalescible
|
|
3181
|
+
* GSI point (a unique-GSI `Query`).
|
|
3182
|
+
*
|
|
3183
|
+
* Absent when the body rebuilds a partition key from `keys.<field>` references
|
|
3184
|
+
* (the range form already captures field names in the {@link keys} record) or
|
|
3185
|
+
* when no explicit list was given to a `publicQueryModel<TKey>()` whole-key form
|
|
3186
|
+
* (the legacy backward-compatible path, which then defaults to the primary key).
|
|
3187
|
+
*/
|
|
3188
|
+
readonly keyFields?: readonly string[];
|
|
3189
|
+
/** The select projection for `query` / `list`; `undefined` for writes. */
|
|
3190
|
+
readonly select?: Readonly<Record<string, unknown>>;
|
|
3191
|
+
/** The changes structure for `update`; `undefined` otherwise. */
|
|
3192
|
+
readonly changes?: Readonly<Record<string, unknown>>;
|
|
3193
|
+
/** The item structure for `put`; `undefined` otherwise. */
|
|
3194
|
+
readonly item?: Readonly<Record<string, unknown>>;
|
|
3195
|
+
/** The optional declarative write condition for writes. */
|
|
3196
|
+
readonly condition?: ConditionInput;
|
|
3197
|
+
/**
|
|
3198
|
+
* External Query compositions (#63) extracted from the recorded `select`: each
|
|
3199
|
+
* `query(OtherContract.method, { childKey: from("$.parentField") })` node placed
|
|
3200
|
+
* at a `select` property becomes one {@link RecordedCompose} here (the property
|
|
3201
|
+
* name is its `as`), and the property is removed from the read projection. Absent
|
|
3202
|
+
* when the body declares no composition.
|
|
3203
|
+
*/
|
|
3204
|
+
readonly compose?: readonly RecordedCompose[];
|
|
3205
|
+
}
|
|
3206
|
+
/**
|
|
3207
|
+
* A recorded External Query composition edge on a contract method op (#63). The
|
|
3208
|
+
* declarative result of a `query(OtherContract.method, { … })` call placed at a
|
|
3209
|
+
* `select` property: it pairs the property name (`as`) with a faithful reference
|
|
3210
|
+
* to the referenced method spec and the declarative child-key binding. The
|
|
3211
|
+
* referenced contract's *name* and the child's derived `resolution` / `cardinality`
|
|
3212
|
+
* are recovered by the serializer (`src/spec/contracts.ts`) from the method spec
|
|
3213
|
+
* by identity against the contract map; they are not duplicated here.
|
|
3214
|
+
*/
|
|
3215
|
+
interface RecordedCompose {
|
|
3216
|
+
/** The result property the composed value attaches to (the `select` key). */
|
|
3217
|
+
readonly as: string;
|
|
3218
|
+
/** The referenced query method spec (`OtherContract.method`). */
|
|
3219
|
+
readonly method: QueryMethodSpec<unknown, unknown, unknown>;
|
|
3220
|
+
/** The child-key binding: child key field → parent-result `from` path. */
|
|
3221
|
+
readonly bind: Readonly<Record<string, ContractFromRef>>;
|
|
3222
|
+
}
|
|
3223
|
+
/**
|
|
3224
|
+
* The **call signature** a contract method exposes to a caller, narrowed by its
|
|
3225
|
+
* decided {@link InputArity} — the type-level half of N+1 rule (a) ("array into a
|
|
3226
|
+
* `range` method", #60). Given a method's `inputArity`:
|
|
3227
|
+
*
|
|
3228
|
+
* - `'single'` (a `range` method) — the key argument is a **single** `TKey` only;
|
|
3229
|
+
* passing an array (`readonly TKey[]`) is a **type error** by construction, so a
|
|
3230
|
+
* `range` method's array overload does not type-check.
|
|
3231
|
+
* - `'either'` (a `point` read / known-key write) — a single key **or** an array
|
|
3232
|
+
* (the array form coalesces to one `BatchGetItem` / batched write).
|
|
3233
|
+
* - `'array'` — an array only (reserved; not produced by the current resolvers).
|
|
3234
|
+
*
|
|
3235
|
+
* This mirrors the proposal's "the generated binding's types encode `inputArity`
|
|
3236
|
+
* (single → bare argument, array → array argument)". It is the type-level narrowing
|
|
3237
|
+
* used by the runtime call surface (`executeQueryMethod`, `src/runtime/contract-runtime.ts`):
|
|
3238
|
+
* the executor keys its key-argument type on the named method's **literal** arity
|
|
3239
|
+
* `A` (carried on {@link QueryMethodSpec} when the body is tagged with
|
|
3240
|
+
* {@link point} / {@link range}), so feeding an array into a `'single'` method is a
|
|
3241
|
+
* `tsc` compile error — the **primary** N+1 rule (a) layer, complementing the
|
|
3242
|
+
* build-time SSoT check ({@link assertContractN1Safe}) and the runtime backstop.
|
|
3243
|
+
*
|
|
3244
|
+
* @typeParam A - The decided input arity (a literal `InputArity`).
|
|
3245
|
+
* @typeParam TKey - The contract Key.
|
|
3246
|
+
* @typeParam TParams - The method's params type.
|
|
3247
|
+
* @typeParam TResult - The method's result type.
|
|
3248
|
+
*/
|
|
3249
|
+
type ContractCallSignature<A extends InputArity, TKey, TParams, TResult> = (key: A extends 'single' ? TKey : A extends 'array' ? readonly TKey[] : TKey | readonly TKey[], params: TParams) => TResult;
|
|
3250
|
+
/**
|
|
3251
|
+
* The resolved IR of one **query** method. Carries the formal method type at the
|
|
3252
|
+
* type level (`QueryMethod<TKey, TParams, TResult>`) and, at runtime, the resolved
|
|
3253
|
+
* internal op plus the **derived** facts (`resolution` / `inputArity`).
|
|
3254
|
+
*
|
|
3255
|
+
* @typeParam TKey - The contract Key.
|
|
3256
|
+
* @typeParam TParams - The method's params type.
|
|
3257
|
+
* @typeParam TResult - The method's result type.
|
|
3258
|
+
* @typeParam A - The method's **literal** {@link InputArity} when the author
|
|
3259
|
+
* tagged the body with {@link point} / {@link range} (the type-level half of
|
|
3260
|
+
* N+1 rule (a), threaded into the call surface so an array fed into a `'single'`
|
|
3261
|
+
* method is a `tsc` error — see {@link ContractCallSignature} and
|
|
3262
|
+
* `executeQueryMethod`). Defaults to the wide `InputArity` for an untagged body,
|
|
3263
|
+
* which then relies on the build-time checker + the runtime backstop only.
|
|
3264
|
+
*/
|
|
3265
|
+
interface QueryMethodSpec<TKey, TParams, TResult, A extends InputArity = InputArity> {
|
|
3266
|
+
/** @internal Marks this object as a query method spec. */
|
|
3267
|
+
readonly __methodKind: 'query';
|
|
3268
|
+
/** The resolved internal operation (closure discarded). */
|
|
3269
|
+
readonly op: ContractMethodOp;
|
|
3270
|
+
/** Derived: `'point'` (unique-key query) or `'range'` (partition list). */
|
|
3271
|
+
readonly resolution: Resolution;
|
|
3272
|
+
/**
|
|
3273
|
+
* Derived: `'either'` for a base-table `point`, `'single'` for a `range` or a
|
|
3274
|
+
* unique-GSI `point`. Typed as the method's **literal** arity `A` when the body
|
|
3275
|
+
* was tagged ({@link point} / {@link range}); a tagged literal is verified
|
|
3276
|
+
* against this derived value at build time (a mismatch is a build error), so the
|
|
3277
|
+
* type cannot lie about the runtime arity.
|
|
3278
|
+
*/
|
|
3279
|
+
readonly inputArity: A;
|
|
3280
|
+
/**
|
|
3281
|
+
* @internal The method's own name in its contract (`'get'`, `'summary'`, …),
|
|
3282
|
+
* stamped by the factory. An External Query composition (#63) references this
|
|
3283
|
+
* spec object; the serializer recovers the referenced method name from here and
|
|
3284
|
+
* the referenced contract name by identity ({@link contractOfMethodSpec}).
|
|
3285
|
+
*/
|
|
3286
|
+
readonly __methodName?: string;
|
|
3287
|
+
/** @internal Phantom carrier retaining the formal `QueryMethod` type. */
|
|
3288
|
+
readonly __signature?: QueryMethod<TKey, TParams, TResult>;
|
|
3289
|
+
}
|
|
3290
|
+
/**
|
|
3291
|
+
* The resolved IR of one **command** method. Symmetric to {@link QueryMethodSpec},
|
|
3292
|
+
* with an explicit declared result (`void` | a `Result` | the updated entity).
|
|
3293
|
+
*
|
|
3294
|
+
* @typeParam TKey - The contract Key.
|
|
3295
|
+
* @typeParam TParams - The method's params type.
|
|
3296
|
+
* @typeParam TResult - The method's declared result type.
|
|
3297
|
+
*/
|
|
3298
|
+
interface CommandMethodSpec<TKey, TParams, TResult> {
|
|
3299
|
+
/** @internal Marks this object as a command method spec. */
|
|
3300
|
+
readonly __methodKind: 'command';
|
|
3301
|
+
/** The resolved internal write operation (closure discarded). */
|
|
3302
|
+
readonly op: ContractMethodOp;
|
|
3303
|
+
/**
|
|
3304
|
+
* Derived input arity: a single key maps to one write op; an array maps to a
|
|
3305
|
+
* batched write (a transaction / `BatchWriteItem`). Writes accept `'either'`.
|
|
3306
|
+
*/
|
|
3307
|
+
readonly inputArity: InputArity;
|
|
3308
|
+
/**
|
|
3309
|
+
* The category of the declared result type (`void` | `result` | `entity`),
|
|
3310
|
+
* part of the contract (it surfaces in OpenAPI and every binding). Derived from
|
|
3311
|
+
* the internal op when not explicitly annotated: an `update` returning the
|
|
3312
|
+
* entity → `'entity'`; a `delete` → `'void'`.
|
|
3313
|
+
*/
|
|
3314
|
+
readonly result: CommandResultKind;
|
|
3315
|
+
/**
|
|
3316
|
+
* The per-call execution **mode** (issue #101): `'transaction'` (default, atomic
|
|
3317
|
+
* all-or-nothing) or `'parallel'` (non-atomic, per-op partial success). It drives
|
|
3318
|
+
* the key-array bulk resolution form directly (the serializer's `modeTargetFor`).
|
|
3319
|
+
* Present on every descriptor-authored command method.
|
|
3320
|
+
*/
|
|
3321
|
+
readonly mode?: CommandMode;
|
|
3322
|
+
/**
|
|
3323
|
+
* The return projection of a {@link mutation}-derived command method (issue
|
|
3324
|
+
* #83): a boolean field map applied to the written entity as a **consistent
|
|
3325
|
+
* read-back** projection after the write commits (the proposal's "return =
|
|
3326
|
+
* read projection"). Present only on a `.plan(mutation)` method; a hand-written
|
|
3327
|
+
* #64 method (the closure form) omits it. JSON-safe (only boolean leaves), so
|
|
3328
|
+
* it serializes verbatim into {@link CommandContractMethodSpec.returnSelection}
|
|
3329
|
+
* and both runtimes apply the **same** projection.
|
|
3330
|
+
*/
|
|
3331
|
+
readonly returnSelection?: Readonly<Record<string, boolean>>;
|
|
3332
|
+
/**
|
|
3333
|
+
* The composed write ops of a **multi-fragment** `mutation`-derived method
|
|
3334
|
+
* (issue #90): when a mutation declares **2+ fragments**, each fragment compiles
|
|
3335
|
+
* to one write {@link ContractMethodOp} and the whole set executes as **one
|
|
3336
|
+
* atomic `TransactWriteItems`** (NOT sequential writes). Present only for N≥2; a
|
|
3337
|
+
* single-fragment method carries only {@link op} (no regression — #83 behavior).
|
|
3338
|
+
* The merge (same-item conflict / cross-fragment dep / ≤25 enforcement) is done
|
|
3339
|
+
* at build time by `compileMutationPlan`; `op` is set to the **primary** fragment
|
|
3340
|
+
* (fragment 0 — the entity the method is keyed by and read back from) so the
|
|
3341
|
+
* Key / read-back machinery is unchanged.
|
|
3342
|
+
*/
|
|
3343
|
+
readonly ops?: readonly ContractMethodOp[];
|
|
3344
|
+
/**
|
|
3345
|
+
* The referential-integrity assertions derived from the mutation's `requires`
|
|
3346
|
+
* effects (issue #84): one {@link DerivedConditionCheck} per
|
|
3347
|
+
* `w.exists(...)`, flattened across every fragment. Present (non-empty) only when
|
|
3348
|
+
* a fragment's lifecycle declares `requires`; absent otherwise (no regression —
|
|
3349
|
+
* the method then carries only its base op(s)). Each becomes a read-only
|
|
3350
|
+
* `ConditionCheck` (`attribute_exists`) item in the method's atomic
|
|
3351
|
+
* `TransactWriteItems`, and their presence **promotes** an otherwise single-op
|
|
3352
|
+
* method to a transaction: the serializer emits a {@link
|
|
3353
|
+
* import('../spec/types.js').TransactionItemSpec}-based `TransactionSpec` and
|
|
3354
|
+
* `single: { mode: 'transaction' }`, and the in-process runtime composes the base
|
|
3355
|
+
* write(s) + the ConditionCheck(s) into one transaction so an absent referent
|
|
3356
|
+
* rolls the WHOLE write back.
|
|
3357
|
+
*/
|
|
3358
|
+
readonly conditionChecks?: readonly DerivedConditionCheck[];
|
|
3359
|
+
/**
|
|
3360
|
+
* The adjacency edge writes derived from the mutation's `edges` effects (issue
|
|
3361
|
+
* #85), flattened across every fragment in declaration order. Present (non-empty)
|
|
3362
|
+
* only when a fragment's lifecycle declares `edges`; absent otherwise (no
|
|
3363
|
+
* regression). Each {@link DerivedEdgeWrite} contributes its adjacency `Put` /
|
|
3364
|
+
* `Delete` item(s) to the method's atomic `TransactWriteItems`, and their presence
|
|
3365
|
+
* (like {@link conditionChecks}) **promotes** an otherwise single-op method to a
|
|
3366
|
+
* transaction so the edge moves / removes atomically with the entity write.
|
|
3367
|
+
*/
|
|
3368
|
+
readonly edgeWrites?: readonly DerivedEdgeWrite[];
|
|
3369
|
+
/**
|
|
3370
|
+
* The derived (cascading) updates from the mutation's `derive` effects (issue
|
|
3371
|
+
* #85), flattened across every fragment in declaration order: each an atomic `ADD`
|
|
3372
|
+
* (`UpdateItem`) on a target counter (e.g. `User.postCount += 1`). Present
|
|
3373
|
+
* (non-empty) only when a fragment's lifecycle declares `derive`; absent otherwise
|
|
3374
|
+
* (no regression). Each contributes an `UpdateItem` (`ADD`) to the method's atomic
|
|
3375
|
+
* `TransactWriteItems` and **promotes** the method to a transaction.
|
|
3376
|
+
*/
|
|
3377
|
+
readonly derivedUpdates?: readonly DerivedUpdate[];
|
|
3378
|
+
/**
|
|
3379
|
+
* The uniqueness guards derived from the mutation's `unique` effects (issue #86),
|
|
3380
|
+
* flattened across every fragment in declaration order. Present (non-empty) only
|
|
3381
|
+
* when a fragment's lifecycle declares `unique`; absent otherwise (no regression).
|
|
3382
|
+
* Each {@link DerivedUniqueGuard} contributes its marker-row `Put` / `Delete`
|
|
3383
|
+
* item(s) to the method's atomic `TransactWriteItems`, and their presence (like
|
|
3384
|
+
* {@link conditionChecks}) **promotes** an otherwise single-op method to a
|
|
3385
|
+
* transaction so a duplicate scoped value (the guard `Put`'s `attribute_not_exists`
|
|
3386
|
+
* failing) rolls the WHOLE write back — the entity is never created.
|
|
3387
|
+
*/
|
|
3388
|
+
readonly uniqueGuards?: readonly DerivedUniqueGuard[];
|
|
3389
|
+
/**
|
|
3390
|
+
* The outbox events derived from the mutation's `emits` effects (issue #87),
|
|
3391
|
+
* flattened across every fragment in declaration order. Present (non-empty) only
|
|
3392
|
+
* when a fragment's lifecycle declares `emits`; absent otherwise (no regression).
|
|
3393
|
+
* Each {@link DerivedOutboxEvent} contributes its transactional-outbox `Put` to the
|
|
3394
|
+
* method's atomic `TransactWriteItems` — recording the event ATOMICALLY with the
|
|
3395
|
+
* entity write (event and state cannot diverge) — and their presence (like
|
|
3396
|
+
* {@link conditionChecks}) **promotes** an otherwise single-op method to a
|
|
3397
|
+
* transaction. The outbox row is drainable via `src/cdc/`; delivery is out of scope.
|
|
3398
|
+
*/
|
|
3399
|
+
readonly outboxEvents?: readonly DerivedOutboxEvent[];
|
|
3400
|
+
/**
|
|
3401
|
+
* The client-token idempotency guard derived from the mutation's `idempotency`
|
|
3402
|
+
* effect (issue #87). Present only when a fragment's lifecycle declares
|
|
3403
|
+
* `idempotency`; absent otherwise (no regression). The {@link DerivedIdempotencyGuard}
|
|
3404
|
+
* contributes its `attribute_not_exists` guard `Put` to the method's atomic
|
|
3405
|
+
* `TransactWriteItems`, and its presence (like {@link conditionChecks}) **promotes**
|
|
3406
|
+
* an otherwise single-op method to a transaction so a same-token re-execution fails
|
|
3407
|
+
* the guard and rolls the WHOLE write back — no effect is double-applied.
|
|
3408
|
+
*/
|
|
3409
|
+
readonly idempotencyGuard?: DerivedIdempotencyGuard;
|
|
3410
|
+
/** @internal Phantom carrier retaining the formal `CommandMethod` type. */
|
|
3411
|
+
readonly __signature?: CommandMethod<TKey, TParams, TResult>;
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* A resolved **QueryModel** — a keyed public read contract holding one or more
|
|
3415
|
+
* named query methods. The Key (`TKey`) is the access pattern; each entry of
|
|
3416
|
+
* `methods` is a use case. Carries the contract `kind` and the resolved per-method
|
|
3417
|
+
* IR; the source method closures are discarded.
|
|
3418
|
+
*
|
|
3419
|
+
* @typeParam TKey - The contract Key (the access pattern / join key).
|
|
3420
|
+
* @typeParam M - The method map (name → resolved {@link QueryMethodSpec}).
|
|
3421
|
+
*/
|
|
3422
|
+
interface QueryModelContract<TKey, M extends Record<string, QueryMethodSpec<TKey, any, any>>> {
|
|
3423
|
+
/** @internal Marks this object as a contract IR node. */
|
|
3424
|
+
readonly __isContract: true;
|
|
3425
|
+
/** Discriminant: a read contract. */
|
|
3426
|
+
readonly kind: 'query';
|
|
3427
|
+
/** The resolved, named query methods (use cases over the same Key). */
|
|
3428
|
+
readonly methods: M;
|
|
3429
|
+
/** @internal Phantom carrier retaining the contract Key type `TKey`. */
|
|
3430
|
+
readonly __keyType?: (value: TKey) => void;
|
|
3431
|
+
}
|
|
3432
|
+
/**
|
|
3433
|
+
* A resolved **CommandModel** — the write counterpart of {@link QueryModelContract},
|
|
3434
|
+
* completely symmetric: keyed, holding one or more named write-use-case methods.
|
|
3435
|
+
*
|
|
3436
|
+
* @typeParam TKey - The contract Key (the target identity).
|
|
3437
|
+
* @typeParam M - The method map (name → resolved {@link CommandMethodSpec}).
|
|
3438
|
+
*/
|
|
3439
|
+
interface CommandModelContract<TKey, M extends Record<string, CommandMethodSpec<TKey, any, any>>> {
|
|
3440
|
+
/** @internal Marks this object as a contract IR node. */
|
|
3441
|
+
readonly __isContract: true;
|
|
3442
|
+
/** Discriminant: a write contract. */
|
|
3443
|
+
readonly kind: 'command';
|
|
3444
|
+
/** The resolved, named command methods (write use cases over the same Key). */
|
|
3445
|
+
readonly methods: M;
|
|
3446
|
+
/** @internal Phantom carrier retaining the contract Key type `TKey`. */
|
|
3447
|
+
readonly __keyType?: (value: TKey) => void;
|
|
3448
|
+
}
|
|
3449
|
+
/** Runtime guard: is `value` a {@link QueryModelContract}? */
|
|
3450
|
+
declare function isQueryModelContract(value: unknown): value is QueryModelContract<unknown, Record<string, QueryMethodSpec<unknown, unknown, unknown>>>;
|
|
3451
|
+
/** Runtime guard: is `value` a {@link CommandModelContract}? */
|
|
3452
|
+
declare function isCommandModelContract(value: unknown): value is CommandModelContract<unknown, Record<string, CommandMethodSpec<unknown, unknown, unknown>>>;
|
|
3453
|
+
/** The entity type a descriptor {@link ModelRef} (model class / `.asModel()` / thunk) targets. */
|
|
3454
|
+
type EntityOfRef<M> = M extends ModelStatic<infer T, infer _C> ? T : M extends abstract new (...args: never[]) => infer T ? T : M extends () => infer R ? EntityOfRef<R> : unknown;
|
|
3455
|
+
/** The projected item type for an entity `T` and a select `S` (relation-nest aware). */
|
|
3456
|
+
type QueryResultFor$1<T, S> = T extends DDBModel ? S extends Record<string, unknown> ? QueryResult<T, ProjectionOf<T, S>> : Record<string, unknown> : Record<string, unknown>;
|
|
3457
|
+
/** The TS value type a descriptor leaf (`param.*` or a literal) represents. */
|
|
3458
|
+
type LeafValueOf<L> = L extends Param<infer V> ? V : L;
|
|
3459
|
+
/** The contract Key inferred from a descriptor `key` record (its `param.*` / literal leaves). */
|
|
3460
|
+
type KeyOf<KeyRec> = {
|
|
3461
|
+
readonly [F in keyof KeyRec]: LeafValueOf<KeyRec[F]>;
|
|
3462
|
+
};
|
|
3463
|
+
/** The params a descriptor method accepts: the union of its `key` + `input` leaf value types. */
|
|
3464
|
+
type DescriptorParamsOf<D> = (D extends {
|
|
3465
|
+
readonly key: infer K;
|
|
3466
|
+
} ? KeyOf<K> : Record<never, never>) & (D extends {
|
|
3467
|
+
readonly input: infer I;
|
|
3468
|
+
} ? {
|
|
3469
|
+
readonly [F in keyof I]: LeafValueOf<I[F]>;
|
|
3470
|
+
} : Record<never, never>);
|
|
3471
|
+
/** The result of a read descriptor: a projected item (`query` → item | null, `list` → connection). */
|
|
3472
|
+
type ReadDescriptorResultOf<D> = D extends {
|
|
3473
|
+
readonly query: infer M;
|
|
3474
|
+
readonly select: infer S;
|
|
3475
|
+
} ? QueryResultFor$1<EntityOfRef<M>, S> | null : D extends {
|
|
3476
|
+
readonly list: infer M;
|
|
3477
|
+
readonly select: infer S;
|
|
3478
|
+
} ? {
|
|
3479
|
+
items: QueryResultFor$1<EntityOfRef<M>, S>[];
|
|
3480
|
+
cursor: string | null;
|
|
3481
|
+
} : unknown;
|
|
3482
|
+
/** A read descriptor's contract Key (from its `key` record). */
|
|
3483
|
+
type ReadDescriptorKeyOf<D> = D extends {
|
|
3484
|
+
readonly key: infer K;
|
|
3485
|
+
} ? KeyOf<K> : unknown;
|
|
3486
|
+
/** The result of a write descriptor: the read-back projection (when `result.select`) or void. */
|
|
3487
|
+
type WriteDescriptorResultOf<D> = D extends {
|
|
3488
|
+
readonly result: {
|
|
3489
|
+
readonly select: infer S;
|
|
3490
|
+
};
|
|
3491
|
+
} ? S extends Record<string, unknown> ? Readonly<Record<Extract<keyof S, string>, unknown>> | null : void : void;
|
|
3492
|
+
/** A write descriptor's contract Key (from its `key` record). */
|
|
3493
|
+
type WriteDescriptorKeyOf<D> = D extends {
|
|
3494
|
+
readonly key: infer K;
|
|
3495
|
+
} ? KeyOf<K> : unknown;
|
|
3496
|
+
/**
|
|
3497
|
+
* The query-method map accepted by the #101 **direct** form
|
|
3498
|
+
* (`publicQueryModel({ get: descriptor, … })`): each value is a
|
|
3499
|
+
* {@link PublicReadDescriptor}. The contract Key and per-method result are inferred
|
|
3500
|
+
* from each descriptor's `key` / `select`.
|
|
3501
|
+
*/
|
|
3502
|
+
type ReadDescriptorMap = Record<string, PublicReadDescriptor>;
|
|
3503
|
+
/** Resolve a read-descriptor map to its {@link QueryModelContract}. */
|
|
3504
|
+
type ResolvedReadDescriptors<M extends ReadDescriptorMap> = QueryModelContract<M[keyof M] extends infer D ? ReadDescriptorKeyOf<D> : unknown, {
|
|
3505
|
+
readonly [K in keyof M]: QueryMethodSpec<ReadDescriptorKeyOf<M[K]>, DescriptorParamsOf<M[K]>, ReadDescriptorResultOf<M[K]>>;
|
|
3506
|
+
}>;
|
|
3507
|
+
/**
|
|
3508
|
+
* The command-method map accepted by the #101 **direct** form
|
|
3509
|
+
* (`publicCommandModel({ create: descriptor, … })`): each value is a
|
|
3510
|
+
* {@link PublicWriteDescriptor}, a {@link PublicComposeDescriptor} (a `mutation()`
|
|
3511
|
+
* wrapped with `input` / `result` / `mode`), or a bare composite `mutation()`
|
|
3512
|
+
* {@link CommandPlan}.
|
|
3513
|
+
*/
|
|
3514
|
+
type WriteDescriptorMap = Record<string, PublicWriteDescriptor | PublicComposeDescriptor | CommandPlan>;
|
|
3515
|
+
/** Resolve a write-descriptor map to its {@link CommandModelContract}. */
|
|
3516
|
+
type ResolvedWriteDescriptors<M extends WriteDescriptorMap> = CommandModelContract<M[keyof M] extends infer D ? WriteDescriptorKeyOf<D> : unknown, {
|
|
3517
|
+
readonly [K in keyof M]: CommandMethodSpec<WriteDescriptorKeyOf<M[K]>, DescriptorParamsOf<M[K]>, WriteDescriptorResultOf<M[K]>>;
|
|
3518
|
+
}>;
|
|
3519
|
+
/**
|
|
3520
|
+
* Create a **QueryModel** from a **descriptor map** (issue #101, the only form):
|
|
3521
|
+
*
|
|
3522
|
+
* ```ts
|
|
3523
|
+
* export const UserQueries = publicQueryModel({
|
|
3524
|
+
* get: { query: User, key: { userId: param.string() }, select: { name: true } },
|
|
3525
|
+
* list: { list: GroupMembership, key: { groupId: param.string() }, select: { role: true } },
|
|
3526
|
+
* });
|
|
3527
|
+
* ```
|
|
3528
|
+
*
|
|
3529
|
+
* The single argument is the method map itself; the contract Key and each method's
|
|
3530
|
+
* result type are **inferred** from the descriptors' `key` / `select` — there is no
|
|
3531
|
+
* generic key parameter, no curried factory call, and no explicit Key-field list
|
|
3532
|
+
* (the legacy `publicQueryModel<TKey>(['email'])` / model-derived overloads were
|
|
3533
|
+
* withdrawn in #101). A GSI-keyed point read is expressed by its descriptor `key`
|
|
3534
|
+
* (e.g. `key: { email: param.string() }`); the build resolves the access pattern
|
|
3535
|
+
* from the model metadata, exactly as the curried key-field list once did. Each
|
|
3536
|
+
* descriptor's `key` fields carry `param.*` placeholders (the public input schema);
|
|
3537
|
+
* a `select` is the existing query projection (relation-nest aware), and may carry
|
|
3538
|
+
* External Query `query(...)` nodes.
|
|
3539
|
+
*/
|
|
3540
|
+
declare function publicQueryModel<const M extends ReadDescriptorMap>(methods: M): ResolvedReadDescriptors<M>;
|
|
3541
|
+
/**
|
|
3542
|
+
* Create a **CommandModel** from a **descriptor map** (issue #101, the only form):
|
|
3543
|
+
*
|
|
3544
|
+
* ```ts
|
|
3545
|
+
* export const MembershipCommands = publicCommandModel({
|
|
3546
|
+
* create: { create: GroupMembership, key: { groupId: param.string(), userId: param.string() },
|
|
3547
|
+
* input: { role: param.string() }, result: { select: { role: true } } },
|
|
3548
|
+
* addMany: { create: GroupMembership, key: { … }, input: { … }, mode: 'parallel' },
|
|
3549
|
+
* join: mutation($ => ({ … })), // composite (multi-fragment, atomic) method
|
|
3550
|
+
* });
|
|
3551
|
+
* ```
|
|
3552
|
+
*
|
|
3553
|
+
* The single argument is the method map itself; the contract Key and each method's
|
|
3554
|
+
* read-back result are **inferred** from the descriptors' `key` / `result.select` —
|
|
3555
|
+
* there is no generic key parameter, no curried factory call, and no explicit
|
|
3556
|
+
* Key-field list (the legacy `publicCommandModel<TKey>()` / model-derived overloads
|
|
3557
|
+
* were withdrawn in #101). A descriptor's `mode` declares the per-call execution
|
|
3558
|
+
* mode (`'transaction'` default | `'parallel'`); `result` presence drives whether
|
|
3559
|
+
* the method reads back and returns the written entity.
|
|
3560
|
+
*/
|
|
3561
|
+
declare function publicCommandModel<const M extends WriteDescriptorMap>(methods: M): ResolvedWriteDescriptors<M>;
|
|
3562
|
+
declare const PLANNED_COMMAND_BRAND: unique symbol;
|
|
3563
|
+
/**
|
|
3564
|
+
* The input param shape a {@link command} declares: a record of named
|
|
3565
|
+
* {@link Param} placeholders (`param.string()` / `param.number()` /
|
|
3566
|
+
* `param.literal(...)`). These are the **external** params of the public Command
|
|
3567
|
+
* IF — the method's caller-supplied input. The mutation fragment binds them by
|
|
3568
|
+
* name into the written entity (`$.<field>`), so each input field name must match
|
|
3569
|
+
* a fragment `$.field` reference / a model field the fragment writes.
|
|
3570
|
+
*/
|
|
3571
|
+
type CommandInputShape = Readonly<Record<string, Param<unknown>>>;
|
|
3572
|
+
/**
|
|
3573
|
+
* The return projection a {@link command} declares (proposal §3: "return = read
|
|
3574
|
+
* projection"): a JSON-safe boolean field map applied to the written entity as a
|
|
3575
|
+
* **consistent read-back** after the write commits. Omit for a fire-and-forget
|
|
3576
|
+
* command (no projected item is returned).
|
|
3577
|
+
*/
|
|
3578
|
+
type CommandSelectShape = Readonly<Record<string, boolean>>;
|
|
3579
|
+
/**
|
|
3580
|
+
* A **planned command method** (#83): the branded result of
|
|
3581
|
+
* `command({ input, select }).plan(mutation)`. It carries the captured fragment
|
|
3582
|
+
* IR (the {@link CommandPlan}), the public input param shape, and the return
|
|
3583
|
+
* `select`. {@link buildCommandContract} detects the brand and routes it to the
|
|
3584
|
+
* single-fragment mutation compiler (NOT through #64's closure path).
|
|
3585
|
+
*
|
|
3586
|
+
* The external surface is **params in / result out only** — the internal mutation
|
|
3587
|
+
* document is never exposed.
|
|
3588
|
+
*
|
|
3589
|
+
* @typeParam TInput - The input param shape (`{ field: Param<…> }`).
|
|
3590
|
+
* @typeParam TSelect - The return projection (`{ field: boolean }`), or `undefined`.
|
|
3591
|
+
*/
|
|
3592
|
+
interface PlannedCommandMethod<TInput extends CommandInputShape = CommandInputShape, TSelect extends CommandSelectShape | undefined = CommandSelectShape | undefined> {
|
|
3593
|
+
readonly [PLANNED_COMMAND_BRAND]: true;
|
|
3594
|
+
/** The captured mutation IR (the fragment list). */
|
|
3595
|
+
readonly plan: CommandPlan;
|
|
3596
|
+
/** The public input param shape. */
|
|
3597
|
+
readonly input: TInput;
|
|
3598
|
+
/** The return projection (consistent read-back), or `undefined`. */
|
|
3599
|
+
readonly select: TSelect;
|
|
3600
|
+
}
|
|
3601
|
+
/** Runtime guard: is `value` a {@link PlannedCommandMethod} (#83)? */
|
|
3602
|
+
declare function isPlannedCommandMethod(value: unknown): value is PlannedCommandMethod;
|
|
3603
|
+
/** The per-call execution mode of a command method (#101): replaces `batch`. */
|
|
3604
|
+
type CommandMode = 'transaction' | 'parallel';
|
|
3605
|
+
/** A public **read** descriptor (issue #101): `{ query | list: Model, key, select, options? }`. */
|
|
3606
|
+
interface PublicReadDescriptor {
|
|
3607
|
+
readonly query?: ModelRef;
|
|
3608
|
+
readonly list?: ModelRef;
|
|
3609
|
+
readonly key: Readonly<Record<string, unknown>>;
|
|
3610
|
+
readonly select: Readonly<Record<string, unknown>>;
|
|
3611
|
+
readonly options?: Readonly<Record<string, unknown>>;
|
|
3612
|
+
}
|
|
3613
|
+
/** A public **write** descriptor (issue #101): `{ create | update | remove: Model, key, input?, condition?, result?, mode? }`. */
|
|
3614
|
+
interface PublicWriteDescriptor {
|
|
3615
|
+
readonly create?: ModelRef;
|
|
3616
|
+
readonly update?: ModelRef;
|
|
3617
|
+
readonly remove?: ModelRef;
|
|
3618
|
+
readonly key: Readonly<Record<string, unknown>>;
|
|
3619
|
+
readonly input?: Readonly<Record<string, unknown>>;
|
|
3620
|
+
readonly condition?: Readonly<Record<string, unknown>>;
|
|
3621
|
+
readonly result?: {
|
|
3622
|
+
readonly select?: Readonly<Record<string, boolean>>;
|
|
3623
|
+
readonly options?: unknown;
|
|
3624
|
+
};
|
|
3625
|
+
readonly mode?: CommandMode;
|
|
3626
|
+
}
|
|
3627
|
+
/**
|
|
3628
|
+
* A public **composite write** descriptor (issue #101) — the closure-free twin of
|
|
3629
|
+
* `command({ input, select }).plan(mutation)`. It wraps an internal
|
|
3630
|
+
* {@link CommandPlan} (`mutation(...)` / `definePlan(...)`) with the public input
|
|
3631
|
+
* param shape, the optional read-back projection, and the optional execution mode.
|
|
3632
|
+
* The mutation document is never exposed; only `input` params + `result.select`
|
|
3633
|
+
* cross the boundary.
|
|
3634
|
+
*/
|
|
3635
|
+
interface PublicComposeDescriptor {
|
|
3636
|
+
readonly command: CommandPlan;
|
|
3637
|
+
readonly input?: Readonly<Record<string, unknown>>;
|
|
3638
|
+
readonly result?: {
|
|
3639
|
+
readonly select?: Readonly<Record<string, boolean>>;
|
|
3640
|
+
readonly options?: unknown;
|
|
3641
|
+
};
|
|
3642
|
+
readonly mode?: CommandMode;
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
/**
|
|
3646
|
+
* Single-service contract **Runtime** — execution of CQRS query-contract methods
|
|
3647
|
+
* (issue #62, Epic #57; spec `docs/cqrs-contract.md`, "Query Method
|
|
3648
|
+
* — Common Interface" + "Cardinality matrix"). READS only; command/write
|
|
3649
|
+
* execution is #64 and single-contract composition / External Query is #63 — both
|
|
3650
|
+
* out of scope here.
|
|
3651
|
+
*
|
|
3652
|
+
* The Runtime is the **execution engine**: given a resolved query contract (a
|
|
3653
|
+
* {@link QueryModelContract} from #58, carrying per-method `resolution` /
|
|
3654
|
+
* `inputArity` and a resolved {@link ContractMethodOp}) it executes one method
|
|
3655
|
+
* for a single key **or** an array of keys and returns the correct result shape
|
|
3656
|
+
* from the proposal's cardinality matrix:
|
|
3657
|
+
*
|
|
3658
|
+
* | Input | `resolution` | Result shape | Notation |
|
|
3659
|
+
* | -------------- | ------------ | ------------------------------------- | ---------------- |
|
|
3660
|
+
* | `key` (single) | `point` | `Item \| null` | `key1 : result1` |
|
|
3661
|
+
* | `key` (single) | `range` | `Connection` (`{ items, cursor }`) | `key1 : resultM` |
|
|
3662
|
+
* | `keys[]` (N) | `point` | keyed `Map<keyId, Item \| null>` | `keyN : resultN` |
|
|
3663
|
+
* | `keys[]` (N) | `range` | **unreachable for a single contract** | `keyN : resultN×M` |
|
|
3664
|
+
*
|
|
3665
|
+
* ## On `keyN : resultN×M` (range + array) — unreachable here, by design
|
|
3666
|
+
*
|
|
3667
|
+
* The fourth matrix cell — a keyed map of connections — is **not reachable for a
|
|
3668
|
+
* single contract** at the direct-call surface. The proposal's N+1 Safety rule
|
|
3669
|
+
* fixes a `range` method's `inputArity` to `'single'` ("A `range` method is
|
|
3670
|
+
* necessarily `'single'`"; "feeding an array of keys into a range method … is an
|
|
3671
|
+
* N+1 fan-out and is rejected at build time, with no opt-in escape hatch"). #58's
|
|
3672
|
+
* {@link inputArityOf} derives exactly that, and #60's N+1 checker rejects any
|
|
3673
|
+
* contract that violates it before it reaches the SSoT. So a range method only
|
|
3674
|
+
* ever accepts one key, and this runtime **rejects** an array fed into a `range`
|
|
3675
|
+
* method ({@link executeQueryMethod}) rather than fanning out. `keyN : resultN×M`
|
|
3676
|
+
* arises only under cross-contract composition (a `point` parent referencing a
|
|
3677
|
+
* child whose per-key result is a connection) — that is #63's External Query
|
|
3678
|
+
* territory, explicitly out of scope for #62. We therefore implement the **three
|
|
3679
|
+
* reachable shapes** and guard the fourth with a clear error, rather than
|
|
3680
|
+
* silently fanning out a forbidden N+1.
|
|
3681
|
+
*
|
|
3682
|
+
* ## Reuse, not reinvention
|
|
3683
|
+
*
|
|
3684
|
+
* Execution delegates to the proven machinery:
|
|
3685
|
+
*
|
|
3686
|
+
* - `point` single → {@link executeQuery} (GetItem / unique Query).
|
|
3687
|
+
* - `point` array → {@link executeKeyedBatchGet} below, which reuses the
|
|
3688
|
+
* `batch-retry` primitives (chunk ≤100, dedup, UnprocessedKeys retry) and
|
|
3689
|
+
* builds the **keyed** map (BatchGet neither preserves order nor returns absent
|
|
3690
|
+
* keys, so missing keys are filled with explicit `null`).
|
|
3691
|
+
* - `range` single → {@link executeListInternal} for a `{ items, cursor }`
|
|
3692
|
+
* connection, with the cursor wrapped in a per-key envelope
|
|
3693
|
+
* ({@link encodePerKeyCursor}) so it carries the key it belongs to.
|
|
3694
|
+
*
|
|
3695
|
+
* The result-shape typing (single → bare, array → keyed) is expressed as call
|
|
3696
|
+
* overloads on {@link executeQueryMethod}.
|
|
3697
|
+
*/
|
|
3698
|
+
|
|
3699
|
+
/**
|
|
3700
|
+
* The **structural** shape of a resolved query contract the runtime executes.
|
|
3701
|
+
* It depends only on the facts the executor reads — the `query` discriminant and
|
|
3702
|
+
* the per-method `resolution` and internal `op` — and is deliberately
|
|
3703
|
+
* **independent of the phantom Key / params / result types** carried by
|
|
3704
|
+
* {@link QueryModelContract}. Those phantom types are invariant /
|
|
3705
|
+
* contravariant on the full interface, so a concrete `QueryModelContract<{…}>`
|
|
3706
|
+
* is not assignable to `QueryModelContract<unknown, …>`; this minimal structural
|
|
3707
|
+
* type sidesteps that variance while accepting every real contract (a
|
|
3708
|
+
* {@link QueryModelContract} is structurally assignable to it). The runtime never
|
|
3709
|
+
* needs the method param / result types — it returns the dynamic
|
|
3710
|
+
* cardinality-matrix shapes ({@link ContractItem} / {@link Connection} /
|
|
3711
|
+
* {@link KeyedResult}).
|
|
3712
|
+
*/
|
|
3713
|
+
interface ExecutableQueryContract {
|
|
3714
|
+
readonly kind: 'query';
|
|
3715
|
+
readonly methods: Readonly<Record<string, {
|
|
3716
|
+
readonly resolution: Resolution;
|
|
3717
|
+
/**
|
|
3718
|
+
* The method's decided input arity (issue #71): `'either'` for a
|
|
3719
|
+
* coalescible base-table `point` (a key array → one `BatchGetItem`),
|
|
3720
|
+
* `'single'` for a `range` (partition `Query`) **or** a unique-GSI `point`
|
|
3721
|
+
* (a per-key GSI `Query`, not coalescible). The executor honors it to
|
|
3722
|
+
* reject an array fed into any `'single'` method rather than fanning out.
|
|
3723
|
+
*/
|
|
3724
|
+
readonly inputArity: InputArity;
|
|
3725
|
+
readonly op: ContractMethodOp;
|
|
3726
|
+
}>>;
|
|
3727
|
+
}
|
|
3728
|
+
/** A plain contract key (the access pattern / join key), e.g. `{ articleId }`. */
|
|
3729
|
+
type ContractKeyInput = Record<string, unknown>;
|
|
3730
|
+
/**
|
|
3731
|
+
* The literal {@link InputArity} the contract `C` carries for method `K`. A
|
|
3732
|
+
* {@link QueryMethodSpec} carries the method's literal arity in its `inputArity`
|
|
3733
|
+
* type when the body was tagged with `point` / `range` (`src/define/contract.ts`);
|
|
3734
|
+
* an untagged method keeps the wide `InputArity`. Falls back to `InputArity` for a
|
|
3735
|
+
* structurally-typed contract (e.g. the bare {@link ExecutableQueryContract}),
|
|
3736
|
+
* which keeps the call surface array-capable (the runtime backstop still rejects an
|
|
3737
|
+
* array fed into a `'single'` method).
|
|
3738
|
+
*/
|
|
3739
|
+
type ArityOfMethod<C extends ExecutableQueryContract, K extends keyof C['methods']> = C['methods'][K] extends {
|
|
3740
|
+
readonly inputArity: infer A extends InputArity;
|
|
3741
|
+
} ? A : InputArity;
|
|
3742
|
+
/**
|
|
3743
|
+
* The **array** key-argument type accepted by `executeQueryMethod` for method `K`
|
|
3744
|
+
* of contract `C`. A method whose literal arity is `'single'` (a `range` read **or**
|
|
3745
|
+
* a unique-GSI `point` — both one request per key) accepts **no** array: the type
|
|
3746
|
+
* collapses to `never`, so passing an array is a `tsc` compile error at the
|
|
3747
|
+
* user-facing surface (issue #71, N+1 rule (a), the **primary** enforcement layer).
|
|
3748
|
+
* Any other arity (`'either'` / `'array'`, or the wide `InputArity` for an untagged
|
|
3749
|
+
* / structural contract) accepts an array of keys (it coalesces to one batched
|
|
3750
|
+
* call). This is the array-overload half of {@link ContractCallSignature}'s
|
|
3751
|
+
* narrowing, applied at the real execution surface.
|
|
3752
|
+
*/
|
|
3753
|
+
type ArrayKeyArg<C extends ExecutableQueryContract, K extends keyof C['methods']> = ArityOfMethod<C, K> extends 'single' ? never : readonly ContractKeyInput[];
|
|
3754
|
+
/** A single hydrated result item (only the method's projected fields). */
|
|
3755
|
+
type ContractItem = Record<string, unknown>;
|
|
3756
|
+
/**
|
|
3757
|
+
* A pagination connection: a page of items plus an opaque `cursor` for the next
|
|
3758
|
+
* page (`null` when the page is the last). For a `range` contract method the
|
|
3759
|
+
* cursor is a **per-key envelope** ({@link encodePerKeyCursor}) that identifies
|
|
3760
|
+
* the key it paginates.
|
|
3761
|
+
*/
|
|
3762
|
+
interface Connection<TItem = ContractItem> {
|
|
3763
|
+
readonly items: TItem[];
|
|
3764
|
+
readonly cursor: string | null;
|
|
3765
|
+
}
|
|
3766
|
+
/**
|
|
3767
|
+
* The keyed result of a batched contract read: input-key identity (the canonical
|
|
3768
|
+
* {@link serializeContractKey} string) → per-key result. Backed by a `Map` so
|
|
3769
|
+
* the association is explicit and order-independent; missing keys are present
|
|
3770
|
+
* with an explicit `null` value (`point`) rather than omitted.
|
|
3771
|
+
*
|
|
3772
|
+
* @typeParam TValue The per-key value (`Item | null` for `point`).
|
|
3773
|
+
*/
|
|
3774
|
+
type KeyedResult<TValue> = Map<string, TValue>;
|
|
3775
|
+
/**
|
|
3776
|
+
* Retrieval options accepted by a contract query method (the serializable subset
|
|
3777
|
+
* from the proposal). All optional; a `point` method ignores the pagination
|
|
3778
|
+
* fields, a `range` method honors them.
|
|
3779
|
+
*/
|
|
3780
|
+
interface ContractQueryParams {
|
|
3781
|
+
/** Strongly-consistent read (point reads on the base table only). */
|
|
3782
|
+
readonly consistentRead?: boolean;
|
|
3783
|
+
/** Page size for a `range` (`list`) method. */
|
|
3784
|
+
readonly limit?: number;
|
|
3785
|
+
/** Resume token — a per-key cursor envelope from a prior `range` page. */
|
|
3786
|
+
readonly after?: string;
|
|
3787
|
+
/** Sort direction for a `range` (`list`) method. */
|
|
3788
|
+
readonly order?: 'ASC' | 'DESC';
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3791
|
+
* Maximum number of in-flight per-key queries for a batched `range` fan-out.
|
|
3792
|
+
* Reuses the relation-traversal cap — the same rationale (bound in-flight
|
|
3793
|
+
* requests, ride out throttling via UnprocessedKeys retry beneath the cap). Note
|
|
3794
|
+
* that a single-contract `range` method is `inputArity: 'single'`, so the
|
|
3795
|
+
* fan-out path is only exercised by the (out-of-scope, #63) composition runtime;
|
|
3796
|
+
* the bound is defined here so the seam is ready and tested in isolation.
|
|
3797
|
+
*/
|
|
3798
|
+
declare const CONTRACT_RANGE_FANOUT_CONCURRENCY = 16;
|
|
3799
|
+
/**
|
|
3800
|
+
* Keyed BatchGet: resolve `keys` against `modelClass` in one batched call and
|
|
3801
|
+
* return a `Map<keyId, Item | null>` with every input key present (misses →
|
|
3802
|
+
* `null`). Reuses the `batch-retry` primitives (chunk ≤100, dedup, UnprocessedKeys
|
|
3803
|
+
* retry) — never reinvented.
|
|
3804
|
+
*
|
|
3805
|
+
* Implementation notes:
|
|
3806
|
+
* - **Dedup**: the same key supplied twice is fetched once; both map entries
|
|
3807
|
+
* point at the same resolved item (or `null`).
|
|
3808
|
+
* - **Order / absence**: BatchGet returns matched items unordered and omits
|
|
3809
|
+
* misses, so each returned raw item is indexed by its `PK::SK` and looked up
|
|
3810
|
+
* per input key; an unmatched key gets `null`.
|
|
3811
|
+
* - **Projection**: items are fetched whole (DynamoDB BatchGet has no reliable
|
|
3812
|
+
* server-side projection for mixed keys here) and hydrated against the
|
|
3813
|
+
* method's `select`, so the per-key item carries exactly the projected fields.
|
|
3814
|
+
*/
|
|
3815
|
+
declare function executeKeyedBatchGet(modelClass: Function, keys: readonly ContractKeyInput[], select: Record<string, unknown>): Promise<KeyedResult<ContractItem | null>>;
|
|
3816
|
+
/**
|
|
3817
|
+
* Execute a query-contract method for a **single** key.
|
|
3818
|
+
*
|
|
3819
|
+
* The result shape is determined by the method's derived `resolution`:
|
|
3820
|
+
* - `point` → `Item | null` (`key1 : result1`);
|
|
3821
|
+
* - `range` → `Connection` (`key1 : resultM`).
|
|
3822
|
+
*
|
|
3823
|
+
* A single key is accepted for **every** method regardless of arity.
|
|
3824
|
+
*
|
|
3825
|
+
* @param contract The resolved query contract (a {@link QueryModelContract}).
|
|
3826
|
+
* @param methodName The method to execute (a method name of `contract`).
|
|
3827
|
+
* @param key A single contract key.
|
|
3828
|
+
* @param params Retrieval options (pagination / consistency).
|
|
3829
|
+
*/
|
|
3830
|
+
declare function executeQueryMethod<C extends ExecutableQueryContract, K extends keyof C['methods'] & string>(contract: C, methodName: K, key: ContractKeyInput, params?: ContractQueryParams): Promise<ContractItem | null | Connection>;
|
|
3831
|
+
/**
|
|
3832
|
+
* Execute a query-contract method for an **array** of keys (a single batched
|
|
3833
|
+
* call). Valid **only** for a method whose literal arity is not `'single'` — a
|
|
3834
|
+
* coalescible base-table `point` (`inputArity: 'either'`, a key array → one
|
|
3835
|
+
* `BatchGetItem`). A `'single'` method (a `range` read **or** a unique-GSI `point`)
|
|
3836
|
+
* makes the array key argument `never`, so passing an array is a **`tsc` compile
|
|
3837
|
+
* error** by construction (issue #71, N+1 rule (a) — the primary enforcement
|
|
3838
|
+
* layer; the build-time checker and runtime backstop remain). The result is a keyed
|
|
3839
|
+
* `Map<keyId, Item | null>` (`keyN : resultN`).
|
|
3840
|
+
*
|
|
3841
|
+
* @param contract The resolved query contract.
|
|
3842
|
+
* @param methodName The method to execute (a method name of `contract`).
|
|
3843
|
+
* @param keys An array of contract keys (rejected for a `'single'` method).
|
|
3844
|
+
* @param params Retrieval options.
|
|
3845
|
+
*/
|
|
3846
|
+
declare function executeQueryMethod<C extends ExecutableQueryContract, K extends keyof C['methods'] & string>(contract: C, methodName: K, keys: ArrayKeyArg<C, K>, params?: ContractQueryParams): Promise<KeyedResult<ContractItem | null>>;
|
|
3847
|
+
/**
|
|
3848
|
+
* The runtime fan-out seam for a batched `range` (`keyN : resultN×M`). NOT
|
|
3849
|
+
* reachable for a single contract under #62 (a `range` method's `inputArity` is
|
|
3850
|
+
* `'single'`, so {@link executeQueryMethod} rejects an array). It is the
|
|
3851
|
+
* cross-contract composition runtime (#63) that drives a per-key range fan-out;
|
|
3852
|
+
* this function implements that fan-out — N bounded-parallel Queries grouped by
|
|
3853
|
+
* input key, each connection carrying its own per-key cursor — so the seam is
|
|
3854
|
+
* defined, reused (via {@link mapWithConcurrency}), and unit-tested in isolation
|
|
3855
|
+
* ahead of #63, rather than left as a stub. #62 does not call it on any contract
|
|
3856
|
+
* surface.
|
|
3857
|
+
*
|
|
3858
|
+
* @param op The `range` method op to run per key.
|
|
3859
|
+
* @param keys The parent keys to fan out over.
|
|
3860
|
+
* @param params Retrieval options applied to **every** key's connection.
|
|
3861
|
+
*/
|
|
3862
|
+
declare function executeRangeFanout(op: ContractMethodOp, keys: readonly ContractKeyInput[], params?: ContractQueryParams): Promise<KeyedResult<Connection>>;
|
|
3863
|
+
|
|
3864
|
+
/**
|
|
3865
|
+
* Single-service contract **Runtime** — execution of CQRS command-contract
|
|
3866
|
+
* methods (issue #64, Epic #57; spec `docs/cqrs-contract.md`,
|
|
3867
|
+
* "Command Contract" + "Command Definition"). WRITES only; query reads are #62
|
|
3868
|
+
* ({@link executeQueryMethod}) and cross-contract composition is #63 — both out
|
|
3869
|
+
* of scope here.
|
|
3870
|
+
*
|
|
3871
|
+
* Given a resolved command contract (a {@link CommandModelContract} from #58,
|
|
3872
|
+
* carrying per-method `result` and the declared {@link CommandBatchMode}, plus a
|
|
3873
|
+
* resolved write {@link ContractMethodOp}) it executes one method for a **single
|
|
3874
|
+
* key** or an **array of keys** and applies the write(s) to DynamoDB:
|
|
3875
|
+
*
|
|
3876
|
+
* | Input | resolution |
|
|
3877
|
+
* | -------------- | ----------------------------------------------------------- |
|
|
3878
|
+
* | `key` (single) | one write op (`put` / `update` / `delete`) |
|
|
3879
|
+
* | `keys[]` (N) | a **batched** write per the method's declared `batch` mode |
|
|
3880
|
+
*
|
|
3881
|
+
* The batched form is **declared by the contract** (proposal "Consistency with
|
|
3882
|
+
* the existing write surface"):
|
|
3883
|
+
*
|
|
3884
|
+
* - `'transact'` — one `TransactWriteItems`: the per-key write applied
|
|
3885
|
+
* **atomically** (all-or-nothing) and **condition-capable** ({@link ConditionInput},
|
|
3886
|
+
* `{ notExists }` / equality). DynamoDB caps `TransactWriteItems` at **25** and
|
|
3887
|
+
* the call is atomic, so an array of **>25 keys cannot be split** without
|
|
3888
|
+
* breaking atomicity — this runtime **rejects** it with a clear error rather
|
|
3889
|
+
* than silently truncating or chunking ({@link MAX_TRANSACT_ITEMS}).
|
|
3890
|
+
* - `'batchWrite'` — a `BatchWriteItem`: the per-key writes applied
|
|
3891
|
+
* **non-atomically**, **without conditions** (DynamoDB's `BatchWriteItem` has no
|
|
3892
|
+
* `ConditionExpression`). An array of any size is permitted; it is chunked per
|
|
3893
|
+
* the 25-items-per-request limit with `UnprocessedItems` retry, reusing the
|
|
3894
|
+
* existing batch machinery ({@link executeBatchWrite}).
|
|
3895
|
+
* - **undefined** — the method declared no batched form: an array is rejected (it
|
|
3896
|
+
* resolves only a single key).
|
|
3897
|
+
*
|
|
3898
|
+
* ## Reuse, not reinvention
|
|
3899
|
+
*
|
|
3900
|
+
* The recorded write op carries declarative sentinels ({@link ContractParamRef} /
|
|
3901
|
+
* {@link ContractKeyFieldRef}) at its value positions; {@link renderRecord} binds
|
|
3902
|
+
* them to the concrete `(key, params)` at execution time, producing the very same
|
|
3903
|
+
* `item` / `changes` / `key` / `condition` objects a hand-written `Model.putItem` /
|
|
3904
|
+
* `Model.updateItem` / `Model.deleteItem` would pass. Execution then delegates to the
|
|
3905
|
+
* proven write machinery:
|
|
3906
|
+
*
|
|
3907
|
+
* - single → {@link executePut} / {@link executeUpdate} / {@link executeDelete};
|
|
3908
|
+
* - transact → one `TransactWriteItems` built from `buildPutInput` /
|
|
3909
|
+
* `buildUpdateInput` / `buildDeleteInput` (the same inputs the imperative
|
|
3910
|
+
* {@link TransactionContext} uses), with the ≤25 limit enforced;
|
|
3911
|
+
* - batchWrite → {@link executeBatchWrite} (chunk ≤25, `UnprocessedItems` retry).
|
|
3912
|
+
*
|
|
3913
|
+
* The TS and Python runtimes produce identical effects for the same SSoT.
|
|
3914
|
+
*/
|
|
3915
|
+
|
|
3916
|
+
/**
|
|
3917
|
+
* The **structural** shape of a resolved command contract the runtime executes.
|
|
3918
|
+
* It depends only on the facts the executor reads — the `command` discriminant
|
|
3919
|
+
* and the per-method `op` / `batch` / `result` — and is deliberately
|
|
3920
|
+
* **independent of the phantom Key / params / result types** carried by
|
|
3921
|
+
* {@link CommandModelContract} (those are invariant on the full interface, so a
|
|
3922
|
+
* concrete `CommandModelContract<{…}>` is not assignable to
|
|
3923
|
+
* `CommandModelContract<unknown, …>`). Every real command contract is
|
|
3924
|
+
* structurally assignable to this minimal type.
|
|
3925
|
+
*/
|
|
3926
|
+
interface ExecutableCommandContract {
|
|
3927
|
+
readonly kind: 'command';
|
|
3928
|
+
readonly methods: Readonly<Record<string, {
|
|
3929
|
+
readonly op: ContractMethodOp;
|
|
3930
|
+
readonly result: CommandResultKind;
|
|
3931
|
+
/**
|
|
3932
|
+
* The #101 first-class per-call execution mode (`'transaction'` |
|
|
3933
|
+
* `'parallel'`). Drives the key-array bulk form directly (`'transaction'`
|
|
3934
|
+
* → atomic `TransactWriteItems`; `'parallel'` → non-atomic per-key fan-out
|
|
3935
|
+
* with partial success, coalescing unconditioned put/delete into a
|
|
3936
|
+
* `BatchWriteItem`).
|
|
3937
|
+
*/
|
|
3938
|
+
readonly mode?: CommandMode;
|
|
3939
|
+
/**
|
|
3940
|
+
* The composed write ops of a **multi-fragment** `mutation`-derived method
|
|
3941
|
+
* (issue #90). When present (N≥2), a **single-key** call executes ALL of
|
|
3942
|
+
* them as **one atomic `TransactWriteItems`** (not sequential writes), so a
|
|
3943
|
+
* condition failure on any fragment rolls back the whole transaction. Absent
|
|
3944
|
+
* for a single-fragment / #83 / hand-written #64 method (which executes its
|
|
3945
|
+
* one {@link op}). The read-back stays keyed on the primary op ({@link op}).
|
|
3946
|
+
*/
|
|
3947
|
+
readonly ops?: readonly ContractMethodOp[];
|
|
3948
|
+
/**
|
|
3949
|
+
* The referential-integrity assertions derived from the mutation's
|
|
3950
|
+
* `requires` effects (issue #84). When present (non-empty), a
|
|
3951
|
+
* **single-key** call composes the base write(s) **and** one read-only
|
|
3952
|
+
* `ConditionCheck` (`attribute_exists`) per assertion into **one atomic
|
|
3953
|
+
* `TransactWriteItems`** — so a single-op write is **promoted** to a
|
|
3954
|
+
* transaction. If any referenced entity is absent, its assertion fails and
|
|
3955
|
+
* the WHOLE transaction rolls back (the entity write never persists). Absent
|
|
3956
|
+
* on a mutation with no `requires` / a #64 hand-written method.
|
|
3957
|
+
*/
|
|
3958
|
+
readonly conditionChecks?: readonly DerivedConditionCheck[];
|
|
3959
|
+
/**
|
|
3960
|
+
* The adjacency edge writes derived from the mutation's `edges` effects
|
|
3961
|
+
* (issue #85). When present, a **single-key** call composes each edge's
|
|
3962
|
+
* adjacency `Put` / `Delete` item(s) into the **same atomic
|
|
3963
|
+
* `TransactWriteItems`** as the entity write (and any ConditionChecks /
|
|
3964
|
+
* derived updates), so the edge is created / removed / moved atomically with
|
|
3965
|
+
* the entity. Absent on a mutation with no `edges` / a #64 hand-written method.
|
|
3966
|
+
*/
|
|
3967
|
+
readonly edgeWrites?: readonly DerivedEdgeWrite[];
|
|
3968
|
+
/**
|
|
3969
|
+
* The derived counter updates from the mutation's `derive` effects (issue
|
|
3970
|
+
* #85): each an atomic `ADD` (`UpdateItem`) on a target counter (e.g.
|
|
3971
|
+
* `User.postCount += 1`). When present, a **single-key** call composes each
|
|
3972
|
+
* `ADD` into the same atomic `TransactWriteItems` as the entity write, so the
|
|
3973
|
+
* counter moves atomically (and never clobbers a concurrent increment — an
|
|
3974
|
+
* `ADD` needs no prior read). Absent on a mutation with no `derive`.
|
|
3975
|
+
*/
|
|
3976
|
+
readonly derivedUpdates?: readonly DerivedUpdate[];
|
|
3977
|
+
/**
|
|
3978
|
+
* The uniqueness guards derived from the mutation's `unique` effects (issue
|
|
3979
|
+
* #86): each a marker-row `Put` (`attribute_not_exists`) / `Delete` /
|
|
3980
|
+
* `Delete(old)+Put(new)` swap. When present, a **single-key** call composes
|
|
3981
|
+
* the guard item(s) into the same atomic `TransactWriteItems` as the entity
|
|
3982
|
+
* write, so a duplicate scoped value (the guard `Put`'s `attribute_not_exists`
|
|
3983
|
+
* failing) rolls the WHOLE transaction back — the entity is never created.
|
|
3984
|
+
* Absent on a mutation with no `unique` / a #64 hand-written method.
|
|
3985
|
+
*/
|
|
3986
|
+
readonly uniqueGuards?: readonly DerivedUniqueGuard[];
|
|
3987
|
+
/**
|
|
3988
|
+
* The outbox events derived from the mutation's `emits` effects (issue #87):
|
|
3989
|
+
* each a transactional-outbox `Put`. When present, a **single-key** call
|
|
3990
|
+
* composes each event's `Put` into the same atomic `TransactWriteItems` as the
|
|
3991
|
+
* entity write, so the event RECORD is written ATOMICALLY with the state it
|
|
3992
|
+
* announces (they can never diverge). The row is drainable from real
|
|
3993
|
+
* DynamoDB Streams (captured at the table layer regardless of code path);
|
|
3994
|
+
* delivery / handlers are OUT of scope. Since #94 the derived-command
|
|
3995
|
+
* transaction also feeds every committed item (including this outbox `Put`)
|
|
3996
|
+
* into the in-process `ChangeCaptureRegistry` seam, so the dev/test
|
|
3997
|
+
* `src/cdc/` emulator now observes this path too — production Streams and the
|
|
3998
|
+
* emulator agree. Absent on a mutation with no `emits`.
|
|
3999
|
+
*/
|
|
4000
|
+
readonly outboxEvents?: readonly DerivedOutboxEvent[];
|
|
4001
|
+
/**
|
|
4002
|
+
* The client-token idempotency guard derived from the mutation's `idempotency`
|
|
4003
|
+
* effect (issue #87): a marker-row `Put` (`attribute_not_exists`). When present,
|
|
4004
|
+
* a **single-key** call composes the guard `Put` into the same atomic
|
|
4005
|
+
* `TransactWriteItems` as the entity write, so a same-token re-execution fails
|
|
4006
|
+
* the guard and rolls the WHOLE transaction back — no effect is double-applied.
|
|
4007
|
+
* Absent on a mutation with no `idempotency` / a #64 hand-written method.
|
|
4008
|
+
*/
|
|
4009
|
+
readonly idempotencyGuard?: DerivedIdempotencyGuard;
|
|
4010
|
+
/**
|
|
4011
|
+
* The return projection of a `mutation`-derived method (issue #83). When
|
|
4012
|
+
* present, a **single-key** call performs the write then issues a
|
|
4013
|
+
* **consistent read-back** (`GetItem` with `ConsistentRead`) of the
|
|
4014
|
+
* written entity's primary key and returns the projected item via the
|
|
4015
|
+
* existing read-projection machinery. Absent on a #64 hand-written method.
|
|
4016
|
+
*/
|
|
4017
|
+
readonly returnSelection?: Readonly<Record<string, boolean>>;
|
|
4018
|
+
}>>;
|
|
4019
|
+
}
|
|
4020
|
+
/** The params a command method consumes (the body's `params` argument). */
|
|
4021
|
+
type ContractCommandParams = Record<string, unknown>;
|
|
4022
|
+
/**
|
|
4023
|
+
* The projected read-back item a `mutation`-derived command method returns, or
|
|
4024
|
+
* `null` when the written row was absent at read-back (e.g. a `remove`). A plain
|
|
4025
|
+
* record of the projected fields.
|
|
4026
|
+
*/
|
|
4027
|
+
type CommandReturn = Record<string, unknown> | null;
|
|
4028
|
+
/**
|
|
4029
|
+
* #101 Phase 3 — the per-op outcome of a `mode: 'parallel'` key-array bulk write.
|
|
4030
|
+
* A non-atomic fan-out reports each key's result independently: `{ ok: true }` on
|
|
4031
|
+
* success, `{ ok: false, error }` on a per-op failure (a failed condition / a
|
|
4032
|
+
* `BatchWriteItem` item that stayed unprocessed after retry). The array index
|
|
4033
|
+
* matches the input key index, and a per-op failure NEVER aborts the others.
|
|
4034
|
+
*/
|
|
4035
|
+
interface ParallelOpResult$1 {
|
|
4036
|
+
readonly ok: boolean;
|
|
4037
|
+
readonly error?: string;
|
|
4038
|
+
}
|
|
4039
|
+
/** The partial-success return of a `mode: 'parallel'` key-array bulk write (#101). */
|
|
4040
|
+
interface CommandParallelReturn {
|
|
4041
|
+
readonly results: readonly ParallelOpResult$1[];
|
|
4042
|
+
}
|
|
4043
|
+
/**
|
|
4044
|
+
* Execute a command-contract method for a **single** key — one write op
|
|
4045
|
+
* (`put` / `update` / `delete`).
|
|
4046
|
+
*
|
|
4047
|
+
* @param contract The resolved command contract (a {@link CommandModelContract}).
|
|
4048
|
+
* @param methodName The method to execute.
|
|
4049
|
+
* @param key A single contract key (the mutation target).
|
|
4050
|
+
* @param params The mutation params (the body's `params` argument).
|
|
4051
|
+
*/
|
|
4052
|
+
declare function executeCommandMethod(contract: ExecutableCommandContract, methodName: string, key: ContractKeyInput, params?: ContractCommandParams): Promise<void | CommandReturn>;
|
|
4053
|
+
/**
|
|
4054
|
+
* Execute a command-contract method for an **array** of keys — a batched write
|
|
4055
|
+
* per the method's declared {@link CommandBatchMode}:
|
|
4056
|
+
*
|
|
4057
|
+
* - `'transact'` → one atomic `TransactWriteItems` (≤25, condition-capable); a
|
|
4058
|
+
* >25-key array is **rejected** (an atomic transaction cannot be split).
|
|
4059
|
+
* - `'batchWrite'` → a `BatchWriteItem` (no conditions; chunked ≤25 with
|
|
4060
|
+
* `UnprocessedItems` retry).
|
|
4061
|
+
*
|
|
4062
|
+
* A method that declared no batched form rejects an array with a clear error.
|
|
4063
|
+
*
|
|
4064
|
+
* @param contract The resolved command contract.
|
|
4065
|
+
* @param methodName The method to execute.
|
|
4066
|
+
* @param keys An array of contract keys.
|
|
4067
|
+
* @param params The mutation params shared across every key.
|
|
4068
|
+
*/
|
|
4069
|
+
declare function executeCommandMethod(contract: ExecutableCommandContract, methodName: string, keys: readonly ContractKeyInput[], params?: ContractCommandParams): Promise<void | CommandParallelReturn>;
|
|
4070
|
+
|
|
4071
|
+
/**
|
|
4072
|
+
* The **unified envelope** in-process execution engine (issue #101).
|
|
4073
|
+
*
|
|
4074
|
+
* `DDBModel.query(map)` and `DDBModel.mutate(map, { mode })` are the in-process,
|
|
4075
|
+
* multi-route counterparts of the single primitives `Model.query` /
|
|
4076
|
+
* `Model.putItem|updateItem|deleteItem`, sharing one descriptor shape (alias → descriptor)
|
|
4077
|
+
* with the declarative `mutation(...)` DSL (#108) and the public contract layer.
|
|
4078
|
+
*
|
|
4079
|
+
* - **READ** — `{ alias: { query | list: Model, key, select, options? } }`. Each
|
|
4080
|
+
* route runs **independently and in parallel** (`Promise.all`): a `query` route
|
|
4081
|
+
* delegates to {@link executeQuery}, a `list` route to {@link executeList}. The
|
|
4082
|
+
* nested selection / relation resolution / optimisation under a route is the
|
|
4083
|
+
* existing read path, reused verbatim. Cross-route consistency is NOT guaranteed
|
|
4084
|
+
* (each route is its own read).
|
|
4085
|
+
*
|
|
4086
|
+
* - **WRITE** — `{ alias: { create | update | remove: Model, key, input?,
|
|
4087
|
+
* condition?, result? } }` plus a call-level `{ mode }`:
|
|
4088
|
+
* - `mode: 'transaction'` (default) — every operation composes into ONE atomic
|
|
4089
|
+
* `TransactWriteItems` (all-or-nothing); a `condition` failure (or any other)
|
|
4090
|
+
* rolls the whole set back and the call throws. Over the 25-item cap is an
|
|
4091
|
+
* error (an atomic transaction is never split).
|
|
4092
|
+
* - `mode: 'parallel'` — non-atomic. Operations are ordered by their
|
|
4093
|
+
* cross-fragment data dependencies (`$.alias.field`) into dependency levels;
|
|
4094
|
+
* within a level they run in parallel. Each operation (by alias) reports
|
|
4095
|
+
* success / failure **individually** in the returned result object (the call
|
|
4096
|
+
* does NOT throw on a per-op failure).
|
|
4097
|
+
*
|
|
4098
|
+
* Both write modes funnel through the **same execution core** the public
|
|
4099
|
+
* contract runtime (`executeCommandMethod`) uses — a descriptor is converted into
|
|
4100
|
+
* an anonymous `mutation()` plan, compiled by `compileMutationPlan`, and committed
|
|
4101
|
+
* via the shared command-runtime primitives — so atomicity / conformance is
|
|
4102
|
+
* identical to the public boundary by construction (no second code path).
|
|
4103
|
+
*/
|
|
4104
|
+
|
|
4105
|
+
/** A read route's options (`query`: consistency/depth; `list`: paging/filter). */
|
|
4106
|
+
interface ReadRouteOptions {
|
|
4107
|
+
readonly consistentRead?: boolean;
|
|
4108
|
+
readonly maxDepth?: number;
|
|
4109
|
+
readonly limit?: number;
|
|
4110
|
+
readonly after?: string;
|
|
4111
|
+
readonly order?: 'ASC' | 'DESC';
|
|
4112
|
+
readonly filter?: Record<string, unknown>;
|
|
4113
|
+
}
|
|
4114
|
+
/** One read route: a `query` (point/unique) or a `list` (partition) descriptor. */
|
|
4115
|
+
interface ReadRouteDescriptor {
|
|
4116
|
+
readonly query?: ModelRef;
|
|
4117
|
+
readonly list?: ModelRef;
|
|
4118
|
+
readonly key: Record<string, unknown>;
|
|
4119
|
+
readonly select: Record<string, unknown>;
|
|
4120
|
+
readonly options?: ReadRouteOptions;
|
|
4121
|
+
}
|
|
4122
|
+
/** The read envelope: alias → {@link ReadRouteDescriptor}. */
|
|
4123
|
+
type ReadEnvelope = Record<string, ReadRouteDescriptor>;
|
|
4124
|
+
/** The result of a read route — a `query` item (or `null`) or a `list` connection. */
|
|
4125
|
+
type ReadRouteResult = Record<string, unknown> | null | {
|
|
4126
|
+
items: Record<string, unknown>[];
|
|
4127
|
+
cursor: string | null;
|
|
4128
|
+
};
|
|
4129
|
+
/** The write execution mode: atomic transaction (default) or non-atomic parallel. */
|
|
4130
|
+
type MutateMode = 'transaction' | 'parallel';
|
|
4131
|
+
/** A read-back projection for a write op (the EXISTING query select shape). */
|
|
4132
|
+
interface WriteResultProjection {
|
|
4133
|
+
readonly select?: Record<string, unknown>;
|
|
4134
|
+
readonly options?: {
|
|
4135
|
+
readonly consistentRead?: boolean;
|
|
4136
|
+
readonly maxDepth?: number;
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
/** One write descriptor (alias entry) for the in-process `DDBModel.mutate`. */
|
|
4140
|
+
interface InProcessWriteDescriptor {
|
|
4141
|
+
readonly create?: ModelRef;
|
|
4142
|
+
readonly update?: ModelRef;
|
|
4143
|
+
readonly remove?: ModelRef;
|
|
4144
|
+
/**
|
|
4145
|
+
* The target's primary-key binding. A single key object applies the op once; an
|
|
4146
|
+
* **array** of key objects (#101 `key: K | K[]`) applies the SAME op to every key
|
|
4147
|
+
* (bulk). Under `transaction` mode a bulk descriptor composes one atomic
|
|
4148
|
+
* `TransactWriteItems` over all keys; under `parallel` mode it runs each key
|
|
4149
|
+
* non-atomically with per-key partial success.
|
|
4150
|
+
*/
|
|
4151
|
+
readonly key: Record<string, unknown> | readonly Record<string, unknown>[];
|
|
4152
|
+
readonly input?: Record<string, unknown>;
|
|
4153
|
+
readonly condition?: Record<string, unknown>;
|
|
4154
|
+
readonly result?: WriteResultProjection;
|
|
4155
|
+
}
|
|
4156
|
+
/** The write envelope: alias → {@link InProcessWriteDescriptor}. */
|
|
4157
|
+
type WriteEnvelope = Record<string, InProcessWriteDescriptor>;
|
|
4158
|
+
/** Options for {@link executeWriteEnvelope}. */
|
|
4159
|
+
interface MutateOptions {
|
|
4160
|
+
readonly mode?: MutateMode;
|
|
4161
|
+
}
|
|
4162
|
+
/** A per-alias result in `parallel` mode: success carries the read-back, else error. */
|
|
4163
|
+
type ParallelOpResult = {
|
|
4164
|
+
readonly ok: true;
|
|
4165
|
+
readonly value: CommandReturn | undefined;
|
|
4166
|
+
} | {
|
|
4167
|
+
readonly ok: false;
|
|
4168
|
+
readonly error: Error;
|
|
4169
|
+
};
|
|
4170
|
+
|
|
4171
|
+
/**
|
|
4172
|
+
* Type-level inference for the unified envelope (issue #101). Maps a read / write
|
|
4173
|
+
* envelope's descriptor map to its alias-keyed result type, deriving each route's
|
|
4174
|
+
* result from its intent discriminator (`query`/`list` ↔ `create`/`update`/`remove`)
|
|
4175
|
+
* and `select` projection — reusing the `Model.query` `ProjectionOf` machinery.
|
|
4176
|
+
*/
|
|
4177
|
+
|
|
4178
|
+
/** The entity type a {@link ModelStatic} value (or model class) carries. */
|
|
4179
|
+
type EntityOf<M> = M extends ModelStatic<infer T, infer _C> ? T : M extends abstract new (...args: never[]) => infer T ? T extends DDBModel ? T : never : unknown;
|
|
4180
|
+
/** The result type of one read route, derived from `query` / `list` + `select`. */
|
|
4181
|
+
type ReadRouteResultOf<R> = R extends {
|
|
4182
|
+
readonly query: infer M;
|
|
4183
|
+
readonly select: infer S;
|
|
4184
|
+
} ? QueryResultFor<EntityOf<M>, S> | null : R extends {
|
|
4185
|
+
readonly list: infer M;
|
|
4186
|
+
readonly select: infer S;
|
|
4187
|
+
} ? {
|
|
4188
|
+
items: QueryResultFor<EntityOf<M>, S>[];
|
|
4189
|
+
cursor: string | null;
|
|
4190
|
+
} : unknown;
|
|
4191
|
+
/** The projected item type for an entity `T` and a select `S` (relation-nest aware). */
|
|
4192
|
+
type QueryResultFor<T, S> = T extends DDBModel ? S extends Record<string, unknown> ? QueryResult<T, ProjectionOf<T, S>> : Record<string, unknown> : Record<string, unknown>;
|
|
4193
|
+
/** The alias-keyed result of {@link DDBModel.query}. */
|
|
4194
|
+
type QueryEnvelopeResult<E extends ReadEnvelope> = {
|
|
4195
|
+
readonly [K in keyof E]: ReadRouteResultOf<E[K]>;
|
|
4196
|
+
};
|
|
4197
|
+
/** The projected read-back type of one write op (or `undefined` when no `result`). */
|
|
4198
|
+
type WriteReadBackOf<W> = W extends {
|
|
4199
|
+
readonly result: {
|
|
4200
|
+
readonly select: infer S;
|
|
4201
|
+
};
|
|
4202
|
+
} ? W extends {
|
|
4203
|
+
readonly create: infer M;
|
|
4204
|
+
} ? QueryResultFor<EntityOf<M>, S> | null : W extends {
|
|
4205
|
+
readonly update: infer M;
|
|
4206
|
+
} ? QueryResultFor<EntityOf<M>, S> | null : W extends {
|
|
4207
|
+
readonly remove: infer M;
|
|
4208
|
+
} ? QueryResultFor<EntityOf<M>, S> | null : CommandReturn : undefined;
|
|
4209
|
+
/**
|
|
4210
|
+
* #101 `key: K | K[]` — is the descriptor a BULK write (its `key` is an array)?
|
|
4211
|
+
* A bulk alias's result is an ARRAY (aligned to the keys); a single key's is scalar.
|
|
4212
|
+
*/
|
|
4213
|
+
type IsBulk<W> = W extends {
|
|
4214
|
+
readonly key: readonly unknown[];
|
|
4215
|
+
} ? true : false;
|
|
4216
|
+
/** Wrap a per-alias result in an array when the descriptor declared a `key` array. */
|
|
4217
|
+
type BulkWrap<W, R> = IsBulk<W> extends true ? readonly R[] : R;
|
|
4218
|
+
/** The alias-keyed result of {@link DDBModel.mutate} in `transaction` mode. */
|
|
4219
|
+
type MutateTransactionResult<E extends WriteEnvelope> = {
|
|
4220
|
+
readonly [K in keyof E as E[K] extends {
|
|
4221
|
+
readonly result: unknown;
|
|
4222
|
+
} ? K : never]: BulkWrap<E[K], WriteReadBackOf<E[K]>>;
|
|
4223
|
+
};
|
|
4224
|
+
/** A per-alias `parallel`-mode result carrying the typed read-back on success. */
|
|
4225
|
+
type ParallelResultOf<W> = {
|
|
4226
|
+
readonly ok: true;
|
|
4227
|
+
readonly value: WriteReadBackOf<W>;
|
|
4228
|
+
} | {
|
|
4229
|
+
readonly ok: false;
|
|
4230
|
+
readonly error: Error;
|
|
4231
|
+
};
|
|
4232
|
+
/** The alias-keyed result of {@link DDBModel.mutate} in `parallel` mode. */
|
|
4233
|
+
type MutateParallelResult<E extends WriteEnvelope> = {
|
|
4234
|
+
readonly [K in keyof E]: BulkWrap<E[K], ParallelResultOf<E[K]>>;
|
|
4235
|
+
};
|
|
4236
|
+
|
|
4237
|
+
declare abstract class DDBModel {
|
|
4238
|
+
/** @internal Type brand — prevents primitives from structurally matching DDBModel. */
|
|
4239
|
+
private readonly __brand;
|
|
4240
|
+
static setClient(client: DynamoDBClient): void;
|
|
4241
|
+
static setTableMapping(mapping: Record<string, string>): void;
|
|
4242
|
+
static transaction(fn: (tx: TransactionContext) => void | Promise<void>): Promise<void>;
|
|
4243
|
+
/**
|
|
4244
|
+
* In-process multi-route READ (issue #101 — the unified envelope). Each alias
|
|
4245
|
+
* route (`{ query | list: Model, key, select, options? }`) runs **independently
|
|
4246
|
+
* and in parallel**; the result is an object keyed by the input aliases. A
|
|
4247
|
+
* `query` route delegates to the single-item read path, a `list` route to the
|
|
4248
|
+
* partition read path. Cross-route consistency is NOT guaranteed.
|
|
4249
|
+
*/
|
|
4250
|
+
static query<const E extends ReadEnvelope>(envelope: E): Promise<QueryEnvelopeResult<E>>;
|
|
4251
|
+
/**
|
|
4252
|
+
* In-process multi-operation WRITE (issue #101 — the unified envelope).
|
|
4253
|
+
*
|
|
4254
|
+
* - `mode: 'transaction'` (default) — every op composes into ONE atomic
|
|
4255
|
+
* `TransactWriteItems`; a `condition` failure rolls the whole set back and the
|
|
4256
|
+
* call throws. Result: `{ alias: readback }` for ops that declared a `result`.
|
|
4257
|
+
* - `mode: 'parallel'` — non-atomic; ops report success / failure individually
|
|
4258
|
+
* (the call does not throw on a per-op failure). Result:
|
|
4259
|
+
* `{ alias: { ok:true, value } | { ok:false, error } }`.
|
|
4260
|
+
*
|
|
4261
|
+
* Both modes funnel through the SAME write-execution core the public contract
|
|
4262
|
+
* runtime uses, so atomicity / derived-effect semantics are identical.
|
|
4263
|
+
*/
|
|
4264
|
+
static mutate<const E extends WriteEnvelope>(envelope: E, options?: {
|
|
4265
|
+
mode?: 'transaction';
|
|
4266
|
+
}): Promise<MutateTransactionResult<E>>;
|
|
4267
|
+
static mutate<const E extends WriteEnvelope>(envelope: E, options: {
|
|
4268
|
+
mode: 'parallel';
|
|
4269
|
+
}): Promise<MutateParallelResult<E>>;
|
|
4270
|
+
static batchGet(requests: BatchGetRequest[]): Promise<BatchGetResult>;
|
|
4271
|
+
static batchWrite(requests: BatchWriteRequest[]): Promise<void>;
|
|
4272
|
+
static asModel<C extends new (...args: never[]) => DDBModel>(this: C): ModelStatic<InstanceType<C>, C>;
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
/**
|
|
4276
|
+
* Internal brand identifying a {@link Column} reference at runtime, used by the
|
|
4277
|
+
* `cond` raw escape hatch to recognize and alias embedded column references.
|
|
4278
|
+
*/
|
|
4279
|
+
declare const COLUMN_REF: unique symbol;
|
|
4280
|
+
/**
|
|
4281
|
+
* A type-safe reference to a model field (column).
|
|
4282
|
+
*
|
|
4283
|
+
* Each column carries its declared value type as a phantom parameter so the
|
|
4284
|
+
* `cond` escape hatch can type-check embedded values against the column. The
|
|
4285
|
+
* `__model` phantom brands the column to its owning model, so passing a column
|
|
4286
|
+
* from a different model into another model's `cond` is a type error.
|
|
4287
|
+
*
|
|
4288
|
+
* @typeParam V - The declared field value type.
|
|
4289
|
+
* @typeParam M - The owning model (brand only; never read at runtime).
|
|
4290
|
+
*/
|
|
4291
|
+
interface Column<V = unknown, M = unknown> {
|
|
4292
|
+
readonly [COLUMN_REF]: true;
|
|
4293
|
+
/** The underlying attribute (property) name. */
|
|
4294
|
+
readonly name: string;
|
|
4295
|
+
/** @internal phantom — value type carried for `cond` value checking. */
|
|
4296
|
+
readonly __value?: V;
|
|
4297
|
+
/** @internal phantom — owning-model brand. */
|
|
4298
|
+
readonly __model?: M;
|
|
4299
|
+
}
|
|
4300
|
+
/** Runtime guard: is this value a {@link Column} reference? */
|
|
4301
|
+
declare function isColumn(value: unknown): value is Column;
|
|
4302
|
+
/**
|
|
4303
|
+
* The `Model.col` map: one {@link Column} per scalar field of the entity, each
|
|
4304
|
+
* typed with the field's declared value type and branded to the model.
|
|
4305
|
+
*
|
|
4306
|
+
* @typeParam T - The Entity type.
|
|
4307
|
+
*/
|
|
4308
|
+
type ColumnMap<T> = {
|
|
4309
|
+
[K in keyof T as T[K] extends Function ? never : NonNullable<T[K]> extends DDBModel ? never : T[K] extends Array<infer E> ? [E] extends [DDBModel] ? never : K : K]: Column<T[K], T>;
|
|
4310
|
+
};
|
|
4311
|
+
|
|
4312
|
+
declare const KEY_MARKER: unique symbol;
|
|
4313
|
+
/**
|
|
4314
|
+
* Brand identifying a {@link KeySegment} produced by the {@link k} tag.
|
|
4315
|
+
*/
|
|
4316
|
+
declare const KEY_SEGMENT: unique symbol;
|
|
4317
|
+
/**
|
|
4318
|
+
* A single structured key segment — the output of one `` k`...` `` tagged
|
|
4319
|
+
* template. Its literal parts and the column references interpolated between
|
|
4320
|
+
* them are preserved verbatim (the column refs are NOT stringified), so the
|
|
4321
|
+
* planner / spec generator can derive `{field}` templates directly from the
|
|
4322
|
+
* structure rather than recovering them by symbolic evaluation.
|
|
4323
|
+
*
|
|
4324
|
+
* Exactly one `` k`...` `` = one segment = one truncation boundary for a
|
|
4325
|
+
* multi-segment sort key.
|
|
4326
|
+
*/
|
|
4327
|
+
interface KeySegment {
|
|
4328
|
+
readonly [KEY_SEGMENT]: true;
|
|
4329
|
+
/** The literal string parts; always one longer than {@link columns}. */
|
|
4330
|
+
readonly literals: readonly string[];
|
|
4331
|
+
/** The interpolated column references, in order. */
|
|
4332
|
+
readonly columns: readonly Column[];
|
|
4333
|
+
}
|
|
4334
|
+
/** Runtime guard: is this value a {@link KeySegment}? */
|
|
4335
|
+
declare function isKeySegment(value: unknown): value is KeySegment;
|
|
4336
|
+
/**
|
|
4337
|
+
* The interpolation slot accepted by {@link k}: a typed column reference
|
|
4338
|
+
* (`c.field`). Bare strings are NOT accepted — a key field must be named via a
|
|
4339
|
+
* {@link Column} reference so the structure stays type-safe and refactor-safe.
|
|
4340
|
+
*/
|
|
4341
|
+
type KeySlot = Column;
|
|
4342
|
+
/**
|
|
4343
|
+
* Tagged-template builder for a single key segment.
|
|
4344
|
+
*
|
|
4345
|
+
* The literal parts and the interpolated column references are kept structurally
|
|
4346
|
+
* (never stringified), mirroring the {@link cond} escape hatch. A segment such
|
|
4347
|
+
* as `` k`USER#${c.userId}` `` becomes `{ literals: ['USER#', ''], columns: [userId] }`,
|
|
4348
|
+
* which the planner turns into the template `USER#{userId}`.
|
|
4349
|
+
*
|
|
4350
|
+
* @example
|
|
4351
|
+
* ```ts
|
|
4352
|
+
* key((c) => ({ pk: k`USER#${c.userId}`, sk: k`PROFILE` }));
|
|
4353
|
+
* key((c) => ({
|
|
4354
|
+
* pk: k`REGION#${c.region}`,
|
|
4355
|
+
* sk: [k`CITY#${c.city}`, k`STORE#${c.store}`],
|
|
4356
|
+
* }));
|
|
4357
|
+
* ```
|
|
4358
|
+
*/
|
|
4359
|
+
declare function k(literals: TemplateStringsArray, ...columns: KeySlot[]): KeySegment;
|
|
4360
|
+
/** A single segment, or an ordered list of segments (multi-segment SK). */
|
|
4361
|
+
type SegmentSpec = KeySegment | KeySegment[];
|
|
4362
|
+
/** The `{ pk, sk }` structure returned by a key builder. */
|
|
4363
|
+
interface KeyStructure {
|
|
4364
|
+
readonly pk: SegmentSpec;
|
|
4365
|
+
readonly sk?: SegmentSpec;
|
|
4366
|
+
}
|
|
4367
|
+
/**
|
|
4368
|
+
* The canonical, structured representation of a key (PK or SK as a list of
|
|
4369
|
+
* segments). This — not an opaque mapping closure — is the single source of
|
|
4370
|
+
* truth for a model's key.
|
|
4371
|
+
*/
|
|
4372
|
+
interface SegmentedKey {
|
|
4373
|
+
/** Partition-key segments (all required at resolve time). */
|
|
4374
|
+
readonly pkSegments: readonly KeySegment[];
|
|
4375
|
+
/** Sort-key segments, evaluated in order with truncation. */
|
|
4376
|
+
readonly skSegments: readonly KeySegment[];
|
|
4377
|
+
}
|
|
4378
|
+
interface KeyDefinitionMarker<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
4379
|
+
readonly [KEY_MARKER]: true;
|
|
4380
|
+
readonly segmented: SegmentedKey;
|
|
4381
|
+
readonly inputFieldNames: string[];
|
|
4382
|
+
/** @internal Phantom field for type-level input type extraction. */
|
|
4383
|
+
readonly _phantom?: T;
|
|
4384
|
+
}
|
|
4385
|
+
/**
|
|
4386
|
+
* Define a model's primary key from a **structured segment** builder.
|
|
4387
|
+
*
|
|
4388
|
+
* The builder receives a typed column accessor `c` (one `Column` per field) and
|
|
4389
|
+
* returns `{ pk, sk }`, where each of `pk` / `sk` is a single `` k`...` ``
|
|
4390
|
+
* segment or an array of them. Column references are captured structurally, so
|
|
4391
|
+
* the key template is recovered directly from the structure — no symbolic
|
|
4392
|
+
* evaluation of an opaque closure.
|
|
4393
|
+
*
|
|
4394
|
+
* @typeParam T - The key input type (drives compile-time key checking for
|
|
4395
|
+
* `query` / `list`). Supplied by the caller's parameter annotation on `c`.
|
|
4396
|
+
*/
|
|
4397
|
+
declare function key<T extends Record<string, unknown>>(builder: (c: {
|
|
4398
|
+
readonly [K in keyof T]-?: Column<T[K], T>;
|
|
4399
|
+
}) => KeyStructure): KeyDefinitionMarker<T>;
|
|
4400
|
+
|
|
4401
|
+
/**
|
|
4402
|
+
* CDC emulator types (issue #72). These types are **cdc-module-owned**; core
|
|
4403
|
+
* never references them. The {@link ChangeEvent} shape mirrors DynamoDB Streams
|
|
4404
|
+
* (`NEW_AND_OLD_IMAGES`) so a consumer written against the emulator runs
|
|
4405
|
+
* unchanged against real Streams in production (spec §4).
|
|
4406
|
+
*/
|
|
4407
|
+
/** Stream view type. Fixed to `NEW_AND_OLD_IMAGES` — incremental aggregation
|
|
4408
|
+
* needs both images (spec §4). */
|
|
4409
|
+
type StreamViewType = 'NEW_AND_OLD_IMAGES';
|
|
4410
|
+
/** Stream event kind, matching DynamoDB Streams. */
|
|
4411
|
+
type ChangeEventName = 'INSERT' | 'MODIFY' | 'REMOVE';
|
|
4412
|
+
/**
|
|
4413
|
+
* A single change event. Within one shard, `sequenceNumber` is monotonically
|
|
4414
|
+
* increasing; across shards order is independent (spec §6).
|
|
4415
|
+
*/
|
|
4416
|
+
interface ChangeEvent<T = Record<string, unknown>> {
|
|
4417
|
+
eventName: ChangeEventName;
|
|
4418
|
+
table: string;
|
|
4419
|
+
/** Resolved model name when known. */
|
|
4420
|
+
model?: string;
|
|
4421
|
+
keys: {
|
|
4422
|
+
pk: string;
|
|
4423
|
+
sk: string;
|
|
4424
|
+
};
|
|
4425
|
+
/** Present for MODIFY / REMOVE. */
|
|
4426
|
+
oldImage?: T;
|
|
4427
|
+
/** Present for INSERT / MODIFY. */
|
|
4428
|
+
newImage?: T;
|
|
4429
|
+
/** Deterministic-clock-derived creation time (ISO 8601). */
|
|
4430
|
+
approximateCreationTime: string;
|
|
4431
|
+
/** Monotonic within a shard (zero-padded for lexicographic ordering). */
|
|
4432
|
+
sequenceNumber: string;
|
|
4433
|
+
/** Partition = hash(pk). */
|
|
4434
|
+
shardId: string;
|
|
4435
|
+
}
|
|
4436
|
+
/** A delivered batch. Records of the same shard are in `sequenceNumber` order. */
|
|
4437
|
+
interface ChangeBatch<T = Record<string, unknown>> {
|
|
4438
|
+
records: ChangeEvent<T>[];
|
|
4439
|
+
}
|
|
4440
|
+
/**
|
|
4441
|
+
* The consumer's response to a batch (spec §6, `ReportBatchItemFailures`
|
|
4442
|
+
* equivalent). Returning the `sequenceNumber`s of records that failed lets the
|
|
4443
|
+
* emulator advance the checkpoint past the successful prefix and redeliver only
|
|
4444
|
+
* the failures.
|
|
4445
|
+
*/
|
|
4446
|
+
interface BatchResult {
|
|
4447
|
+
/** `sequenceNumber`s of records the consumer failed to process. */
|
|
4448
|
+
batchItemFailures?: string[];
|
|
4449
|
+
}
|
|
4450
|
+
/** The consumer handler. */
|
|
4451
|
+
type ChangeHandler = (batch: ChangeBatch) => Promise<BatchResult | void>;
|
|
4452
|
+
/** Unsubscribe handle. */
|
|
4453
|
+
type Unsubscribe = () => void;
|
|
4454
|
+
/** Where a fresh subscriber begins reading. */
|
|
4455
|
+
type StartingPosition = 'TRIM_HORIZON' | 'LATEST';
|
|
4456
|
+
/** Clock mode. `virtual` makes time deterministic via `advanceClock`. */
|
|
4457
|
+
type ClockMode = 'real' | 'virtual';
|
|
4458
|
+
/** Delivery mode (spec §7). */
|
|
4459
|
+
type CdcMode = 'inline' | 'queued' | 'record' | 'replay';
|
|
4460
|
+
/** Deterministic fault-injection spec (spec §8). All probabilities are seeded. */
|
|
4461
|
+
interface FaultSpec {
|
|
4462
|
+
/** Probability [0,1] a delivered record is also re-delivered (duplicate). */
|
|
4463
|
+
duplicate?: number;
|
|
4464
|
+
/** Shuffle within-shard order on delivery (invariant-violation testing). */
|
|
4465
|
+
reorder?: boolean;
|
|
4466
|
+
/** Per-record virtual-time delivery delay range (ms). Requires queued mode. */
|
|
4467
|
+
delay?: {
|
|
4468
|
+
min: number;
|
|
4469
|
+
max: number;
|
|
4470
|
+
};
|
|
4471
|
+
/** Probability [0,1] a record is dropped on first delivery then redelivered. */
|
|
4472
|
+
dropThenRedeliver?: number;
|
|
4473
|
+
/** Probability [0,1] a record is reported as a partial-batch failure. */
|
|
4474
|
+
partialBatchFailure?: number;
|
|
4475
|
+
}
|
|
4476
|
+
/** A reference whose recompute should be forced to race a mark (spec §8, §7). */
|
|
4477
|
+
interface ConcurrentRecomputeRef {
|
|
4478
|
+
/** Opaque node ref the harness recomputes concurrently with marking. */
|
|
4479
|
+
ref: string;
|
|
4480
|
+
}
|
|
4481
|
+
/** A persisted event log (spec §10, record/replay). */
|
|
4482
|
+
interface EventLog {
|
|
4483
|
+
seed: number;
|
|
4484
|
+
events: ChangeEvent[];
|
|
4485
|
+
}
|
|
4486
|
+
/** Replay options (spec §10). */
|
|
4487
|
+
interface ReplayOptions {
|
|
4488
|
+
/** Deliver events in a deterministic shuffled order (any-order replay). */
|
|
4489
|
+
shuffle?: boolean;
|
|
4490
|
+
/** Probability [0,1] each event is delivered twice during replay. */
|
|
4491
|
+
duplicate?: number;
|
|
4492
|
+
}
|
|
4493
|
+
/** Constructor options (spec §11). */
|
|
4494
|
+
interface CdcEmulatorOptions {
|
|
4495
|
+
/** Stream-enabled model classes (opt-in; default none). */
|
|
4496
|
+
models?: Function[];
|
|
4497
|
+
/** Delivery mode. Default `inline`. */
|
|
4498
|
+
mode?: CdcMode;
|
|
4499
|
+
/** Clock mode. Default `virtual` for queued, `real` otherwise. */
|
|
4500
|
+
clock?: ClockMode;
|
|
4501
|
+
/** Records per delivered batch. Default 100. */
|
|
4502
|
+
batchSize?: number;
|
|
4503
|
+
/** Max redelivery attempts before a record goes to the DLQ. Default 3. */
|
|
4504
|
+
maxRetries?: number;
|
|
4505
|
+
/** Where a subscriber starts. Default `TRIM_HORIZON`. */
|
|
4506
|
+
startingPosition?: StartingPosition;
|
|
4507
|
+
/** Seed for fault injection / shard assignment determinism. Default 1. */
|
|
4508
|
+
seed?: number;
|
|
4509
|
+
/** Number of shards events are partitioned across. Default 8. */
|
|
4510
|
+
shardCount?: number;
|
|
4511
|
+
}
|
|
4512
|
+
type ShardId = string;
|
|
4513
|
+
|
|
4514
|
+
export { type BridgeBundle as $, type EntityInput as A, type BatchGetExecInput as B, type ChangeEvent as C, type DynamoDBOperation as D, type Executor as E, type FaultSpec as F, type UniqueQueryKeyOf as G, type EntityRef as H, type ConditionInput as I, type QueryMethodSpec as J, type CommandModelContract as K, type CommandMethodSpec as L, type ModelStatic as M, type ContractSpec as N, type OperationDefinition as O, type PutInput as P, type QueryModelContract as Q, type ResolvedKey as R, type SelectableOf as S, type TransactWriteExecItem as T, type UpdateInput as U, type QuerySpec as V, type WriteExecOptions as W, type CommandSpec as X, type ContextSpec as Y, type OperationsDocument as Z, type AnyOperationDefinition as _, type ExecutorResult as a, type InputArity as a$, type ConditionSpec as a0, type BatchDeleteRequest as a1, type BatchGetRequest as a2, BatchGetResult as a3, type BatchPutRequest as a4, type BatchResult as a5, type BatchWriteRequest as a6, CONTRACT_RANGE_FANOUT_CONCURRENCY as a7, type CdcMode as a8, type ChangeBatch as a9, type ContractKeyRef as aA, type ContractKeySpec as aB, type ContractKind as aC, type ContractMethodOp as aD, type ContractParamRef as aE, type ContractQueryParams as aF, type ContractResolution as aG, type DeleteOptions as aH, type DeriveEffect as aI, type DerivedEdgeWrite as aJ, type DerivedUpdate as aK, type DescriptorBinding as aL, ENTITY_WRITES_MARKER as aM, type EdgeEffect as aN, type EffectPath as aO, type EmitEffect as aP, type EntityWritesDefinition as aQ, type EntityWritesShape as aR, type ExecutableCommandContract as aS, type ExecutableQueryContract as aT, type FilterInput as aU, type FilterSpec as aV, type FragmentInput as aW, type GsiDefinitionMarker as aX, type GsiOptions as aY, type IdempotencyEffect as aZ, type InProcessWriteDescriptor as a_, type ChangeEventName as aa, type ClockMode as ab, type Column as ac, type ColumnMap as ad, type CommandContractMethodSpec as ae, type CommandInputShape as af, type CommandMethod as ag, type CommandPlan as ah, type CommandResolutionTarget as ai, type CommandResultKind as aj, type CommandSelectShape as ak, type CompiledFragment as al, type ComposeSpec as am, type CondSlot as an, type ConditionCheckInput as ao, type Connection as ap, type ContractCallSignature as aq, type ContractCardinality as ar, type ContractCommandParams as as, type ContractCommandResult as at, type ContractComposeNode as au, type ContractFromRef as av, type ContractInputArity as aw, type ContractItem as ax, type ContractKeyFieldRef as ay, type ContractKeyInput as az, type WriteResult as b, type UniqueEffect as b$, type KeyDefinitionMarker as b0, type KeySegment as b1, type KeySlot as b2, type KeyStructure as b3, type KeyedResult as b4, LIFECYCLE_CONTRACT_MARKER as b5, type LifecycleContract as b6, type LifecycleEffects as b7, type LiteralParam as b8, type ManifestEntity as b9, type QueryContractMethodSpec as bA, type QueryEnvelopeResult as bB, type QueryKeyOf as bC, type QueryMethod as bD, type QueryResult as bE, type RangeConditionSpec as bF, type RawCondition as bG, type ReadEnvelope as bH, type ReadOperationType as bI, type ReadRouteDescriptor as bJ, type ReadRouteOptions as bK, type ReadRouteResult as bL, type RecordedCompose as bM, type RelationBuilder as bN, type RelationSelect as bO, type RelationSpec as bP, type RequiresEffect as bQ, type Resolution as bR, SPEC_VERSION as bS, type SegmentSpec as bT, type SelectBuilder as bU, type SelectOf as bV, type StartingPosition as bW, type StreamViewType as bX, type StringParam as bY, TransactionContext as bZ, type TransactionItemType as b_, type ManifestField as ba, type ManifestFieldType as bb, type ManifestGsi as bc, type ManifestKey as bd, type ManifestRelation as be, type ManifestTable as bf, type ModelRef as bg, type MutateMode as bh, type MutateOptions as bi, type MutateParallelResult as bj, type MutateTransactionResult as bk, type MutationBody as bl, type MutationDescriptorMap as bm, type MutationFragment as bn, type MutationInputProxy as bo, type MutationInputRef as bp, type MutationIntent as bq, type NumberParam as br, type OperationKind as bs, type OperationSpec as bt, type ParallelOpResult as bu, type ParamKind as bv, type ParamSpec as bw, type ParamStructure as bx, type PlannedCommandMethod as by, type PutOptions as bz, type DeleteInput as c, type Updatable as c0, type UpdateOptions as c1, type WhenSpec as c2, type WriteDescriptor as c3, type WriteEnvelope as c4, type WriteLifecyclePhase as c5, type WriteOperationType as c6, type WriteRecorder as c7, type WriteResultProjection as c8, attachModelClass as c9, isContractComposeNode as cA, isContractFromRef as cB, isContractKeyFieldRef as cC, isContractKeyRef as cD, isContractParamRef as cE, isEntityWritesDefinition as cF, isKeySegment as cG, isLifecycleContract as cH, isMutationFragment as cI, isMutationInputRef as cJ, isParam as cK, isPlannedCommandMethod as cL, isQueryModelContract as cM, k as cN, key as cO, lifecyclePhaseForIntent as cP, mintContractKeyFieldRef as cQ, mintContractParamRef as cR, mutation as cS, param as cT, publicCommandModel as cU, publicQueryModel as cV, query as cW, resolveLifecycle as cX, wholeKeysSentinel as cY, buildDeleteInput as ca, buildPutInput as cb, buildUpdateInput as cc, compileFragment as cd, compileMutationPlan as ce, compileSingleFragmentPlan as cf, cond as cg, contractOfMethodSpec as ch, definePlan as ci, entityWrites as cj, executeBatchGet as ck, executeBatchWrite as cl, executeCommandMethod as cm, executeDelete as cn, executeKeyedBatchGet as co, executePut as cp, executeQueryMethod as cq, executeRangeFanout as cr, executeTransaction as cs, executeUpdate as ct, from as cu, getEntityWrites as cv, gsi as cw, isColumn as cx, isCommandModelContract as cy, isCommandPlan as cz, type BatchWriteExecItem as d, DDBModel as e, type PrimaryKeyOf as f, type ExecutionPlanSpec as g, type SegmentedKey as h, type SelectBuilderSpec as i, type TransactionSpec as j, type Manifest as k, type ExecutionPlan as l, type CdcEmulatorOptions as m, type ChangeHandler as n, type Unsubscribe as o, type ConcurrentRecomputeRef as p, type EventLog as q, type ReplayOptions as r, type ShardId as s, type TransactionItemSpec as t, type Param as u, type ParamDescriptor as v, type DefinitionMap as w, type WriteDefinitionOptions as x, type PartialQueryKeyOf as y, type StrictSelectSpec as z };
|