graphddb 0.1.1 → 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.
@@ -1,1203 +0,0 @@
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.update(user, { name: 'New' }); // ok — branded
425
- * // await User.update({ ...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()`.
568
- *
569
- * - `select` is either a plain projection or a `project(...)` builder.
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
- * @typeParam Sel - The select spec (plain object or builder), inferred per call.
577
- */
578
- type ListCallOptions<T, Sel extends SelectSpec<T>> = {
579
- select: Sel & StrictSelectSpec<T, Sel>;
580
- limit?: number;
581
- after?: string;
582
- order?: 'ASC' | 'DESC';
583
- filter?: FilterInput<T>;
584
- /**
585
- * When `true`, each returned item is updatable: it carries its resolved
586
- * base-table key as a hidden, non-enumerable property and is branded
587
- * `& Updatable`, so it can be passed straight back to `update()`.
588
- */
589
- updatable?: boolean;
590
- };
591
- /**
592
- * Public, type-inferring API surface produced by {@link DDBModel.asModel}.
593
- *
594
- * @typeParam T - The Entity type of the model.
595
- * @typeParam C - The model class constructor type. Supplied automatically by
596
- * `asModel()`; when omitted, key/option types widen to permissive shapes so
597
- * that `ModelStatic<T>` keeps compiling for callers that only name the entity.
598
- */
599
- interface ModelStatic<T extends DDBModel, C = unknown> {
600
- /**
601
- * Type-safe column references for this model, one per scalar field. Used by
602
- * the `cond` raw escape hatch (`cond\`${Model.col.age} > ${18}\``). Each
603
- * column carries its declared type and is branded to this model, so passing
604
- * another model's column into this model's `cond` is a compile error.
605
- */
606
- readonly col: ColumnMap<T>;
607
- /**
608
- * Begin a model-bound, top-level select builder. The entity type `T` is fixed
609
- * by this model and the projection `S` is inferred from the argument.
610
- * `.filter(...)` is typed `FilterInput<T>` against the full entity.
611
- *
612
- * @example
613
- * ```ts
614
- * Order.list({ userId: 'u' }, {
615
- * select: Order.project({ orderId: true, amount: true })
616
- * .filter({ status: 'confirmed', amount: { gt: 100 } }),
617
- * });
618
- * ```
619
- */
620
- project<const S extends SelectableOf<T>>(select: S): SelectBuilder<T, S>;
621
- /**
622
- * Begin a model-bound relation select builder for use as a relation field's
623
- * value inside a parent `select`. `T` is this (relation target) model's
624
- * entity type; `S` is inferred from the argument. Same model-bound inference
625
- * as {@link ModelStatic.project}, plus per-relation pagination
626
- * (`limit` / `after` / `order`).
627
- *
628
- * @example
629
- * ```ts
630
- * User.query({ userId: 'u' }, {
631
- * name: true,
632
- * orders: Order.relation({ orderId: true, amount: true })
633
- * .filter({ amount: { gt: 100 } }),
634
- * });
635
- * ```
636
- */
637
- relation<const S extends SelectableOf<T>>(select: S): RelationBuilder<T, S>;
638
- /**
639
- * Fetch a single item by a unique key and **hydrate** the plain result onto a
640
- * host-side object (issue #53). The `hydrate` factory receives the fully-built
641
- * plain object — `QueryResult<T, ProjectionOf<T, Sel>>`, typed against exactly
642
- * the `select`ed fields — and its return type `R` becomes the read's return
643
- * type (subsuming a `queryAs(Ctor, …)` style: `hydrate: (raw) => new Ctor(raw)`).
644
- *
645
- * Applied as the **final after-fetch step**, only to a present item: a missing
646
- * item resolves to `null` and the factory is NOT invoked (hence `R | null`). A
647
- * throw from the factory propagates verbatim as the read's rejection.
648
- *
649
- * Host-runtime ONLY: the factory is a host-language closure, never serialized
650
- * into the SSoT / `operations.json`, so it does not impede the bridge (#48).
651
- */
652
- query<Sel extends SelectSpec<T>, R>(key: QueryKey<C>, select: Sel & StrictSelectSpec<T, Sel>, options: {
653
- consistentRead?: boolean;
654
- maxDepth?: number;
655
- updatable?: boolean;
656
- hydrate: (raw: QueryResult<T, ProjectionOf<T, Sel>>) => R;
657
- }): Promise<R | null>;
658
- /**
659
- * Fetch a single item by a unique key, requesting an **updatable** result.
660
- * The returned item (when non-null) carries its resolved base-table key as a
661
- * hidden, non-enumerable property and is branded `& Updatable`, so it can be
662
- * passed straight back to {@link ModelStatic.update} — even for a partial
663
- * `select` or a GSI-based query. Do not spread / clone / JSON round-trip the
664
- * result before updating: that drops both the hidden key and the brand.
665
- */
666
- query<Sel extends SelectSpec<T>>(key: QueryKey<C>, select: Sel & StrictSelectSpec<T, Sel>, options: {
667
- consistentRead?: boolean;
668
- maxDepth?: number;
669
- updatable: true;
670
- }): Promise<(QueryResult<T, ProjectionOf<T, Sel>> & Updatable) | null>;
671
- /**
672
- * Fetch a single item by a unique key. The return type contains only the
673
- * selected fields, derived from `select`.
674
- */
675
- query<Sel extends SelectSpec<T>>(key: QueryKey<C>, select: Sel & StrictSelectSpec<T, Sel>, options?: {
676
- consistentRead?: boolean;
677
- maxDepth?: number;
678
- updatable?: false;
679
- }): Promise<QueryResult<T, ProjectionOf<T, Sel>> | null>;
680
- /**
681
- * List items for a partition. `options.select` may be a plain projection or a
682
- * `project(...)` builder. `options.filter` is the declarative server-side
683
- * FilterExpression typed against the full Entity. Returned items contain only
684
- * the selected fields.
685
- */
686
- list<Sel extends SelectSpec<T>>(key: ListKey<C>, options: ListCallOptions<T, Sel> & {
687
- updatable: true;
688
- }): Promise<{
689
- items: (QueryResult<T, ProjectionOf<T, Sel>> & Updatable)[];
690
- cursor: string | null;
691
- }>;
692
- list<Sel extends SelectSpec<T>>(key: ListKey<C>, options: ListCallOptions<T, Sel>): Promise<{
693
- items: QueryResult<T, ProjectionOf<T, Sel>>[];
694
- cursor: string | null;
695
- }>;
696
- explain<Sel extends SelectSpec<T>>(key: ListKey<C>, options?: {
697
- select?: Sel & StrictSelectSpec<T, Sel>;
698
- limit?: number;
699
- after?: string;
700
- order?: 'ASC' | 'DESC';
701
- consistentRead?: boolean;
702
- filter?: FilterInput<T>;
703
- }): ExecutionPlan;
704
- put(item: EntityInput<T>, options?: Record<string, unknown>): Promise<void>;
705
- /**
706
- * Update an item identified by its **explicit base-table primary key**
707
- * (`PrimaryKeyOf<C>`). This is the canonical form — it requires the key input
708
- * up front, so it can never silently target the wrong item.
709
- *
710
- * @example `await User.update({ userId: 'alice' }, { status: 'disabled' });`
711
- */
712
- update(key: UpdateExplicitKey<C>, changes: Partial<T>, options?: Record<string, unknown>): Promise<void>;
713
- /**
714
- * Update an item using an **updatable** entity obtained from
715
- * `query(..., { updatable: true })` / `list(..., { updatable: true })`. The
716
- * hidden base-table key carried by that entity is used directly, so this
717
- * works even for a partial `select` or a GSI-based read.
718
- *
719
- * Pass the entity as-is. A spread / clone / JSON round-trip drops both the
720
- * brand (rejected here at compile time) and the hidden key (rejected at
721
- * runtime), preventing an accidental wrong-item update.
722
- */
723
- update(entity: Partial<T> & Updatable, changes: Partial<T>, options?: Record<string, unknown>): Promise<void>;
724
- delete(key: DeleteKey<C>, options?: Record<string, unknown>): Promise<void>;
725
- }
726
-
727
- interface PutOptions {
728
- condition?: Record<string, unknown>;
729
- }
730
- interface UpdateOptions {
731
- condition?: Record<string, unknown>;
732
- }
733
- interface DeleteOptions {
734
- condition?: Record<string, unknown>;
735
- }
736
-
737
- interface PutInput {
738
- TableName: string;
739
- Item: Record<string, unknown>;
740
- ConditionExpression?: string;
741
- ExpressionAttributeNames?: Record<string, string>;
742
- ExpressionAttributeValues?: Record<string, unknown>;
743
- }
744
- declare function buildPutInput(modelClass: Function, item: Record<string, unknown>, options?: PutOptions): PutInput;
745
- declare function executePut(modelClass: Function, item: Record<string, unknown>, options?: PutOptions): Promise<void>;
746
-
747
- interface UpdateInput {
748
- TableName: string;
749
- Key: Record<string, unknown>;
750
- UpdateExpression: string;
751
- ExpressionAttributeNames: Record<string, string>;
752
- ExpressionAttributeValues?: Record<string, unknown>;
753
- ConditionExpression?: string;
754
- }
755
- declare function buildUpdateInput(modelClass: Function, entity: Record<string, unknown>, changes: Record<string, unknown>, options?: UpdateOptions): UpdateInput;
756
- declare function executeUpdate(modelClass: Function, entity: Record<string, unknown>, changes: Record<string, unknown>, options?: UpdateOptions): Promise<void>;
757
-
758
- interface DeleteInput {
759
- TableName: string;
760
- Key: Record<string, unknown>;
761
- ConditionExpression?: string;
762
- ExpressionAttributeNames?: Record<string, string>;
763
- ExpressionAttributeValues?: Record<string, unknown>;
764
- }
765
- declare function buildDeleteInput(modelClass: Function, keyObj: Record<string, unknown>, options?: DeleteOptions): DeleteInput;
766
- declare function executeDelete(modelClass: Function, keyObj: Record<string, unknown>, options?: DeleteOptions): Promise<void>;
767
-
768
- /** Result of a read operation (GetItem / Query / BatchGetItem). */
769
- interface ExecutorResult {
770
- items: Record<string, unknown>[];
771
- lastEvaluatedKey?: Record<string, unknown>;
772
- }
773
- /**
774
- * Result of a single structured write. `oldItem` is the pre-write image,
775
- * present only when the caller requested it (`returnOldImage: true`, mapped to
776
- * DynamoDB `ReturnValues: ALL_OLD`) and an item existed before the write. The
777
- * write-capture seam (issue #72) reads it to derive INSERT vs MODIFY and the
778
- * REMOVE OldImage.
779
- */
780
- interface WriteResult {
781
- oldItem?: Record<string, unknown>;
782
- }
783
- /** Options shared by the single-item structured write methods. */
784
- interface WriteExecOptions {
785
- /** Request the pre-write image (DynamoDB `ReturnValues: ALL_OLD`). */
786
- returnOldImage?: boolean;
787
- }
788
- /**
789
- * One BatchGetItem sub-request: a single physical table plus its keys and an
790
- * optional projection. Mirrors {@link import('../planner/types.js').BatchGetItemOperation}
791
- * without the discriminant `type` field, so callers that already chunked keys
792
- * (relation traversal) and the planner-driven read path share one entry point.
793
- */
794
- interface BatchGetExecInput {
795
- tableName: string;
796
- keys: Record<string, unknown>[];
797
- projectionExpression?: string;
798
- expressionAttributeNames?: Record<string, string>;
799
- consistentRead?: boolean;
800
- }
801
- /** A single item in a {@link Executor.batchWrite} request, grouped by table. */
802
- type BatchWriteExecItem = {
803
- type: 'put';
804
- item: Record<string, unknown>;
805
- } | {
806
- type: 'delete';
807
- key: Record<string, unknown>;
808
- };
809
- /**
810
- * A read-only `ConditionCheck` entry for {@link Executor.transactWrite} (issue
811
- * #81): it asserts a `ConditionExpression` holds for the keyed item without
812
- * mutating it. A failed assertion cancels the **whole** `TransactWriteItems`
813
- * atomically — the foundation for referential-integrity derivation.
814
- */
815
- interface ConditionCheckInput {
816
- TableName: string;
817
- Key: Record<string, unknown>;
818
- ConditionExpression: string;
819
- ExpressionAttributeNames?: Record<string, string>;
820
- ExpressionAttributeValues?: Record<string, unknown>;
821
- }
822
- /**
823
- * A `{ Put | Update | Delete | ConditionCheck }` entry for
824
- * {@link Executor.transactWrite}. The first three mutate an item; `ConditionCheck`
825
- * (issue #81) is a read-only assertion on another item.
826
- */
827
- type TransactWriteExecItem = {
828
- Put: PutInput;
829
- } | {
830
- Update: UpdateInput;
831
- } | {
832
- Delete: DeleteInput;
833
- } | {
834
- ConditionCheck: ConditionCheckInput;
835
- };
836
- /**
837
- * The unified I/O seam (issue #76). All of GraphDDB's read and write I/O is
838
- * funneled through one injectable executor so the engine (planner / hydrator /
839
- * relation traversal / write paths) can run unchanged against either real
840
- * DynamoDB ({@link DynamoExecutor}, the default) or an in-process memory store
841
- * (`MemoryExecutor`, the test adapter).
842
- *
843
- * Reads take a structured {@link DynamoDBOperation}; writes take the structured
844
- * inputs the operation layer already builds (`PutInput` / `UpdateInput` / …),
845
- * never an AWS SDK command. This keeps the seam at the *structured operation
846
- * boundary* (proposal option B): no expression-string parsing is required to
847
- * implement a fake.
848
- */
849
- interface Executor {
850
- /** Read path: GetItem / Query / BatchGetItem. */
851
- execute(operation: DynamoDBOperation): Promise<ExecutorResult>;
852
- /**
853
- * BatchGetItem for a single table. The caller is responsible for chunking
854
- * keys ≤100 (the relation traversal and the planner-driven path both do).
855
- */
856
- batchGet(input: BatchGetExecInput): Promise<ExecutorResult>;
857
- /** Put one item. */
858
- put(input: PutInput, options?: WriteExecOptions): Promise<WriteResult>;
859
- /** Update one item (applies its structured UpdateExpression). */
860
- update(input: UpdateInput, options?: WriteExecOptions): Promise<WriteResult>;
861
- /** Delete one item. */
862
- delete(input: DeleteInput, options?: WriteExecOptions): Promise<WriteResult>;
863
- /**
864
- * BatchWriteItem for a single table (non-atomic). Keys/items are chunked ≤25
865
- * with UnprocessedItems retry by the implementation.
866
- */
867
- batchWrite(tableName: string, items: BatchWriteExecItem[]): Promise<void>;
868
- /** TransactWriteItems — all-or-nothing atomic. */
869
- transactWrite(items: TransactWriteExecItem[]): Promise<void>;
870
- }
871
-
872
- type TransactWriteItemInput = {
873
- Put: PutInput;
874
- } | {
875
- Update: UpdateInput;
876
- } | {
877
- Delete: DeleteInput;
878
- } | {
879
- ConditionCheck: ConditionCheckInput;
880
- };
881
- declare function attachModelClass<T extends DDBModel>(modelStatic: ModelStatic<T>, modelClass: new (...args: unknown[]) => T): ModelStatic<T>;
882
- /**
883
- * Per-item capture descriptor recorded alongside each transact item, so the
884
- * write-capture seam (issue #72) can emit a labelled raw record after the
885
- * transaction commits. `TransactWriteItems` returns no images (spec §14), so
886
- * these carry the keys + the new item for Put/Update and keys only for Delete.
887
- */
888
- interface TransactCaptureMeta {
889
- modelName?: string;
890
- op: 'put' | 'update' | 'delete';
891
- table: string;
892
- keys: {
893
- pk: string;
894
- sk: string;
895
- };
896
- newItem?: Record<string, unknown>;
897
- }
898
- declare class TransactionContext {
899
- private readonly items;
900
- private readonly captureMeta;
901
- get itemCount(): number;
902
- put(model: ModelStatic<DDBModel>, item: Record<string, unknown>, options?: PutOptions): void;
903
- update(model: ModelStatic<DDBModel>, entity: Record<string, unknown>, changes: Record<string, unknown>, options?: UpdateOptions): void;
904
- delete(model: ModelStatic<DDBModel>, key: Record<string, unknown>, options?: DeleteOptions): void;
905
- /**
906
- * Add a read-only `ConditionCheck` assertion on a keyed item (issue #81). The
907
- * item is **not** mutated; the `options.condition` (e.g.
908
- * `{ attributeExists: 'PK' }` to require the row exists) is asserted, and a
909
- * failed assertion cancels the **whole** `TransactWriteItems` atomically. This
910
- * is the foundation for referential-integrity derivation (proposal: `requires
911
- * <Entity> exists`). A ConditionCheck emits no change-capture record (it writes
912
- * nothing).
913
- *
914
- * @throws if `options.condition` is missing — a ConditionCheck without an
915
- * assertion is meaningless.
916
- */
917
- conditionCheck(model: ModelStatic<DDBModel>, key: Record<string, unknown>, options: {
918
- condition: Record<string, unknown>;
919
- }): void;
920
- /** @internal */
921
- getTransactItems(): TransactWriteItemInput[];
922
- /** @internal — capture descriptors for the write-capture seam (issue #72). */
923
- getCaptureMeta(): TransactCaptureMeta[];
924
- private assertWithinLimit;
925
- }
926
- declare function executeTransaction(fn: (tx: TransactionContext) => void | Promise<void>): Promise<void>;
927
-
928
- interface BatchGetRequest {
929
- model: ModelStatic<DDBModel>;
930
- keys: Record<string, unknown>[];
931
- }
932
- interface BatchPutRequest {
933
- type: 'put';
934
- model: ModelStatic<DDBModel>;
935
- item: Record<string, unknown>;
936
- options?: PutOptions;
937
- }
938
- interface BatchDeleteRequest {
939
- type: 'delete';
940
- model: ModelStatic<DDBModel>;
941
- key: Record<string, unknown>;
942
- options?: DeleteOptions;
943
- }
944
- type BatchWriteRequest = BatchPutRequest | BatchDeleteRequest;
945
- declare class BatchGetResult {
946
- private readonly groups;
947
- constructor(groups: Map<Function, Record<string, unknown>[]>);
948
- get<T extends DDBModel>(model: ModelStatic<T>): Partial<T>[];
949
- }
950
- declare function executeBatchGet(requests: BatchGetRequest[]): Promise<BatchGetResult>;
951
- declare function executeBatchWrite(requests: BatchWriteRequest[]): Promise<void>;
952
-
953
- declare abstract class DDBModel {
954
- /** @internal Type brand — prevents primitives from structurally matching DDBModel. */
955
- private readonly __brand;
956
- static setClient(client: DynamoDBClient): void;
957
- static setTableMapping(mapping: Record<string, string>): void;
958
- static transaction(fn: (tx: TransactionContext) => void | Promise<void>): Promise<void>;
959
- static batchGet(requests: BatchGetRequest[]): Promise<BatchGetResult>;
960
- static batchWrite(requests: BatchWriteRequest[]): Promise<void>;
961
- static asModel<C extends new (...args: never[]) => DDBModel>(this: C): ModelStatic<InstanceType<C>, C>;
962
- }
963
-
964
- /**
965
- * Internal brand identifying a {@link Column} reference at runtime, used by the
966
- * `cond` raw escape hatch to recognize and alias embedded column references.
967
- */
968
- declare const COLUMN_REF: unique symbol;
969
- /**
970
- * A type-safe reference to a model field (column).
971
- *
972
- * Each column carries its declared value type as a phantom parameter so the
973
- * `cond` escape hatch can type-check embedded values against the column. The
974
- * `__model` phantom brands the column to its owning model, so passing a column
975
- * from a different model into another model's `cond` is a type error.
976
- *
977
- * @typeParam V - The declared field value type.
978
- * @typeParam M - The owning model (brand only; never read at runtime).
979
- */
980
- interface Column<V = unknown, M = unknown> {
981
- readonly [COLUMN_REF]: true;
982
- /** The underlying attribute (property) name. */
983
- readonly name: string;
984
- /** @internal phantom — value type carried for `cond` value checking. */
985
- readonly __value?: V;
986
- /** @internal phantom — owning-model brand. */
987
- readonly __model?: M;
988
- }
989
- /** Runtime guard: is this value a {@link Column} reference? */
990
- declare function isColumn(value: unknown): value is Column;
991
- /**
992
- * The `Model.col` map: one {@link Column} per scalar field of the entity, each
993
- * typed with the field's declared value type and branded to the model.
994
- *
995
- * @typeParam T - The Entity type.
996
- */
997
- type ColumnMap<T> = {
998
- [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>;
999
- };
1000
-
1001
- declare const KEY_MARKER: unique symbol;
1002
- /**
1003
- * Brand identifying a {@link KeySegment} produced by the {@link k} tag.
1004
- */
1005
- declare const KEY_SEGMENT: unique symbol;
1006
- /**
1007
- * A single structured key segment — the output of one `` k`...` `` tagged
1008
- * template. Its literal parts and the column references interpolated between
1009
- * them are preserved verbatim (the column refs are NOT stringified), so the
1010
- * planner / spec generator can derive `{field}` templates directly from the
1011
- * structure rather than recovering them by symbolic evaluation.
1012
- *
1013
- * Exactly one `` k`...` `` = one segment = one truncation boundary for a
1014
- * multi-segment sort key.
1015
- */
1016
- interface KeySegment {
1017
- readonly [KEY_SEGMENT]: true;
1018
- /** The literal string parts; always one longer than {@link columns}. */
1019
- readonly literals: readonly string[];
1020
- /** The interpolated column references, in order. */
1021
- readonly columns: readonly Column[];
1022
- }
1023
- /** Runtime guard: is this value a {@link KeySegment}? */
1024
- declare function isKeySegment(value: unknown): value is KeySegment;
1025
- /**
1026
- * The interpolation slot accepted by {@link k}: a typed column reference
1027
- * (`c.field`). Bare strings are NOT accepted — a key field must be named via a
1028
- * {@link Column} reference so the structure stays type-safe and refactor-safe.
1029
- */
1030
- type KeySlot = Column;
1031
- /**
1032
- * Tagged-template builder for a single key segment.
1033
- *
1034
- * The literal parts and the interpolated column references are kept structurally
1035
- * (never stringified), mirroring the {@link cond} escape hatch. A segment such
1036
- * as `` k`USER#${c.userId}` `` becomes `{ literals: ['USER#', ''], columns: [userId] }`,
1037
- * which the planner turns into the template `USER#{userId}`.
1038
- *
1039
- * @example
1040
- * ```ts
1041
- * key((c) => ({ pk: k`USER#${c.userId}`, sk: k`PROFILE` }));
1042
- * key((c) => ({
1043
- * pk: k`REGION#${c.region}`,
1044
- * sk: [k`CITY#${c.city}`, k`STORE#${c.store}`],
1045
- * }));
1046
- * ```
1047
- */
1048
- declare function k(literals: TemplateStringsArray, ...columns: KeySlot[]): KeySegment;
1049
- /** A single segment, or an ordered list of segments (multi-segment SK). */
1050
- type SegmentSpec = KeySegment | KeySegment[];
1051
- /** The `{ pk, sk }` structure returned by a key builder. */
1052
- interface KeyStructure {
1053
- readonly pk: SegmentSpec;
1054
- readonly sk?: SegmentSpec;
1055
- }
1056
- /**
1057
- * The canonical, structured representation of a key (PK or SK as a list of
1058
- * segments). This — not an opaque mapping closure — is the single source of
1059
- * truth for a model's key.
1060
- */
1061
- interface SegmentedKey {
1062
- /** Partition-key segments (all required at resolve time). */
1063
- readonly pkSegments: readonly KeySegment[];
1064
- /** Sort-key segments, evaluated in order with truncation. */
1065
- readonly skSegments: readonly KeySegment[];
1066
- }
1067
- interface KeyDefinitionMarker<T extends Record<string, unknown> = Record<string, unknown>> {
1068
- readonly [KEY_MARKER]: true;
1069
- readonly segmented: SegmentedKey;
1070
- readonly inputFieldNames: string[];
1071
- /** @internal Phantom field for type-level input type extraction. */
1072
- readonly _phantom?: T;
1073
- }
1074
- /**
1075
- * Define a model's primary key from a **structured segment** builder.
1076
- *
1077
- * The builder receives a typed column accessor `c` (one `Column` per field) and
1078
- * returns `{ pk, sk }`, where each of `pk` / `sk` is a single `` k`...` ``
1079
- * segment or an array of them. Column references are captured structurally, so
1080
- * the key template is recovered directly from the structure — no symbolic
1081
- * evaluation of an opaque closure.
1082
- *
1083
- * @typeParam T - The key input type (drives compile-time key checking for
1084
- * `query` / `list`). Supplied by the caller's parameter annotation on `c`.
1085
- */
1086
- declare function key<T extends Record<string, unknown>>(builder: (c: {
1087
- readonly [K in keyof T]-?: Column<T[K], T>;
1088
- }) => KeyStructure): KeyDefinitionMarker<T>;
1089
-
1090
- /**
1091
- * CDC emulator types (issue #72). These types are **cdc-module-owned**; core
1092
- * never references them. The {@link ChangeEvent} shape mirrors DynamoDB Streams
1093
- * (`NEW_AND_OLD_IMAGES`) so a consumer written against the emulator runs
1094
- * unchanged against real Streams in production (spec §4).
1095
- */
1096
- /** Stream view type. Fixed to `NEW_AND_OLD_IMAGES` — incremental aggregation
1097
- * needs both images (spec §4). */
1098
- type StreamViewType = 'NEW_AND_OLD_IMAGES';
1099
- /** Stream event kind, matching DynamoDB Streams. */
1100
- type ChangeEventName = 'INSERT' | 'MODIFY' | 'REMOVE';
1101
- /**
1102
- * A single change event. Within one shard, `sequenceNumber` is monotonically
1103
- * increasing; across shards order is independent (spec §6).
1104
- */
1105
- interface ChangeEvent<T = Record<string, unknown>> {
1106
- eventName: ChangeEventName;
1107
- table: string;
1108
- /** Resolved model name when known. */
1109
- model?: string;
1110
- keys: {
1111
- pk: string;
1112
- sk: string;
1113
- };
1114
- /** Present for MODIFY / REMOVE. */
1115
- oldImage?: T;
1116
- /** Present for INSERT / MODIFY. */
1117
- newImage?: T;
1118
- /** Deterministic-clock-derived creation time (ISO 8601). */
1119
- approximateCreationTime: string;
1120
- /** Monotonic within a shard (zero-padded for lexicographic ordering). */
1121
- sequenceNumber: string;
1122
- /** Partition = hash(pk). */
1123
- shardId: string;
1124
- }
1125
- /** A delivered batch. Records of the same shard are in `sequenceNumber` order. */
1126
- interface ChangeBatch<T = Record<string, unknown>> {
1127
- records: ChangeEvent<T>[];
1128
- }
1129
- /**
1130
- * The consumer's response to a batch (spec §6, `ReportBatchItemFailures`
1131
- * equivalent). Returning the `sequenceNumber`s of records that failed lets the
1132
- * emulator advance the checkpoint past the successful prefix and redeliver only
1133
- * the failures.
1134
- */
1135
- interface BatchResult {
1136
- /** `sequenceNumber`s of records the consumer failed to process. */
1137
- batchItemFailures?: string[];
1138
- }
1139
- /** The consumer handler. */
1140
- type ChangeHandler = (batch: ChangeBatch) => Promise<BatchResult | void>;
1141
- /** Unsubscribe handle. */
1142
- type Unsubscribe = () => void;
1143
- /** Where a fresh subscriber begins reading. */
1144
- type StartingPosition = 'TRIM_HORIZON' | 'LATEST';
1145
- /** Clock mode. `virtual` makes time deterministic via `advanceClock`. */
1146
- type ClockMode = 'real' | 'virtual';
1147
- /** Delivery mode (spec §7). */
1148
- type CdcMode = 'inline' | 'queued' | 'record' | 'replay';
1149
- /** Deterministic fault-injection spec (spec §8). All probabilities are seeded. */
1150
- interface FaultSpec {
1151
- /** Probability [0,1] a delivered record is also re-delivered (duplicate). */
1152
- duplicate?: number;
1153
- /** Shuffle within-shard order on delivery (invariant-violation testing). */
1154
- reorder?: boolean;
1155
- /** Per-record virtual-time delivery delay range (ms). Requires queued mode. */
1156
- delay?: {
1157
- min: number;
1158
- max: number;
1159
- };
1160
- /** Probability [0,1] a record is dropped on first delivery then redelivered. */
1161
- dropThenRedeliver?: number;
1162
- /** Probability [0,1] a record is reported as a partial-batch failure. */
1163
- partialBatchFailure?: number;
1164
- }
1165
- /** A reference whose recompute should be forced to race a mark (spec §8, §7). */
1166
- interface ConcurrentRecomputeRef {
1167
- /** Opaque node ref the harness recomputes concurrently with marking. */
1168
- ref: string;
1169
- }
1170
- /** A persisted event log (spec §10, record/replay). */
1171
- interface EventLog {
1172
- seed: number;
1173
- events: ChangeEvent[];
1174
- }
1175
- /** Replay options (spec §10). */
1176
- interface ReplayOptions {
1177
- /** Deliver events in a deterministic shuffled order (any-order replay). */
1178
- shuffle?: boolean;
1179
- /** Probability [0,1] each event is delivered twice during replay. */
1180
- duplicate?: number;
1181
- }
1182
- /** Constructor options (spec §11). */
1183
- interface CdcEmulatorOptions {
1184
- /** Stream-enabled model classes (opt-in; default none). */
1185
- models?: Function[];
1186
- /** Delivery mode. Default `inline`. */
1187
- mode?: CdcMode;
1188
- /** Clock mode. Default `virtual` for queued, `real` otherwise. */
1189
- clock?: ClockMode;
1190
- /** Records per delivered batch. Default 100. */
1191
- batchSize?: number;
1192
- /** Max redelivery attempts before a record goes to the DLQ. Default 3. */
1193
- maxRetries?: number;
1194
- /** Where a subscriber starts. Default `TRIM_HORIZON`. */
1195
- startingPosition?: StartingPosition;
1196
- /** Seed for fault injection / shard assignment determinism. Default 1. */
1197
- seed?: number;
1198
- /** Number of shards events are partitioned across. Default 8. */
1199
- shardCount?: number;
1200
- }
1201
- type ShardId = string;
1202
-
1203
- export { type KeyStructure as $, type CdcMode as A, type BatchGetExecInput as B, type ChangeEvent as C, type DynamoDBOperation as D, type Executor as E, type FaultSpec as F, type ChangeBatch as G, type ChangeEventName as H, type ClockMode as I, type Column as J, type ColumnMap as K, type CondSlot as L, type ModelStatic as M, type ConditionCheckInput as N, type DeleteOptions as O, type PutInput as P, type FilterInput as Q, type ResolvedKey as R, type SelectableOf as S, type TransactWriteExecItem as T, type UpdateInput as U, type GsiDefinitionMarker as V, type WriteExecOptions as W, type GsiOptions as X, type KeyDefinitionMarker as Y, type KeySegment as Z, type KeySlot as _, type ExecutorResult as a, type PutOptions as a0, type QueryKeyOf as a1, type QueryResult as a2, type RawCondition as a3, type RelationBuilder as a4, type RelationSelect as a5, type RelationSpec as a6, type SegmentSpec as a7, type SelectBuilder as a8, type SelectOf as a9, type StartingPosition as aa, type StreamViewType as ab, TransactionContext as ac, type Updatable as ad, type UpdateOptions as ae, attachModelClass as af, buildDeleteInput as ag, buildPutInput as ah, buildUpdateInput as ai, cond as aj, executeBatchGet as ak, executeBatchWrite as al, executeDelete as am, executePut as an, executeTransaction as ao, executeUpdate as ap, gsi as aq, isColumn as ar, isKeySegment as as, k as at, key as au, type WriteResult as b, type DeleteInput as c, type BatchWriteExecItem as d, DDBModel as e, type PrimaryKeyOf as f, type SegmentedKey as g, type SelectBuilderSpec as h, type ExecutionPlan as i, type CdcEmulatorOptions as j, type ChangeHandler as k, type Unsubscribe as l, type ConcurrentRecomputeRef as m, type EventLog as n, type ReplayOptions as o, type ShardId as p, type PartialQueryKeyOf as q, type StrictSelectSpec as r, type EntityInput as s, type UniqueQueryKeyOf as t, type BatchDeleteRequest as u, type BatchGetRequest as v, BatchGetResult as w, type BatchPutRequest as x, type BatchResult as y, type BatchWriteRequest as z };