graphddb 0.7.9 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/cdc/index.d.ts +389 -4
- package/dist/cdc/index.js +4 -3
- package/dist/{chunk-ZPNRLOKA.js → chunk-GS4C5VGO.js} +4 -6
- package/dist/chunk-HNY2EJPV.js +1184 -0
- package/dist/{chunk-NYM7K2ST.js → chunk-I4LEJ4TF.js} +3812 -6724
- package/dist/{chunk-PFFPLD4B.js → chunk-L2NEDS7U.js} +725 -2112
- package/dist/chunk-L4QRCHRQ.js +278 -0
- package/dist/chunk-LGHSZIEE.js +187 -0
- package/dist/chunk-N4NWYNGZ.js +1987 -0
- package/dist/{chunk-PDUVTYC5.js → chunk-XTWXMOHD.js} +0 -1
- package/dist/cli.js +63 -252
- package/dist/index.d.ts +23 -1548
- package/dist/index.js +100 -1778
- package/dist/internal/index.d.ts +84 -0
- package/dist/internal/index.js +701 -0
- package/dist/{maintenance-view-adapter-BATUh_I8.d.ts → key-DR7_lpyk.d.ts} +538 -2975
- package/dist/linter/index.d.ts +39 -6
- package/dist/linter/index.js +22 -4
- package/dist/{registry-CXhP4TaE.d.ts → linter-C-vypgut.d.ts} +22 -22
- package/dist/prepared-artifact-BpPgkXEo.d.ts +281 -0
- package/dist/spec/index.d.ts +506 -4
- package/dist/spec/index.js +36 -17
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +4 -3
- package/dist/transform/index.d.ts +460 -1
- package/dist/transform/index.js +2085 -2
- package/dist/types-2PMXEn5x.d.ts +1205 -0
- package/dist/types-BXLzIcQD.d.ts +450 -0
- package/docs/cdc-projection.md +5 -5
- package/docs/class-hydration.md +1 -1
- package/docs/cqrs-contract.md +28 -20
- package/docs/design-patterns.md +5 -5
- package/docs/docs-generation.md +6 -6
- package/docs/middleware.md +15 -15
- package/docs/mutation-command-derivation.md +52 -42
- package/docs/prepared-statements.md +14 -14
- package/docs/python-bridge.md +113 -66
- package/docs/spec.md +153 -124
- package/docs/testing.md +9 -8
- package/package.json +20 -5
- package/dist/chunk-MMVHOUM4.js +0 -24
- package/dist/from-change-DanwjE5b.d.ts +0 -327
- package/dist/index-CtPJSMrc.d.ts +0 -934
- package/dist/relation-depth-Dg3yhl7S.d.ts +0 -36
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a1 as Param, E as ExpressionSpec, a2 as ParamKind, T as TransactionSpec, X as TransactionItemSpec } from './types-2PMXEn5x.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* CDC emulator types (issue #72). These types are **cdc-module-owned**; core
|
|
@@ -113,132 +113,6 @@ interface CdcEmulatorOptions {
|
|
|
113
113
|
}
|
|
114
114
|
type ShardId = string;
|
|
115
115
|
|
|
116
|
-
/**
|
|
117
|
-
* App-level throttle / transient-error retry for single-op sends (issue #111).
|
|
118
|
-
*
|
|
119
|
-
* GraphDDB's single-item ops (GetItem / Query / Put / Update / Delete) and the
|
|
120
|
-
* read fan-out (relation resolution) used to throw on throttling
|
|
121
|
-
* (`ProvisionedThroughputExceededException` / `ThrottlingException`) and rely
|
|
122
|
-
* solely on the AWS SDK v3 default retry, so the LIBRARY itself guaranteed no
|
|
123
|
-
* throttle-resilience. The batch ops already retry partial completion
|
|
124
|
-
* (`UnprocessedKeys` / `UnprocessedItems`, see `src/operations/batch-retry.ts`);
|
|
125
|
-
* this module is the SEPARATE, library-owned ERROR retry that wraps every
|
|
126
|
-
* `docClient.send()` for the single ops via {@link RetryingExecutor}.
|
|
127
|
-
*
|
|
128
|
-
* The two retry layers are DISTINCT and must not be merged:
|
|
129
|
-
* - (A) partial-batch retry (`batch-retry.ts`) — DynamoDB accepted the request
|
|
130
|
-
* but could not process every key/item; the request itself did NOT error.
|
|
131
|
-
* - (B) error retry (this module) — `send()` REJECTED with a throttle / transient
|
|
132
|
-
* error and is retried whole.
|
|
133
|
-
* They share {@link computeBackoffDelay} so backoff is consistent across both,
|
|
134
|
-
* but they are different mechanisms.
|
|
135
|
-
*
|
|
136
|
-
* The default policy is ENABLED out of the box (see {@link DEFAULT_RETRY_POLICY}):
|
|
137
|
-
* even with zero configuration a sensible exponential-backoff-with-jitter,
|
|
138
|
-
* bounded-attempts retry applies. Users tune it globally via
|
|
139
|
-
* `DDBModel.setRetryPolicy(...)` or per-call via the `retry?:` option on the
|
|
140
|
-
* operation option bags; `retry: false` disables retry for that one call.
|
|
141
|
-
*
|
|
142
|
-
* NOTE on the AWS SDK's OWN retry: now that the library owns retry, a
|
|
143
|
-
* `DynamoDBClient` left at the SDK default `maxAttempts: 3` retries
|
|
144
|
-
* MULTIPLICATIVELY underneath this layer. Users should construct their client
|
|
145
|
-
* with `maxAttempts: 1` so the library is the single source of truth for retry
|
|
146
|
-
* (documented at `DDBModel.setClient` and in the README). The library does NOT
|
|
147
|
-
* silently mutate a user-provided client.
|
|
148
|
-
*/
|
|
149
|
-
/** The single-op kind a retry is wrapping — surfaced to {@link RetryPolicy.onRetry}. */
|
|
150
|
-
type RetryOperationKind = 'get' | 'query' | 'batchGet' | 'put' | 'update' | 'delete' | 'batchWrite' | 'transactWrite';
|
|
151
|
-
/** The context handed to {@link RetryPolicy.onRetry} before each backoff sleep. */
|
|
152
|
-
interface RetryInfo {
|
|
153
|
-
/** 1-based attempt number that just FAILED and is about to be retried. */
|
|
154
|
-
readonly attempt: number;
|
|
155
|
-
/** The throttle / transient error that triggered this retry. */
|
|
156
|
-
readonly error: unknown;
|
|
157
|
-
/** The backoff delay (ms) about to be slept before the next attempt. */
|
|
158
|
-
readonly delayMs: number;
|
|
159
|
-
/** Which single-op kind is being retried. */
|
|
160
|
-
readonly operation: RetryOperationKind;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* A retry policy for the single-op error-retry layer (issue #111). Every field is
|
|
164
|
-
* optional and resolved INDEPENDENTLY, in layers (see {@link resolveRetryPolicy}):
|
|
165
|
-
* a per-call override field wins, else the globally-configured policy's field
|
|
166
|
-
* (from {@link import('../model/DDBModel.js').DDBModel.setRetryPolicy}), else
|
|
167
|
-
* {@link DEFAULT_RETRY_POLICY}. So a partial per-call override (e.g.
|
|
168
|
-
* `{ maxAttempts: 3 }`) overrides ONLY that field and INHERITS the rest from the
|
|
169
|
-
* global policy — a global `onRetry` / `jitter` is NOT dropped. A policy value of
|
|
170
|
-
* `false` on an option bag disables retry entirely for that call.
|
|
171
|
-
*/
|
|
172
|
-
interface RetryPolicy {
|
|
173
|
-
/**
|
|
174
|
-
* Maximum number of attempts (the FIRST try counts as attempt 1). `1` disables
|
|
175
|
-
* retry (a single try, no backoff). Default {@link DEFAULT_MAX_ATTEMPTS}.
|
|
176
|
-
*/
|
|
177
|
-
readonly maxAttempts?: number;
|
|
178
|
-
/**
|
|
179
|
-
* Compute the base backoff delay (ms) for a 1-based retry attempt, BEFORE jitter.
|
|
180
|
-
* Defaults to {@link computeBackoffDelay} (`min(1000, 50*2^(attempt-1))`), the
|
|
181
|
-
* same ramp the batch partial-retry layer uses, so backoff is consistent across
|
|
182
|
-
* the two layers.
|
|
183
|
-
*/
|
|
184
|
-
readonly computeDelay?: (attempt: number) => number;
|
|
185
|
-
/**
|
|
186
|
-
* Jitter strategy applied to the base delay:
|
|
187
|
-
* - `'full'` (default) — uniform random in `[0, base]` (AWS "full jitter"),
|
|
188
|
-
* the recommended strategy to de-correlate retries across clients.
|
|
189
|
-
* - `'none'` — use the base delay verbatim (deterministic; handy in tests).
|
|
190
|
-
*/
|
|
191
|
-
readonly jitter?: 'full' | 'none';
|
|
192
|
-
/**
|
|
193
|
-
* Classify whether an error is retryable. Defaults to {@link isRetryableError}
|
|
194
|
-
* (throttle + 5xx / transient). A custom classifier fully replaces the default —
|
|
195
|
-
* it should still avoid retrying validation / conditional-check failures.
|
|
196
|
-
*/
|
|
197
|
-
readonly isRetryable?: (error: unknown) => boolean;
|
|
198
|
-
/**
|
|
199
|
-
* Observability hook (issue #111 §8): invoked BEFORE each backoff sleep with the
|
|
200
|
-
* attempt number, the triggering error, the computed (post-jitter) delay, and the
|
|
201
|
-
* operation kind. Never invoked on the terminal success or the terminal failure.
|
|
202
|
-
*/
|
|
203
|
-
readonly onRetry?: (info: RetryInfo) => void;
|
|
204
|
-
}
|
|
205
|
-
/** Default cap on attempts: the first try plus up to 9 retries (mirrors the batch cap). */
|
|
206
|
-
declare const DEFAULT_MAX_ATTEMPTS = 10;
|
|
207
|
-
/**
|
|
208
|
-
* The default, always-on retry policy (issue #111 §3). Exponential backoff (the
|
|
209
|
-
* shared {@link computeBackoffDelay} ramp) + full jitter, capped at
|
|
210
|
-
* {@link DEFAULT_MAX_ATTEMPTS} attempts, retrying throttle / 5xx / transient
|
|
211
|
-
* errors only (never validation / conditional-check failures).
|
|
212
|
-
*/
|
|
213
|
-
declare const DEFAULT_RETRY_POLICY: Required<Pick<RetryPolicy, 'maxAttempts' | 'computeDelay' | 'jitter' | 'isRetryable'>>;
|
|
214
|
-
/**
|
|
215
|
-
* Default error classifier (issue #111 §5). Retryable when the error is a known
|
|
216
|
-
* throttle / transient fault, an AWS SDK error flagged `$retryable`, or a 5xx /
|
|
217
|
-
* 429 status. NEVER retryable for validation / conditional-check / not-found /
|
|
218
|
-
* access-denied — those are deterministic and would just burn the attempt budget.
|
|
219
|
-
*
|
|
220
|
-
* NOTE: a {@link https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html
|
|
221
|
-
* TransactionCanceledException} is handled SEPARATELY by
|
|
222
|
-
* {@link isRetryableTransactionCancellation}; this function treats it as
|
|
223
|
-
* non-retryable so the transact path can inspect its `CancellationReasons` first.
|
|
224
|
-
*/
|
|
225
|
-
declare function isRetryableError(error: unknown): boolean;
|
|
226
|
-
/**
|
|
227
|
-
* Classify a `TransactionCanceledException` (issue #111 §6). Inspects the
|
|
228
|
-
* `CancellationReasons`:
|
|
229
|
-
* - ANY `ConditionalCheckFailed` reason ⇒ NOT retryable (a deterministic
|
|
230
|
-
* condition failure; retrying would just fail again) — return `false`.
|
|
231
|
-
* - Otherwise, when EVERY non-`None` reason is a throttle / conflict code
|
|
232
|
-
* (`ThrottlingError` / `ProvisionedThroughputExceeded` / `TransactionConflict`)
|
|
233
|
-
* ⇒ retryable — return `true`.
|
|
234
|
-
* - A cancellation with no inspectable reasons, or any other reason code, is
|
|
235
|
-
* treated conservatively as NOT retryable.
|
|
236
|
-
*
|
|
237
|
-
* Only meaningful for an error whose name is `TransactionCanceledException`; the
|
|
238
|
-
* caller checks that first.
|
|
239
|
-
*/
|
|
240
|
-
declare function isRetryableTransactionCancellation(error: unknown): boolean;
|
|
241
|
-
|
|
242
116
|
interface RangeCondition {
|
|
243
117
|
operator: 'begins_with';
|
|
244
118
|
key: string;
|
|
@@ -279,149 +153,6 @@ type DynamoDBOperation = GetItemOperation | QueryOperation | BatchGetItemOperati
|
|
|
279
153
|
interface ExecutionPlan {
|
|
280
154
|
operations: DynamoDBOperation[];
|
|
281
155
|
}
|
|
282
|
-
type ResolvedKey = {
|
|
283
|
-
type: 'pk';
|
|
284
|
-
partial: boolean;
|
|
285
|
-
inputFieldNames: string[];
|
|
286
|
-
} | {
|
|
287
|
-
type: 'gsi';
|
|
288
|
-
indexName: string;
|
|
289
|
-
unique: boolean;
|
|
290
|
-
partial: boolean;
|
|
291
|
-
inputFieldNames: string[];
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Parameter placeholders for the parameterized query / command definition DSL
|
|
296
|
-
* (issue #41, Python-bridge Phase 0a).
|
|
297
|
-
*
|
|
298
|
-
* A `Param<T>` is a **branded placeholder** standing in for a value that is not
|
|
299
|
-
* known at definition time but is supplied later (at execution, e.g. from
|
|
300
|
-
* Python). It carries:
|
|
301
|
-
*
|
|
302
|
-
* - the **TypeScript value type** `T` it represents (`string`, `number`, or a
|
|
303
|
-
* string-literal union), preserved through the IR and the inferred return
|
|
304
|
-
* types of `defineQueries` / `defineCommands`; and
|
|
305
|
-
* - a **runtime descriptor** (`kind` + optional `literals`) that the static
|
|
306
|
-
* planner (#42) and the code generator read to emit `operations.json`.
|
|
307
|
-
*
|
|
308
|
-
* Placeholders are created with `param.string()`, `param.number()`, and
|
|
309
|
-
* `param.literal(...)`. They are *only* legal at value positions inside a
|
|
310
|
-
* parameterized key / changes structure passed to the `define*` entry points —
|
|
311
|
-
* never to the live `Model.query` / `Model.putItem` runtime API, whose types remain
|
|
312
|
-
* uncontaminated by params.
|
|
313
|
-
*/
|
|
314
|
-
/** Brand tag preventing a plain object from structurally matching a {@link Param}. */
|
|
315
|
-
declare const PARAM_BRAND: unique symbol;
|
|
316
|
-
/** Runtime discriminant for a parameter placeholder. */
|
|
317
|
-
type ParamKind = 'string' | 'number' | 'literal' | 'array';
|
|
318
|
-
/**
|
|
319
|
-
* A branded parameter placeholder representing a value of TypeScript type `T`
|
|
320
|
-
* that is bound at execution time rather than definition time.
|
|
321
|
-
*
|
|
322
|
-
* The brand (`[PARAM_BRAND]: T`) keeps `Param<string>` and `Param<number>`
|
|
323
|
-
* distinct and prevents a bare value from being mistaken for a placeholder.
|
|
324
|
-
*
|
|
325
|
-
* @typeParam T - The value type this placeholder stands in for.
|
|
326
|
-
*/
|
|
327
|
-
interface Param<out T> {
|
|
328
|
-
/** @internal Type brand carrying the represented value type. */
|
|
329
|
-
readonly [PARAM_BRAND]: T;
|
|
330
|
-
/** Runtime discriminant: `'string'` | `'number'` | `'literal'` | `'array'`. */
|
|
331
|
-
readonly kind: ParamKind;
|
|
332
|
-
/**
|
|
333
|
-
* For `param.literal(...)`, the allowed literal values (preserved at runtime
|
|
334
|
-
* for the generator). `undefined` for `string` / `number`.
|
|
335
|
-
*/
|
|
336
|
-
readonly literals?: readonly T[];
|
|
337
|
-
/**
|
|
338
|
-
* For `param.array({...})`, the descriptor of each element's fields. Lets the
|
|
339
|
-
* transaction planner (#46) type `forEach` element references and emit the
|
|
340
|
-
* `{item.<field>}` templates. `undefined` for scalar params.
|
|
341
|
-
*/
|
|
342
|
-
readonly element?: Readonly<Record<string, Param<unknown>>>;
|
|
343
|
-
/**
|
|
344
|
-
* Optional human-readable description of the parameter (issue #154), supplied
|
|
345
|
-
* via `param.string({ description })` etc. Pure documentation metadata — it does
|
|
346
|
-
* NOT affect the runtime descriptor (`kind` / `literals`) the planner reads for
|
|
347
|
-
* execution. When present it is propagated to the param's {@link
|
|
348
|
-
* import('./ir.js').ParamDescriptor} and then to the serialized
|
|
349
|
-
* {@link import('../spec/types.js').ParamSpec} in `operations.json`. Absent →
|
|
350
|
-
* unchanged output (backward compatible).
|
|
351
|
-
*/
|
|
352
|
-
readonly description?: string;
|
|
353
|
-
}
|
|
354
|
-
/** A `Param` whose represented value type is `string`. */
|
|
355
|
-
type StringParam = Param<string>;
|
|
356
|
-
/** A `Param` whose represented value type is `number`. */
|
|
357
|
-
type NumberParam = Param<number>;
|
|
358
|
-
/** A `Param` whose represented value type is the literal union `L`. */
|
|
359
|
-
type LiteralParam<L extends string | number> = Param<L>;
|
|
360
|
-
/** The element-field descriptor shape accepted by {@link param.array}. */
|
|
361
|
-
type ArrayElementShape = Record<string, Param<unknown>>;
|
|
362
|
-
/**
|
|
363
|
-
* The TypeScript element type implied by an {@link ArrayElementShape} `E`: each
|
|
364
|
-
* field carries the value type its placeholder represents.
|
|
365
|
-
*/
|
|
366
|
-
type ElementOf<E extends ArrayElementShape> = {
|
|
367
|
-
[K in keyof E]: E[K] extends Param<infer V> ? V : never;
|
|
368
|
-
};
|
|
369
|
-
/** A `Param` standing in for an **array** whose elements have shape `E`. */
|
|
370
|
-
type ArrayParam<E extends ArrayElementShape> = Param<ElementOf<E>[]> & {
|
|
371
|
-
readonly element: E;
|
|
372
|
-
};
|
|
373
|
-
/** Allowed scalar value types a placeholder may stand in for. */
|
|
374
|
-
type ParamValue = string | number;
|
|
375
|
-
/**
|
|
376
|
-
* Options accepted by the placeholder factories (issue #154). Currently carries
|
|
377
|
-
* only the optional human-readable {@link Param.description} — pure documentation
|
|
378
|
-
* metadata that does not affect the runtime descriptor. The object form is
|
|
379
|
-
* additive: `param.string()` (no options) is unchanged.
|
|
380
|
-
*/
|
|
381
|
-
interface ParamOptions {
|
|
382
|
-
/** Optional human-readable description, propagated to `operations.json`. */
|
|
383
|
-
readonly description?: string;
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Placeholder factory. Each call returns a branded {@link Param} carrying both
|
|
387
|
-
* the TypeScript value type and a runtime descriptor. Each scalar factory accepts
|
|
388
|
-
* an optional {@link ParamOptions} (issue #154) carrying a `description` — pure
|
|
389
|
-
* documentation propagated to `operations.json`; the no-argument call is unchanged.
|
|
390
|
-
*/
|
|
391
|
-
declare const param: {
|
|
392
|
-
/** A placeholder for a `string` value. */
|
|
393
|
-
readonly string: (options?: ParamOptions) => StringParam;
|
|
394
|
-
/** A placeholder for a `number` value. */
|
|
395
|
-
readonly number: (options?: ParamOptions) => NumberParam;
|
|
396
|
-
/**
|
|
397
|
-
* A placeholder constrained to one of the given literal values. The value
|
|
398
|
-
* type of the resulting {@link Param} is the **union of the literals**, so it
|
|
399
|
-
* is preserved precisely in the IR and the inferred definition types. A final
|
|
400
|
-
* plain-object argument is read as {@link ParamOptions} (a `description`),
|
|
401
|
-
* distinguished from the `string` / `number` literal values.
|
|
402
|
-
*
|
|
403
|
-
* @example
|
|
404
|
-
* ```ts
|
|
405
|
-
* param.literal('active', 'disabled'); // Param<'active' | 'disabled'>
|
|
406
|
-
* param.literal('active', 'disabled', { description: 'Account state.' });
|
|
407
|
-
* ```
|
|
408
|
-
*/
|
|
409
|
-
readonly literal: <const L extends readonly [ParamValue, ...ParamValue[]]>(...valuesAndOptions: [...L] | [...L, ParamOptions]) => LiteralParam<L[number]>;
|
|
410
|
-
/**
|
|
411
|
-
* A placeholder for an **array** parameter whose elements have the given field
|
|
412
|
-
* shape. Used by `defineTransaction`'s `tx.forEach(p.<arrayParam>, …)` to bind
|
|
413
|
-
* each element's fields to `{item.<field>}` templates.
|
|
414
|
-
*
|
|
415
|
-
* @example
|
|
416
|
-
* ```ts
|
|
417
|
-
* param.array({ userId: param.string(), role: param.string() });
|
|
418
|
-
* // Param<{ userId: string; role: string }[]>
|
|
419
|
-
* ```
|
|
420
|
-
*/
|
|
421
|
-
readonly array: <const E extends ArrayElementShape>(element: E, options?: ParamOptions) => ArrayParam<E>;
|
|
422
|
-
};
|
|
423
|
-
/** Runtime type guard: is `value` a parameter placeholder? */
|
|
424
|
-
declare function isParam(value: unknown): value is Param<unknown>;
|
|
425
156
|
|
|
426
157
|
/**
|
|
427
158
|
* Internal brand identifying a {@link RawCondition} produced by {@link cond}.
|
|
@@ -598,27 +329,6 @@ interface WriteOptions<T> {
|
|
|
598
329
|
readonly condition?: WriteCondition<T>;
|
|
599
330
|
}
|
|
600
331
|
|
|
601
|
-
/**
|
|
602
|
-
* Internal brand symbol identifying a compiled select builder at runtime.
|
|
603
|
-
* Consumers should not depend on this; it is used by the execution layer to
|
|
604
|
-
* distinguish a builder instance from a plain select object.
|
|
605
|
-
*/
|
|
606
|
-
declare const SELECT_BUILDER: unique symbol;
|
|
607
|
-
/**
|
|
608
|
-
* The normalized spec a select builder resolves to. This is the shape the
|
|
609
|
-
* planner / traversal layer consumes. `filter` is the declarative server-side
|
|
610
|
-
* DynamoDB FilterExpression input.
|
|
611
|
-
*
|
|
612
|
-
* @internal
|
|
613
|
-
*/
|
|
614
|
-
interface SelectBuilderSpec {
|
|
615
|
-
readonly [SELECT_BUILDER]: true;
|
|
616
|
-
select: Record<string, unknown>;
|
|
617
|
-
filter?: Record<string, unknown>;
|
|
618
|
-
limit?: number;
|
|
619
|
-
after?: string;
|
|
620
|
-
order?: 'ASC' | 'DESC';
|
|
621
|
-
}
|
|
622
332
|
/**
|
|
623
333
|
* Top-level select builder produced by `Model.project(...)`.
|
|
624
334
|
*
|
|
@@ -958,65 +668,6 @@ type AtLeastOneKey<T> = {
|
|
|
958
668
|
*/
|
|
959
669
|
type PartialQueryKeyOf<C> = QueryKeyOf<C> extends infer U ? U extends Record<string, unknown> ? AtLeastOneKey<U> : never : never;
|
|
960
670
|
|
|
961
|
-
interface PutOptions<T = unknown> {
|
|
962
|
-
condition?: WriteCondition<T>;
|
|
963
|
-
/**
|
|
964
|
-
* Per-call throttle / transient-error retry override (issue #111). A
|
|
965
|
-
* {@link RetryPolicy} replaces the global policy for this call; `false` disables
|
|
966
|
-
* retry for this call. Omit to use the configured (or built-in default) policy.
|
|
967
|
-
*/
|
|
968
|
-
retry?: RetryOverride;
|
|
969
|
-
/**
|
|
970
|
-
* Host-injected per-call write context (issue #50 / #139) — exposed to every
|
|
971
|
-
* write hook (W1–W5) as `ctx.context`. A host-language value, NEVER serialized
|
|
972
|
-
* into the SSoT / `operations.json` (the bridge #48 is unaffected). Absent ⇒ `{}`.
|
|
973
|
-
*/
|
|
974
|
-
context?: RequestContext;
|
|
975
|
-
}
|
|
976
|
-
interface UpdateOptions<T = unknown> {
|
|
977
|
-
condition?: WriteCondition<T>;
|
|
978
|
-
/**
|
|
979
|
-
* Per-call throttle / transient-error retry override (issue #111). A
|
|
980
|
-
* {@link RetryPolicy} replaces the global policy for this call; `false` disables
|
|
981
|
-
* retry for this call.
|
|
982
|
-
*/
|
|
983
|
-
retry?: RetryOverride;
|
|
984
|
-
/**
|
|
985
|
-
* How to re-derive a GSI key whose composing fields are changed but **not all**
|
|
986
|
-
* available from `{ ...key, ...changes }` (issue #115).
|
|
987
|
-
*
|
|
988
|
-
* - **unset (default)** — refuse with an explicit error rather than let the
|
|
989
|
-
* index silently rot. The error names the index, the changed field, and the
|
|
990
|
-
* missing field(s). The happy path (all composing fields available) re-derives
|
|
991
|
-
* in the SAME `UpdateExpression` and is unaffected by this option.
|
|
992
|
-
* - `'read-modify-write'` — read the current item, merge `{ ...item, ...changes }`,
|
|
993
|
-
* re-derive every affected GSI key from the merged image, and write back under
|
|
994
|
-
* an optimistic condition (the item must still exist / be unchanged). This costs
|
|
995
|
-
* one extra read and is NOT atomic with the original update, but always re-derives
|
|
996
|
-
* correctly.
|
|
997
|
-
*/
|
|
998
|
-
rederive?: 'read-modify-write';
|
|
999
|
-
/**
|
|
1000
|
-
* Host-injected per-call write context (issue #50 / #139) — exposed to every
|
|
1001
|
-
* write hook (W1–W5) as `ctx.context`. NEVER serialized. Absent ⇒ `{}`.
|
|
1002
|
-
*/
|
|
1003
|
-
context?: RequestContext;
|
|
1004
|
-
}
|
|
1005
|
-
interface DeleteOptions<T = unknown> {
|
|
1006
|
-
condition?: WriteCondition<T>;
|
|
1007
|
-
/**
|
|
1008
|
-
* Per-call throttle / transient-error retry override (issue #111). A
|
|
1009
|
-
* {@link RetryPolicy} replaces the global policy for this call; `false` disables
|
|
1010
|
-
* retry for this call.
|
|
1011
|
-
*/
|
|
1012
|
-
retry?: RetryOverride;
|
|
1013
|
-
/**
|
|
1014
|
-
* Host-injected per-call write context (issue #50 / #139) — exposed to every
|
|
1015
|
-
* write hook (W1–W5) as `ctx.context`. NEVER serialized. Absent ⇒ `{}`.
|
|
1016
|
-
*/
|
|
1017
|
-
context?: RequestContext;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
671
|
interface PutInput {
|
|
1021
672
|
TableName: string;
|
|
1022
673
|
Item: Record<string, unknown>;
|
|
@@ -1024,8 +675,6 @@ interface PutInput {
|
|
|
1024
675
|
ExpressionAttributeNames?: Record<string, string>;
|
|
1025
676
|
ExpressionAttributeValues?: Record<string, unknown>;
|
|
1026
677
|
}
|
|
1027
|
-
declare function buildPutInput(modelClass: Function, item: Record<string, unknown>, options?: PutOptions): PutInput;
|
|
1028
|
-
declare function executePut(modelClass: Function, item: Record<string, unknown>, options?: PutOptions): Promise<void>;
|
|
1029
678
|
|
|
1030
679
|
interface UpdateInput {
|
|
1031
680
|
TableName: string;
|
|
@@ -1035,8 +684,6 @@ interface UpdateInput {
|
|
|
1035
684
|
ExpressionAttributeValues?: Record<string, unknown>;
|
|
1036
685
|
ConditionExpression?: string;
|
|
1037
686
|
}
|
|
1038
|
-
declare function buildUpdateInput(modelClass: Function, entity: Record<string, unknown>, changes: Record<string, unknown>, options?: UpdateOptions): UpdateInput;
|
|
1039
|
-
declare function executeUpdate(modelClass: Function, entity: Record<string, unknown>, changes: Record<string, unknown>, options?: UpdateOptions): Promise<void>;
|
|
1040
687
|
|
|
1041
688
|
interface DeleteInput {
|
|
1042
689
|
TableName: string;
|
|
@@ -1045,8 +692,96 @@ interface DeleteInput {
|
|
|
1045
692
|
ExpressionAttributeNames?: Record<string, string>;
|
|
1046
693
|
ExpressionAttributeValues?: Record<string, unknown>;
|
|
1047
694
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* App-level throttle / transient-error retry for single-op sends (issue #111).
|
|
698
|
+
*
|
|
699
|
+
* GraphDDB's single-item ops (GetItem / Query / Put / Update / Delete) and the
|
|
700
|
+
* read fan-out (relation resolution) used to throw on throttling
|
|
701
|
+
* (`ProvisionedThroughputExceededException` / `ThrottlingException`) and rely
|
|
702
|
+
* solely on the AWS SDK v3 default retry, so the LIBRARY itself guaranteed no
|
|
703
|
+
* throttle-resilience. The batch ops already retry partial completion
|
|
704
|
+
* (`UnprocessedKeys` / `UnprocessedItems`, see `src/operations/batch-retry.ts`);
|
|
705
|
+
* this module is the SEPARATE, library-owned ERROR retry that wraps every
|
|
706
|
+
* `docClient.send()` for the single ops via {@link RetryingExecutor}.
|
|
707
|
+
*
|
|
708
|
+
* The two retry layers are DISTINCT and must not be merged:
|
|
709
|
+
* - (A) partial-batch retry (`batch-retry.ts`) — DynamoDB accepted the request
|
|
710
|
+
* but could not process every key/item; the request itself did NOT error.
|
|
711
|
+
* - (B) error retry (this module) — `send()` REJECTED with a throttle / transient
|
|
712
|
+
* error and is retried whole.
|
|
713
|
+
* They share {@link computeBackoffDelay} so backoff is consistent across both,
|
|
714
|
+
* but they are different mechanisms.
|
|
715
|
+
*
|
|
716
|
+
* The default policy is ENABLED out of the box (see {@link DEFAULT_RETRY_POLICY}):
|
|
717
|
+
* even with zero configuration a sensible exponential-backoff-with-jitter,
|
|
718
|
+
* bounded-attempts retry applies. Users tune it globally via
|
|
719
|
+
* `graphddb.config.retry(...)` or per-call via the `retry?:` option on the
|
|
720
|
+
* operation option bags; `retry: false` disables retry for that one call.
|
|
721
|
+
*
|
|
722
|
+
* NOTE on the AWS SDK's OWN retry: now that the library owns retry, a
|
|
723
|
+
* `DynamoDBClient` left at the SDK default `maxAttempts: 3` retries
|
|
724
|
+
* MULTIPLICATIVELY underneath this layer. Users should construct their client
|
|
725
|
+
* with `maxAttempts: 1` so the library is the single source of truth for retry
|
|
726
|
+
* (documented at `graphddb.config.client` and in the README). The library does NOT
|
|
727
|
+
* silently mutate a user-provided client.
|
|
728
|
+
*/
|
|
729
|
+
/** The single-op kind a retry is wrapping — surfaced to {@link RetryPolicy.onRetry}. */
|
|
730
|
+
type RetryOperationKind = 'get' | 'query' | 'batchGet' | 'put' | 'update' | 'delete' | 'batchWrite' | 'transactWrite';
|
|
731
|
+
/** The context handed to {@link RetryPolicy.onRetry} before each backoff sleep. */
|
|
732
|
+
interface RetryInfo {
|
|
733
|
+
/** 1-based attempt number that just FAILED and is about to be retried. */
|
|
734
|
+
readonly attempt: number;
|
|
735
|
+
/** The throttle / transient error that triggered this retry. */
|
|
736
|
+
readonly error: unknown;
|
|
737
|
+
/** The backoff delay (ms) about to be slept before the next attempt. */
|
|
738
|
+
readonly delayMs: number;
|
|
739
|
+
/** Which single-op kind is being retried. */
|
|
740
|
+
readonly operation: RetryOperationKind;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* A retry policy for the single-op error-retry layer (issue #111). Every field is
|
|
744
|
+
* optional and resolved INDEPENDENTLY, in layers (see {@link resolveRetryPolicy}):
|
|
745
|
+
* a per-call override field wins, else the globally-configured policy's field
|
|
746
|
+
* (from `graphddb.config.retry`), else
|
|
747
|
+
* {@link DEFAULT_RETRY_POLICY}. So a partial per-call override (e.g.
|
|
748
|
+
* `{ maxAttempts: 3 }`) overrides ONLY that field and INHERITS the rest from the
|
|
749
|
+
* global policy — a global `onRetry` / `jitter` is NOT dropped. A policy value of
|
|
750
|
+
* `false` on an option bag disables retry entirely for that call.
|
|
751
|
+
*/
|
|
752
|
+
interface RetryPolicy {
|
|
753
|
+
/**
|
|
754
|
+
* Maximum number of attempts (the FIRST try counts as attempt 1). `1` disables
|
|
755
|
+
* retry (a single try, no backoff). Default {@link DEFAULT_MAX_ATTEMPTS}.
|
|
756
|
+
*/
|
|
757
|
+
readonly maxAttempts?: number;
|
|
758
|
+
/**
|
|
759
|
+
* Compute the base backoff delay (ms) for a 1-based retry attempt, BEFORE jitter.
|
|
760
|
+
* Defaults to {@link computeBackoffDelay} (`min(1000, 50*2^(attempt-1))`), the
|
|
761
|
+
* same ramp the batch partial-retry layer uses, so backoff is consistent across
|
|
762
|
+
* the two layers.
|
|
763
|
+
*/
|
|
764
|
+
readonly computeDelay?: (attempt: number) => number;
|
|
765
|
+
/**
|
|
766
|
+
* Jitter strategy applied to the base delay:
|
|
767
|
+
* - `'full'` (default) — uniform random in `[0, base]` (AWS "full jitter"),
|
|
768
|
+
* the recommended strategy to de-correlate retries across clients.
|
|
769
|
+
* - `'none'` — use the base delay verbatim (deterministic; handy in tests).
|
|
770
|
+
*/
|
|
771
|
+
readonly jitter?: 'full' | 'none';
|
|
772
|
+
/**
|
|
773
|
+
* Classify whether an error is retryable. Defaults to {@link isRetryableError}
|
|
774
|
+
* (throttle + 5xx / transient). A custom classifier fully replaces the default —
|
|
775
|
+
* it should still avoid retrying validation / conditional-check failures.
|
|
776
|
+
*/
|
|
777
|
+
readonly isRetryable?: (error: unknown) => boolean;
|
|
778
|
+
/**
|
|
779
|
+
* Observability hook (issue #111 §8): invoked BEFORE each backoff sleep with the
|
|
780
|
+
* attempt number, the triggering error, the computed (post-jitter) delay, and the
|
|
781
|
+
* operation kind. Never invoked on the terminal success or the terminal failure.
|
|
782
|
+
*/
|
|
783
|
+
readonly onRetry?: (info: RetryInfo) => void;
|
|
784
|
+
}
|
|
1050
785
|
|
|
1051
786
|
/** Result of a read operation (GetItem / Query / BatchGetItem). */
|
|
1052
787
|
interface ExecutorResult {
|
|
@@ -1188,7 +923,7 @@ interface Executor {
|
|
|
1188
923
|
* registry / runtime machinery the read hooks (#138) introduced.
|
|
1189
924
|
*
|
|
1190
925
|
* A write {@link Middleware} hook is host-only, runtime-registered (via
|
|
1191
|
-
* `
|
|
926
|
+
* `graphddb.config.use`), and **never serialized** — like the read hooks it lives only on
|
|
1192
927
|
* the {@link import('../client/ClientManager.js').ClientManager} host-runtime
|
|
1193
928
|
* singleton and never touches the planner / spec generator / `operations.json`, so
|
|
1194
929
|
* the TS↔Python bridge (#48) is unaffected. It is **unrestricted by intent**: a
|
|
@@ -1341,7 +1076,7 @@ interface WriteMiddleware {
|
|
|
1341
1076
|
* module implements the **read** hook points R1–R5 and their context objects.
|
|
1342
1077
|
*
|
|
1343
1078
|
* A {@link Middleware} is a host-only, runtime-registered object (registered via
|
|
1344
|
-
* `
|
|
1079
|
+
* `graphddb.config.use`) that runs at fixed seams in the read pipeline. Hooks are
|
|
1345
1080
|
* **never serialized** — they live only on the {@link import('../client/ClientManager.js').ClientManager}
|
|
1346
1081
|
* host-runtime singleton and never touch the planner / spec generator /
|
|
1347
1082
|
* `operations.json`, so the TS↔Python bridge (#48) is unaffected. Hooks are
|
|
@@ -1525,7 +1260,7 @@ type ProjectionOf<T, Sel> = Sel extends SelectableOf<T> ? Sel : SelectOf<Sel>;
|
|
|
1525
1260
|
* usages keep compiling, while the precise inference is available when the
|
|
1526
1261
|
* constructor type is supplied (as it is from `DDBModel.asModel()`).
|
|
1527
1262
|
*/
|
|
1528
|
-
type AnyModelClass
|
|
1263
|
+
type AnyModelClass = abstract new (...args: any[]) => DDBModel;
|
|
1529
1264
|
/**
|
|
1530
1265
|
* The accepted PK / GSI key type for `list()` / `explain()`.
|
|
1531
1266
|
*
|
|
@@ -1535,24 +1270,24 @@ type AnyModelClass$1 = abstract new (...args: any[]) => DDBModel;
|
|
|
1535
1270
|
* {@link QueryKey}. Derived from the model class when available, otherwise
|
|
1536
1271
|
* permissive.
|
|
1537
1272
|
*/
|
|
1538
|
-
type ListKey<C> = [C] extends [AnyModelClass
|
|
1273
|
+
type ListKey<C> = [C] extends [AnyModelClass] ? PartialQueryKeyOf<C> : Record<string, unknown>;
|
|
1539
1274
|
/**
|
|
1540
1275
|
* The accepted key type for `query()` — only keys guaranteed to return a
|
|
1541
1276
|
* single item (PK + unique GSIs). Derived from the model class when available.
|
|
1542
1277
|
*/
|
|
1543
|
-
type QueryKey<C> = [C] extends [AnyModelClass
|
|
1278
|
+
type QueryKey<C> = [C] extends [AnyModelClass] ? UniqueQueryKeyOf<C> : Record<string, unknown>;
|
|
1544
1279
|
/**
|
|
1545
1280
|
* The accepted key type for `delete()` (PK only). Derived from the model
|
|
1546
1281
|
* class when available.
|
|
1547
1282
|
*/
|
|
1548
|
-
type DeleteKey<C> = [C] extends [AnyModelClass
|
|
1283
|
+
type DeleteKey<C> = [C] extends [AnyModelClass] ? PrimaryKeyOf<C> : Record<string, unknown>;
|
|
1549
1284
|
/**
|
|
1550
1285
|
* The accepted *explicit* key type for `update()` — the base-table primary key
|
|
1551
1286
|
* only. DynamoDB `UpdateItem` can target an item solely by its base-table PK
|
|
1552
1287
|
* (GSI-based updates are impossible), so this is `PrimaryKeyOf<C>`, not
|
|
1553
1288
|
* `UniqueQueryKeyOf<C>`. Derived from the model class when available.
|
|
1554
1289
|
*/
|
|
1555
|
-
type UpdateExplicitKey<C> = [C] extends [AnyModelClass
|
|
1290
|
+
type UpdateExplicitKey<C> = [C] extends [AnyModelClass] ? PrimaryKeyOf<C> : Record<string, unknown>;
|
|
1556
1291
|
/**
|
|
1557
1292
|
* Options accepted by `list()` (third argument). The projection is supplied as
|
|
1558
1293
|
* the separate second `select` argument, symmetric with `query()`.
|
|
@@ -1726,145 +1461,6 @@ interface ModelStatic<T extends DDBModel, C = unknown> {
|
|
|
1726
1461
|
deleteItem(key: DeleteKey<C>, options?: WriteOptions<T>): Promise<void>;
|
|
1727
1462
|
}
|
|
1728
1463
|
|
|
1729
|
-
type TransactWriteItemInput = {
|
|
1730
|
-
Put: PutInput;
|
|
1731
|
-
} | {
|
|
1732
|
-
Update: UpdateInput;
|
|
1733
|
-
} | {
|
|
1734
|
-
Delete: DeleteInput;
|
|
1735
|
-
} | {
|
|
1736
|
-
ConditionCheck: ConditionCheckInput;
|
|
1737
|
-
};
|
|
1738
|
-
declare function attachModelClass<T extends DDBModel>(modelStatic: ModelStatic<T>, modelClass: new (...args: unknown[]) => T): ModelStatic<T>;
|
|
1739
|
-
/**
|
|
1740
|
-
* Per-item capture descriptor recorded alongside each transact item, so the
|
|
1741
|
-
* write-capture seam (issue #72) can emit a labelled raw record after the
|
|
1742
|
-
* transaction commits. `TransactWriteItems` returns no images (spec §14), so
|
|
1743
|
-
* these carry the keys + the new item for Put/Update and keys only for Delete.
|
|
1744
|
-
*/
|
|
1745
|
-
interface TransactCaptureMeta {
|
|
1746
|
-
modelName?: string;
|
|
1747
|
-
op: 'put' | 'update' | 'delete';
|
|
1748
|
-
table: string;
|
|
1749
|
-
keys: {
|
|
1750
|
-
pk: string;
|
|
1751
|
-
sk: string;
|
|
1752
|
-
};
|
|
1753
|
-
newItem?: Record<string, unknown>;
|
|
1754
|
-
}
|
|
1755
|
-
/**
|
|
1756
|
-
* One recorded logical write op of an imperative {@link TransactionContext}
|
|
1757
|
-
* (issue #139). Captured at `tx.put/update/delete` call time so the write hooks
|
|
1758
|
-
* W1 (`write.before`) can run on each op — and rebuild its physical item from the
|
|
1759
|
-
* possibly-mutated input — before the batch composes / commits. A `conditionCheck`
|
|
1760
|
-
* records NO logical op (it is a read-only assertion, not a logical write).
|
|
1761
|
-
*/
|
|
1762
|
-
interface LogicalWriteOp {
|
|
1763
|
-
kind: WriteKind;
|
|
1764
|
-
modelClass: Function;
|
|
1765
|
-
item?: Record<string, unknown>;
|
|
1766
|
-
key?: Record<string, unknown>;
|
|
1767
|
-
changes?: Record<string, unknown>;
|
|
1768
|
-
options?: PutOptions & UpdateOptions & DeleteOptions;
|
|
1769
|
-
}
|
|
1770
|
-
declare class TransactionContext {
|
|
1771
|
-
private readonly items;
|
|
1772
|
-
private readonly captureMeta;
|
|
1773
|
-
/**
|
|
1774
|
-
* The logical write ops, parallel to {@link items} but EXCLUDING `conditionCheck`
|
|
1775
|
-
* entries (those are read-only assertions, recorded only in `items`). Used by the
|
|
1776
|
-
* write-hook path (#139) to run W1 on each logical op and recompose the batch.
|
|
1777
|
-
* A `null` entry marks an `items` slot that has no logical op (a ConditionCheck),
|
|
1778
|
-
* so the eager and logical arrays can be re-aligned at commit.
|
|
1779
|
-
*/
|
|
1780
|
-
private readonly logicalOps;
|
|
1781
|
-
get itemCount(): number;
|
|
1782
|
-
put(model: ModelStatic<DDBModel>, item: Record<string, unknown>, options?: PutOptions): void;
|
|
1783
|
-
update(model: ModelStatic<DDBModel>, entity: Record<string, unknown>, changes: Record<string, unknown>, options?: UpdateOptions): void;
|
|
1784
|
-
delete(model: ModelStatic<DDBModel>, key: Record<string, unknown>, options?: DeleteOptions): void;
|
|
1785
|
-
/**
|
|
1786
|
-
* Add a read-only `ConditionCheck` assertion on a keyed item (issue #81). The
|
|
1787
|
-
* item is **not** mutated; the `options.condition` (e.g.
|
|
1788
|
-
* `{ attributeExists: 'PK' }` to require the row exists) is asserted, and a
|
|
1789
|
-
* failed assertion cancels the **whole** `TransactWriteItems` atomically. This
|
|
1790
|
-
* is the foundation for referential-integrity derivation (proposal: `requires
|
|
1791
|
-
* <Entity> exists`). A ConditionCheck emits no change-capture record (it writes
|
|
1792
|
-
* nothing).
|
|
1793
|
-
*
|
|
1794
|
-
* @throws if `options.condition` is missing — a ConditionCheck without an
|
|
1795
|
-
* assertion is meaningless.
|
|
1796
|
-
*/
|
|
1797
|
-
conditionCheck(model: ModelStatic<DDBModel>, key: Record<string, unknown>, options: {
|
|
1798
|
-
condition: Record<string, unknown>;
|
|
1799
|
-
}): void;
|
|
1800
|
-
/** @internal */
|
|
1801
|
-
getTransactItems(): TransactWriteItemInput[];
|
|
1802
|
-
/** @internal — the recorded logical write ops (issue #139 W1), `null` per ConditionCheck slot. */
|
|
1803
|
-
getLogicalOps(): readonly (LogicalWriteOp | null)[];
|
|
1804
|
-
/** @internal — capture descriptors for the write-capture seam (issue #72). */
|
|
1805
|
-
getCaptureMeta(): TransactCaptureMeta[];
|
|
1806
|
-
private assertWithinLimit;
|
|
1807
|
-
}
|
|
1808
|
-
declare function executeTransaction(fn: (tx: TransactionContext) => void | Promise<void>, options?: {
|
|
1809
|
-
context?: RequestContext;
|
|
1810
|
-
}): Promise<void>;
|
|
1811
|
-
|
|
1812
|
-
interface BatchGetRequest {
|
|
1813
|
-
model: ModelStatic<DDBModel>;
|
|
1814
|
-
keys: Record<string, unknown>[];
|
|
1815
|
-
}
|
|
1816
|
-
/**
|
|
1817
|
-
* Per-call options for {@link DDBModel.batchGet} (issue #142).
|
|
1818
|
-
*/
|
|
1819
|
-
interface BatchGetOptions {
|
|
1820
|
-
/**
|
|
1821
|
-
* Host-injected per-call read context (issue #50 / #138 / #142) — exposed to
|
|
1822
|
-
* every read hook (R1/R4/R5 at the `batchGet` request level, R2/R3/R5 on each
|
|
1823
|
-
* underlying `BatchGetItem` op) as `ctx.context`, and threaded UNCHANGED to
|
|
1824
|
-
* every physical op so a global hook (e.g. a tenant-scope R2 FilterExpression
|
|
1825
|
-
* or an observability probe) applies across the whole multi-key batch read. A
|
|
1826
|
-
* host-language value, NEVER serialized into the SSoT / `operations.json` (the
|
|
1827
|
-
* bridge #48 is unaffected). Absent ⇒ `{}`.
|
|
1828
|
-
*/
|
|
1829
|
-
context?: RequestContext;
|
|
1830
|
-
}
|
|
1831
|
-
interface BatchPutRequest {
|
|
1832
|
-
type: 'put';
|
|
1833
|
-
model: ModelStatic<DDBModel>;
|
|
1834
|
-
item: Record<string, unknown>;
|
|
1835
|
-
options?: PutOptions;
|
|
1836
|
-
}
|
|
1837
|
-
interface BatchDeleteRequest {
|
|
1838
|
-
type: 'delete';
|
|
1839
|
-
model: ModelStatic<DDBModel>;
|
|
1840
|
-
key: Record<string, unknown>;
|
|
1841
|
-
options?: DeleteOptions;
|
|
1842
|
-
}
|
|
1843
|
-
type BatchWriteRequest = BatchPutRequest | BatchDeleteRequest;
|
|
1844
|
-
declare class BatchGetResult {
|
|
1845
|
-
private readonly groups;
|
|
1846
|
-
constructor(groups: Map<Function, Record<string, unknown>[]>);
|
|
1847
|
-
get<T extends DDBModel>(model: ModelStatic<T>): Partial<T>[];
|
|
1848
|
-
}
|
|
1849
|
-
/**
|
|
1850
|
-
* The multi-key, multi-model batch-read core (issue #142). Wraps the request
|
|
1851
|
-
* entry in the request-level read hooks R1 (`read.before`) / R4
|
|
1852
|
-
* (`read.afterFetch`) / R5 (`read.onError`) with request kind `'batchGet'`, and
|
|
1853
|
-
* each underlying physical `BatchGetItem` op (per table/chunk) in R2/R3/R5 via
|
|
1854
|
-
* {@link executeBatchGetForTable}. The read middleware runtime is built ONCE here
|
|
1855
|
-
* from `options.context` and threaded UNCHANGED to every op, so a global hook
|
|
1856
|
-
* (e.g. a tenant-scope R2 FilterExpression) applies across the whole batch.
|
|
1857
|
-
*
|
|
1858
|
-
* R1 sees the mutable batch `requests` on `ctx.params.requests`; the library
|
|
1859
|
-
* re-reads them after R1 runs, so a hook may add / drop / mutate requests before
|
|
1860
|
-
* any op is issued and may `throw` to cancel the whole batch. R4 may transform /
|
|
1861
|
-
* replace the assembled {@link BatchGetResult}; R5 may recover by returning one.
|
|
1862
|
-
* With no middleware registered the shared `NO_MIDDLEWARE` runtime keeps the hot
|
|
1863
|
-
* path unchanged (the zero-overhead fast path).
|
|
1864
|
-
*/
|
|
1865
|
-
declare function executeBatchGet(requests: BatchGetRequest[], options?: BatchGetOptions): Promise<BatchGetResult>;
|
|
1866
|
-
declare function executeBatchWrite(requests: BatchWriteRequest[]): Promise<void>;
|
|
1867
|
-
|
|
1868
1464
|
/**
|
|
1869
1465
|
* Intermediate representation (IR) produced by the parameterized definition DSL
|
|
1870
1466
|
* (issue #41). This is the **in-memory, typed** representation consumed by the
|
|
@@ -1952,6 +1548,8 @@ type ConditionInput<X = never> = {
|
|
|
1952
1548
|
readonly attributeExists: string;
|
|
1953
1549
|
} | {
|
|
1954
1550
|
readonly attributeNotExists: string;
|
|
1551
|
+
} | {
|
|
1552
|
+
readonly scpExpr: ExpressionSpec;
|
|
1955
1553
|
} | RawCondition | ConditionTree<X>;
|
|
1956
1554
|
/** A recursive declarative condition tree (field clauses + logical groups). */
|
|
1957
1555
|
interface ConditionTree<X = never> {
|
|
@@ -1960,26 +1558,6 @@ interface ConditionTree<X = never> {
|
|
|
1960
1558
|
readonly not?: ConditionInput<X>;
|
|
1961
1559
|
readonly [field: string]: ConditionLeaf<X> | ConditionOperatorObject<X> | readonly ConditionInput<X>[] | ConditionInput<X> | undefined;
|
|
1962
1560
|
}
|
|
1963
|
-
/** Options accepted by the write `define*` entry points (issue #46). */
|
|
1964
|
-
interface WriteDefinitionOptions {
|
|
1965
|
-
/** Optional declarative write condition (subset). */
|
|
1966
|
-
readonly condition?: ConditionInput;
|
|
1967
|
-
/**
|
|
1968
|
-
* Optional human-readable description of the command use case (issue #154). Pure
|
|
1969
|
-
* documentation — propagated to `operations.json` and the generated Python
|
|
1970
|
-
* repository-method docstring. Omit for unchanged output.
|
|
1971
|
-
*/
|
|
1972
|
-
readonly description?: string;
|
|
1973
|
-
}
|
|
1974
|
-
/** Options accepted by the read `define*` entry points (issue #154). */
|
|
1975
|
-
interface ReadDefinitionOptions {
|
|
1976
|
-
/**
|
|
1977
|
-
* Optional human-readable description of the query use case (issue #154). Pure
|
|
1978
|
-
* documentation — propagated to `operations.json` and the generated Python
|
|
1979
|
-
* repository-method docstring. Omit for unchanged output.
|
|
1980
|
-
*/
|
|
1981
|
-
readonly description?: string;
|
|
1982
|
-
}
|
|
1983
1561
|
/**
|
|
1984
1562
|
* Identifies the entity an operation targets. `name` is the model class name
|
|
1985
1563
|
* (entity name); `modelClass` is retained so the planner can resolve metadata
|
|
@@ -2025,8 +1603,8 @@ type ParamStructure = {
|
|
|
2025
1603
|
/**
|
|
2026
1604
|
* The typed IR for one definition. Generic over the entity, the parameterized
|
|
2027
1605
|
* structure(s), the (optional) select projection, and the collected params map
|
|
2028
|
-
* so that all type information survives into the
|
|
2029
|
-
* `
|
|
1606
|
+
* so that all type information survives into the operation IR the contract layer
|
|
1607
|
+
* (`publishQuery` / `publishCommand`) lowers to via `opToDefinition`.
|
|
2030
1608
|
*
|
|
2031
1609
|
* Field presence by `operation`:
|
|
2032
1610
|
*
|
|
@@ -2066,7 +1644,7 @@ interface OperationDefinition<T extends DDBModel, Op extends OperationKind, K, S
|
|
|
2066
1644
|
readonly condition?: ConditionInput;
|
|
2067
1645
|
/**
|
|
2068
1646
|
* Optional human-readable description of the definition (issue #154), supplied
|
|
2069
|
-
* via
|
|
1647
|
+
* via a contract method's `description` field. Pure
|
|
2070
1648
|
* documentation — propagated to the serialized {@link
|
|
2071
1649
|
* import('../spec/types.js').QuerySpec.description} /
|
|
2072
1650
|
* {@link import('../spec/types.js').CommandSpec.description} and to the generated
|
|
@@ -2080,62 +1658,7 @@ interface OperationDefinition<T extends DDBModel, Op extends OperationKind, K, S
|
|
|
2080
1658
|
}
|
|
2081
1659
|
/** Any operation definition node, regardless of its type parameters. */
|
|
2082
1660
|
type AnyOperationDefinition = OperationDefinition<any, OperationKind, unknown, unknown, unknown, Record<string, ParamDescriptor>>;
|
|
2083
|
-
/**
|
|
2084
|
-
* A map of definition name → {@link OperationDefinition}, as accepted by
|
|
2085
|
-
* `defineQueries` / `defineCommands` and returned (unchanged in type) from them.
|
|
2086
|
-
*/
|
|
2087
|
-
type DefinitionMap = Record<string, AnyOperationDefinition>;
|
|
2088
1661
|
|
|
2089
|
-
/**
|
|
2090
|
-
* Model write-semantics — the reusable *save contract* `entityWrites` (issue #83,
|
|
2091
|
-
* Epic #80; spec `docs/mutation-command-derivation.md` §2).
|
|
2092
|
-
*
|
|
2093
|
-
* `Model.writes` declares the **write-side invariants / effects required for this
|
|
2094
|
-
* entity to be consistent on DynamoDB** — a *reusable save contract*, NOT a
|
|
2095
|
-
* mutation and NOT business logic. It is a per-lifecycle map
|
|
2096
|
-
* (`{ create?, update?, remove? }`) whose values are {@link LifecycleContract}s
|
|
2097
|
-
* built with `w.lifecycle({...})`; each carries the §2 effect arrays
|
|
2098
|
-
* (`requires` / `unique` / `edges` / `derive` / `emits` / `idempotency`).
|
|
2099
|
-
*
|
|
2100
|
-
* ## What #83 does — and deliberately does NOT — with the effects
|
|
2101
|
-
*
|
|
2102
|
-
* The mutation compiler (#83) resolves a fragment's lifecycle from this save
|
|
2103
|
-
* contract (the fragment's intent — `m.create` / `m.update` / `m.remove` — picks
|
|
2104
|
-
* `create` / `update` / `remove`) and emits the **base write op** for that
|
|
2105
|
-
* lifecycle (create → `PutItem` with `attribute_not_exists(PK)`, update →
|
|
2106
|
-
* `UpdateItem`, remove → `DeleteItem`). The §2 effect arrays are **stored
|
|
2107
|
-
* opaquely** here and consumed by NONE of #83: deriving `ConditionCheck`
|
|
2108
|
-
* (requires, #84), uniqueness guards (#86), adjacency edge `Put` / `Delete` from
|
|
2109
|
-
* `edges` (#85), derived counter `UpdateItem`s (#85), outbox events (#87), and
|
|
2110
|
-
* the idempotency guard (#87) are each their own follow-up issue. #83 only shapes
|
|
2111
|
-
* the declaration and exposes the {@link LifecycleContract.effects} a later issue
|
|
2112
|
-
* reads — the **per-fragment hook** the compiler leaves empty (see
|
|
2113
|
-
* `compileFragment` in `src/spec/mutation-command.ts`).
|
|
2114
|
-
*
|
|
2115
|
-
* ## Coexistence with edge writes (`edgeWrites`, #82)
|
|
2116
|
-
*
|
|
2117
|
-
* This is a **distinct** construct from {@link edgeWrites} (#82, the adjacency
|
|
2118
|
-
* edge-only `writes` member). `edgeWrites` declares *only* the edge write side of
|
|
2119
|
-
* an adjacency entity; `entityWrites` is the full per-lifecycle save contract a
|
|
2120
|
-
* mutation fragment adopts. Both are recognized by their own brand, so a model
|
|
2121
|
-
* may carry either form on its static `writes` member without collision; the
|
|
2122
|
-
* mutation compiler reads {@link getEntityWrites} (this marker), the edge
|
|
2123
|
-
* derivation reads its own.
|
|
2124
|
-
*
|
|
2125
|
-
* ## Trivial base op when a model declares no `writes`
|
|
2126
|
-
*
|
|
2127
|
-
* A target model that declares **no** `entityWrites` save contract has no
|
|
2128
|
-
* lifecycle to resolve; the fragment then compiles the **trivial base op** for
|
|
2129
|
-
* its intent (create → `Put` `attribute_not_exists(PK)`, update → `Update`,
|
|
2130
|
-
* remove → `Delete`) directly against the plain model. So
|
|
2131
|
-
* `{ create: () => PostModel, key, input }` works on a bare model — see
|
|
2132
|
-
* {@link resolveLifecycle} in `src/spec/mutation-command.ts`.
|
|
2133
|
-
*/
|
|
2134
|
-
/**
|
|
2135
|
-
* The lifecycle phase a save contract entry declares. The mutation fragment's
|
|
2136
|
-
* intent (`m.create` / `m.update` / `m.remove`) selects the matching entry.
|
|
2137
|
-
*/
|
|
2138
|
-
type WriteLifecyclePhase = 'create' | 'update' | 'remove';
|
|
2139
1662
|
/**
|
|
2140
1663
|
* A path-rooted leaf value in a save-contract effect (proposal §2): every value
|
|
2141
1664
|
* binds to an explicit path root so its source is unambiguous —
|
|
@@ -2255,18 +1778,6 @@ declare const MAINTAIN_TRIGGER_BRAND: unique symbol;
|
|
|
2255
1778
|
type MaintainTrigger = `${string}.${MaintainEvent}` & {
|
|
2256
1779
|
readonly [MAINTAIN_TRIGGER_BRAND]: true;
|
|
2257
1780
|
};
|
|
2258
|
-
/**
|
|
2259
|
-
* Construct a validated {@link MaintainTrigger} from an `Entity.event` string.
|
|
2260
|
-
* Rejects a value that is not a non-empty entity name followed by `.created` /
|
|
2261
|
-
* `.updated` / `.removed`, so an invalid trigger is refused at construction time
|
|
2262
|
-
* (the AC's "invalid trigger string is rejected"). The brand is a compile-time
|
|
2263
|
-
* marker only; the returned value is the input string verbatim.
|
|
2264
|
-
*
|
|
2265
|
-
* @throws if `value` is not a well-formed `Entity.event` trigger string.
|
|
2266
|
-
*/
|
|
2267
|
-
declare function maintainTrigger(value: string): MaintainTrigger;
|
|
2268
|
-
/** Runtime guard: is `value` a well-formed {@link MaintainTrigger} string? */
|
|
2269
|
-
declare function isMaintainTrigger(value: unknown): value is MaintainTrigger;
|
|
2270
1781
|
/**
|
|
2271
1782
|
* A projection transform op (issue #120, 論点3 = 関数形): the function-form
|
|
2272
1783
|
* `w.transform(path, op, ...args)` is the **primary** authoring surface (there is
|
|
@@ -2631,8 +2142,6 @@ interface LifecycleContract {
|
|
|
2631
2142
|
/** The §2 effect arrays, stored opaquely by #83. */
|
|
2632
2143
|
readonly effects: LifecycleEffects;
|
|
2633
2144
|
}
|
|
2634
|
-
/** Runtime guard: is `value` a {@link LifecycleContract} (a `w.lifecycle(...)`)? */
|
|
2635
|
-
declare function isLifecycleContract(value: unknown): value is LifecycleContract;
|
|
2636
2145
|
/**
|
|
2637
2146
|
* Marker carried by a model's static `writes` member when it is an
|
|
2638
2147
|
* {@link EntityWritesDefinition} (an `entityWrites(...)` save contract), so it is
|
|
@@ -2655,8 +2164,6 @@ interface EntityWritesDefinition {
|
|
|
2655
2164
|
/** The remove-lifecycle save contract, if declared. */
|
|
2656
2165
|
readonly remove?: LifecycleContract;
|
|
2657
2166
|
}
|
|
2658
|
-
/** Runtime guard: is `value` an {@link EntityWritesDefinition}? */
|
|
2659
|
-
declare function isEntityWritesDefinition(value: unknown): value is EntityWritesDefinition;
|
|
2660
2167
|
/**
|
|
2661
2168
|
* The recorder handed to the {@link entityWrites} builder. `w.lifecycle({...})`
|
|
2662
2169
|
* accepts the §2 effect arrays and brands them into a {@link LifecycleContract}.
|
|
@@ -2783,8 +2290,6 @@ declare function entityWrites<M = unknown>(builder: (w: WriteRecorder) => Entity
|
|
|
2783
2290
|
* collision (a model carries one or the other; the marker disambiguates).
|
|
2784
2291
|
*/
|
|
2785
2292
|
declare function getEntityWrites(modelClass: new (...args: unknown[]) => unknown): EntityWritesDefinition | undefined;
|
|
2786
|
-
/** The lifecycle phase a mutation fragment intent selects. */
|
|
2787
|
-
declare function lifecyclePhaseForIntent(intent: 'create' | 'update' | 'remove'): WriteLifecyclePhase;
|
|
2788
2293
|
|
|
2789
2294
|
/**
|
|
2790
2295
|
* Internal write-plan composition DSL — `mutation` / `definePlan` (issue #83,
|
|
@@ -2793,7 +2298,7 @@ declare function lifecyclePhaseForIntent(intent: 'create' | 'update' | 'remove')
|
|
|
2793
2298
|
* A **mutation is INTERNAL** — it is *not* a public API. It is a *write-plan
|
|
2794
2299
|
* composition language* used as the **implementation** of a Command: it declares
|
|
2795
2300
|
* a set of write **fragments** that must execute atomically. The only externally
|
|
2796
|
-
* exposed surface is the fixed Command IF (`
|
|
2301
|
+
* exposed surface is the fixed Command IF (`publishCommand(...).plan(...)`,
|
|
2797
2302
|
* `src/define/contract.ts`); the mutation document is never exposed.
|
|
2798
2303
|
*
|
|
2799
2304
|
* The authoring form (issue #108) is a **descriptor map**: the body returns an
|
|
@@ -2844,11 +2349,18 @@ declare function lifecyclePhaseForIntent(intent: 'create' | 'update' | 'remove')
|
|
|
2844
2349
|
|
|
2845
2350
|
/**
|
|
2846
2351
|
* The intent of a write fragment (proposal §3): `create` / `update` / `remove`
|
|
2847
|
-
* (`remove`, **not** `del`). The intent selects the lifecycle from the
|
|
2848
|
-
* save contract, and the base write op (create → `PutItem`, update →
|
|
2849
|
-
* `UpdateItem`, remove → `DeleteItem`).
|
|
2850
|
-
|
|
2851
|
-
|
|
2352
|
+
* / `upsert` (`remove`, **not** `del`). The intent selects the lifecycle from the
|
|
2353
|
+
* target's save contract, and the base write op (create → `PutItem`, update →
|
|
2354
|
+
* `UpdateItem`, remove → `DeleteItem`, upsert → `PutItem`).
|
|
2355
|
+
*
|
|
2356
|
+
* `upsert` is a `PutItem` with **no** `attribute_not_exists(PK)` guard — an
|
|
2357
|
+
* unconditional overwrite (the semantics of the removed `definePut`). It shares
|
|
2358
|
+
* `create`'s base op (`put`) and lifecycle phase (a Put's derived effects are the
|
|
2359
|
+
* same), but the trivial base op carries no existence guard: an author-supplied
|
|
2360
|
+
* `condition` is honored verbatim (not ANDed with a `notExists`). That absence of
|
|
2361
|
+
* the guard is exactly what distinguishes an `upsert` from a `create`.
|
|
2362
|
+
*/
|
|
2363
|
+
type MutationIntent = 'create' | 'update' | 'remove' | 'upsert';
|
|
2852
2364
|
declare const INPUT_REF_BRAND: unique symbol;
|
|
2853
2365
|
/**
|
|
2854
2366
|
* A faithful reference to a mutation **input** field, minted by reading `$.<field>`
|
|
@@ -2865,8 +2377,6 @@ interface MutationInputRef {
|
|
|
2865
2377
|
/** The template token this reference renders to, e.g. `{title}`. */
|
|
2866
2378
|
readonly token: string;
|
|
2867
2379
|
}
|
|
2868
|
-
/** Runtime guard: is `value` a {@link MutationInputRef} (a `$.<field>` read)? */
|
|
2869
|
-
declare function isMutationInputRef(value: unknown): value is MutationInputRef;
|
|
2870
2380
|
declare const ENTITY_REF_BRAND: unique symbol;
|
|
2871
2381
|
/**
|
|
2872
2382
|
* A faithful **cross-fragment reference** (`$.entity[i].field`): the value a later
|
|
@@ -2888,25 +2398,6 @@ interface MutationEntityRef {
|
|
|
2888
2398
|
/** The source path, e.g. `$.entity[0].postId` (documentary; surfaces in errors). */
|
|
2889
2399
|
readonly path: string;
|
|
2890
2400
|
}
|
|
2891
|
-
/**
|
|
2892
|
-
* The placeholder proxy handed to a mutation body as `$`. In the descriptor-map
|
|
2893
|
-
* authoring form (issue #108) a top-level read `$.<x>` is **ambiguous** until used:
|
|
2894
|
-
*
|
|
2895
|
-
* - `$.role` placed at a `key` / `input` leaf is an **input-field reference**
|
|
2896
|
-
* (a {@link MutationInputRef}, token `{role}`) — bound from the command input;
|
|
2897
|
-
* - `$.membership.role` is a **cross-fragment reference** — the `role` field
|
|
2898
|
-
* written by the fragment named `membership` (an alias). It mints an
|
|
2899
|
-
* {@link AliasFieldRef}, which the {@link mutation} assembler rewrites into the
|
|
2900
|
-
* legacy `$.entity[<index>].<field>` string the compiler resolves (the alias →
|
|
2901
|
-
* declaration-order index is known once every alias key is seen).
|
|
2902
|
-
*
|
|
2903
|
-
* So each top-level read returns a value that is **both** a faithful
|
|
2904
|
-
* {@link MutationInputRef} (when used directly as a leaf) and supports one further
|
|
2905
|
-
* `.<field>` access (when used as an alias root). Any other access / coercion
|
|
2906
|
-
* throws — the binding stays declarative (only a direct `$.field` / `$.alias.field`
|
|
2907
|
-
* reference or a literal is representable, never a transform).
|
|
2908
|
-
*/
|
|
2909
|
-
type MutationInputProxy = Record<string, MutationInputRef>;
|
|
2910
2401
|
declare const FRAGMENT_BRAND: unique symbol;
|
|
2911
2402
|
/**
|
|
2912
2403
|
* The per-field input binding of a fragment: model field name → a faithful
|
|
@@ -2915,14 +2406,6 @@ declare const FRAGMENT_BRAND: unique symbol;
|
|
|
2915
2406
|
* field they key, never a transform.
|
|
2916
2407
|
*/
|
|
2917
2408
|
type FragmentInput = Readonly<Record<string, MutationInputRef | string | number | boolean | Date | null>>;
|
|
2918
|
-
/**
|
|
2919
|
-
* A descriptor `key` / `input` value as authored in the descriptor-map form
|
|
2920
|
-
* (issue #108): a `$.field` input reference, a `$.alias.field` cross-fragment
|
|
2921
|
-
* reference (an opaque alias-field ref the assembler rewrites), or a literal.
|
|
2922
|
-
* Structurally `unknown` at the leaf because the alias-field ref is internal; the
|
|
2923
|
-
* assembler validates every leaf via {@link assertFaithfulInput}.
|
|
2924
|
-
*/
|
|
2925
|
-
type DescriptorBinding = Readonly<Record<string, unknown>>;
|
|
2926
2409
|
/** A concrete scalar admissible at a condition operand leaf. */
|
|
2927
2410
|
type ConditionScalar = MutationInputRef | string | number | boolean | Date | null;
|
|
2928
2411
|
/** A declarative operator object on one condition field (the #114-A subset). */
|
|
@@ -2942,8 +2425,8 @@ interface FragmentConditionOperatorObject {
|
|
|
2942
2425
|
}
|
|
2943
2426
|
/**
|
|
2944
2427
|
* A declarative write **condition** authored on a {@link WriteDescriptor} (issue
|
|
2945
|
-
* #242, Phase 2) — the same #114-A subset the #101 public write descriptor
|
|
2946
|
-
*
|
|
2428
|
+
* #242, Phase 2) — the same #114-A subset the #101 public write descriptor
|
|
2429
|
+
* supports, but with {@link MutationInputRef} (`$.field`) leaves
|
|
2947
2430
|
* (the mutation authoring form's placeholder) rather than `param.*` / concrete
|
|
2948
2431
|
* values. It is a recursive tree of field clauses (bare equality or an operator
|
|
2949
2432
|
* object) and `and` / `or` / `not` logical groups. The compiler translates each
|
|
@@ -2996,8 +2479,6 @@ interface MutationFragment {
|
|
|
2996
2479
|
*/
|
|
2997
2480
|
readonly condition?: FragmentCondition;
|
|
2998
2481
|
}
|
|
2999
|
-
/** Runtime guard: is `value` a {@link MutationFragment}? */
|
|
3000
|
-
declare function isMutationFragment(value: unknown): value is MutationFragment;
|
|
3001
2482
|
/**
|
|
3002
2483
|
* A model reference in a write descriptor (issue #108): either the model class /
|
|
3003
2484
|
* `.asModel()` value **directly** (`{ create: GroupMembership }`) or a **thunk**
|
|
@@ -3007,60 +2488,6 @@ declare function isMutationFragment(value: unknown): value is MutationFragment;
|
|
|
3007
2488
|
* `m.create(() => Model, …)` recorder required.
|
|
3008
2489
|
*/
|
|
3009
2490
|
type ModelRef = ModelStatic<DDBModel> | (new (...args: unknown[]) => unknown) | (() => ModelStatic<DDBModel> | (new (...args: unknown[]) => unknown));
|
|
3010
|
-
/**
|
|
3011
|
-
* One **write descriptor** in the descriptor-map authoring form (issue #108) —
|
|
3012
|
-
* the declarative twin of an in-process `DDBModel.mutate` entry (#101). The intent
|
|
3013
|
-
* key (`create` / `update` / `remove`) is the **discriminator** and carries the
|
|
3014
|
-
* target {@link ModelRef} (direct or thunk). The remaining fields are declarative
|
|
3015
|
-
* bindings (`$.field` references or literals):
|
|
3016
|
-
*
|
|
3017
|
-
* - `key` — the target row's primary-key binding (identifies the row);
|
|
3018
|
-
* - `input` — the non-key field binding (the body / changed fields). Optional for
|
|
3019
|
-
* a `remove` (a delete writes no fields);
|
|
3020
|
-
* - `condition` — an optional declarative write gate (issue #242, Phase 2). It is
|
|
3021
|
-
* the #114-A condition subset (bare equality, operator objects `{ gt }` / `{ in }`
|
|
3022
|
-
* / `{ between }` / …, and `and` / `or` / `not` groups) whose operand leaves are
|
|
3023
|
-
* `$.field` input references or literals. The declaration-DSL compiler **consumes**
|
|
3024
|
-
* it: each `$.field` leaf becomes a {@link import('./contract.js').ContractParamRef}
|
|
3025
|
-
* and the tree is attached to the compiled base op's `condition`, so it is
|
|
3026
|
-
* serialized (into the operation spec) and evaluated at runtime exactly as a #101
|
|
3027
|
-
* public write descriptor's / `defineCommands` condition is (single-op, in-process
|
|
3028
|
-
* transaction, and the Python bridge). A conditioned `update` / `remove` therefore
|
|
3029
|
-
* fails a CAS-style write when the gate does not hold, rather than silently
|
|
3030
|
-
* committing;
|
|
3031
|
-
* - `result` — an optional read-back projection `{ select, options? }` (likewise
|
|
3032
|
-
* accepted for authoring parity; the public read-back projection of a
|
|
3033
|
-
* `command(...).plan(...)` method is still declared on `command({ select })`).
|
|
3034
|
-
*
|
|
3035
|
-
* Exactly one intent key must be present.
|
|
3036
|
-
*/
|
|
3037
|
-
interface WriteDescriptor {
|
|
3038
|
-
readonly create?: ModelRef;
|
|
3039
|
-
readonly update?: ModelRef;
|
|
3040
|
-
readonly remove?: ModelRef;
|
|
3041
|
-
/** The target row's primary-key binding (`{ field: $.field | literal }`). */
|
|
3042
|
-
readonly key: DescriptorBinding;
|
|
3043
|
-
/** The non-key field binding; optional (a `remove` writes no fields). */
|
|
3044
|
-
readonly input?: DescriptorBinding;
|
|
3045
|
-
/**
|
|
3046
|
-
* Optional declarative write gate — the #114-A condition subset (issue #242,
|
|
3047
|
-
* Phase 2). Compiled, serialized, and evaluated at runtime; see the interface doc.
|
|
3048
|
-
*/
|
|
3049
|
-
readonly condition?: FragmentCondition;
|
|
3050
|
-
/** Optional read-back projection `{ select, options? }` (accepted for #101 parity). */
|
|
3051
|
-
readonly result?: {
|
|
3052
|
-
readonly select?: unknown;
|
|
3053
|
-
readonly options?: unknown;
|
|
3054
|
-
};
|
|
3055
|
-
/**
|
|
3056
|
-
* Optional custom save contract to adopt (the **whole** {@link
|
|
3057
|
-
* EntityWritesDefinition}). Omit to default to the target model's own `writes`.
|
|
3058
|
-
* Per the `use:` rule this is never a single lifecycle — the intent picks it.
|
|
3059
|
-
*/
|
|
3060
|
-
readonly use?: EntityWritesDefinition;
|
|
3061
|
-
}
|
|
3062
|
-
/** The descriptor map a {@link mutation} body returns: alias → {@link WriteDescriptor}. */
|
|
3063
|
-
type MutationDescriptorMap = Record<string, WriteDescriptor>;
|
|
3064
2491
|
declare const COMMAND_PLAN_BRAND: unique symbol;
|
|
3065
2492
|
/**
|
|
3066
2493
|
* The recorded **CommandPlan** — a mutation's IR (proposal §3). It carries the
|
|
@@ -3076,54 +2503,187 @@ interface CommandPlan {
|
|
|
3076
2503
|
/** The declared write fragments, in declaration order (the mutation IR). */
|
|
3077
2504
|
readonly fragments: readonly MutationFragment[];
|
|
3078
2505
|
}
|
|
3079
|
-
|
|
3080
|
-
declare function isCommandPlan(value: unknown): value is CommandPlan;
|
|
2506
|
+
|
|
3081
2507
|
/**
|
|
3082
|
-
*
|
|
3083
|
-
*
|
|
3084
|
-
*
|
|
3085
|
-
*
|
|
3086
|
-
*
|
|
2508
|
+
* Declarative transaction definition DSL (issue #46, Python-bridge Phase 4).
|
|
2509
|
+
*
|
|
2510
|
+
* `defineTransaction(params, (tx, p) => { … })` captures a **single source of
|
|
2511
|
+
* truth** for a `TransactWriteItems` batch: a parameter map plus a declarative
|
|
2512
|
+
* instruction list (`tx.put` / `tx.update` / `tx.delete`, plus `tx.forEach` over
|
|
2513
|
+
* an array param). The callback is evaluated at definition time with sentinel
|
|
2514
|
+
* proxies in place of params / loop elements, so the recorded instructions carry
|
|
2515
|
+
* **field-reference templates** (`{groupId}`, `{item.userId}`) rather than
|
|
2516
|
+
* concrete values — exactly the shape the static planner (#42) turns into a
|
|
2517
|
+
* serializable `transaction` op the TS and Python runtimes expand identically.
|
|
2518
|
+
*
|
|
2519
|
+
* ## Declarativity boundary (enforced — same soundness as #42 keys)
|
|
2520
|
+
*
|
|
2521
|
+
* The callback body may only:
|
|
2522
|
+
* - call `tx.put/update/delete(Model, …)` with **field references** (from `p`
|
|
2523
|
+
* or a `forEach` element) or **concrete literals** at scalar leaves;
|
|
2524
|
+
* - call `tx.forEach(p.<arrayParam>, (el) => { … }, { when? })` whose body is
|
|
2525
|
+
* itself only `tx.*` writes;
|
|
2526
|
+
* - attach a declarative `when` (a small comparison over field refs) and/or a
|
|
2527
|
+
* `condition` (the `{ notExists } | equality` subset, as for single writes).
|
|
2528
|
+
*
|
|
2529
|
+
* The same hardening that protects **key** positions (#42 `src/spec/symbolic.ts`)
|
|
2530
|
+
* is applied to **value** positions (item / changes / condition values, `when`
|
|
2531
|
+
* right-hand side). Value positions are in fact held to a *stricter* boundary
|
|
2532
|
+
* than keys: a key may legitimately interpolate a field (`` `USER#${id}` ``), so
|
|
2533
|
+
* coercion is allowed there and the differential template check is the boundary;
|
|
2534
|
+
* a value leaf may **only** be a *direct* field reference or a concrete literal,
|
|
2535
|
+
* so at a value position a faithful reference is **never coerced**. That single
|
|
2536
|
+
* fact is the soundness boundary:
|
|
2537
|
+
*
|
|
2538
|
+
* 1. **Throwing sentinel Proxy.** Each param / element placeholder is a Proxy
|
|
2539
|
+
* whose `get` trap whitelists only primitive coercion
|
|
2540
|
+
* (`Symbol.toPrimitive` / `toString` / `valueOf`) and the brand / `token` /
|
|
2541
|
+
* `origin` reads the planner needs, and **throws on every other property or
|
|
2542
|
+
* method access**. So a *direct* transform on a value placeholder —
|
|
2543
|
+
* `p.role.length`, `p.role[0]`, `p.role.toUpperCase()` — fails at the access
|
|
2544
|
+
* site, exactly like a key sentinel. (Same first-line defense as #42.)
|
|
2545
|
+
* 2. **Per-access coercion ledger (primary boundary).** Every minted field ref
|
|
2546
|
+
* records each time it is coerced to a primitive (`Symbol.toPrimitive` /
|
|
2547
|
+
* `toString` / `valueOf`). Because a faithful value reference is stored as
|
|
2548
|
+
* the Proxy *object* and never coerced, **any** coercion of a value ref is
|
|
2549
|
+
* positive proof the callback consumed the parameter into a transform, a
|
|
2550
|
+
* branch, an interpolation, or a comparison — and the build is rejected. This
|
|
2551
|
+
* is *access-site* accurate: it fires even when the same field is *also*
|
|
2552
|
+
* referenced faithfully elsewhere in the item (so its token still surfaces),
|
|
2553
|
+
* and it catches transforms that **converge to a constant or a shared marker
|
|
2554
|
+
* fragment** — `String(p.role).slice(0,0)` → `''`, `.slice(0,3)` → a shared
|
|
2555
|
+
* prefix, `[0]` / `.charAt(0)`, `.includes('Tx')` → a constant boolean, any
|
|
2556
|
+
* `… + 'CONST'` built atop them — which a purely output-comparing check could
|
|
2557
|
+
* not (the dropped field is masked by its faithful sibling, and the converged
|
|
2558
|
+
* value is byte-identical across passes).
|
|
2559
|
+
* 3. **Differential evaluation (secondary).** The callback is evaluated **twice**,
|
|
2560
|
+
* each run delivering placeholders that coerce to a *different* per-field
|
|
2561
|
+
* marker (the two markers share no common prefix and no common substring of
|
|
2562
|
+
* length ≥ 2, and differ in length). Every recorded value leaf must be either
|
|
2563
|
+
* (a) a faithful field reference (the Proxy object survived intact in *both*
|
|
2564
|
+
* runs) or (b) a concrete literal byte-identical across both runs. This is a
|
|
2565
|
+
* defense-in-depth layer behind the coercion ledger.
|
|
2566
|
+
* 4. **Consumed-field check.** A placeholder field that the callback *accessed*
|
|
2567
|
+
* but that never reached the recorded output — e.g. `(p.role === 'admin') ?
|
|
2568
|
+
* 'A' : 'B'`, where `===` does **not** coerce (so the ledger does not fire),
|
|
2569
|
+
* the comparison is `false`, and only a literal is emitted — is rejected: the
|
|
2570
|
+
* param was silently dropped and a branch burned into the spec. (Same
|
|
2571
|
+
* consumed-field accounting #42 uses for `kind === 'admin' ? … : …`.)
|
|
2572
|
+
*
|
|
2573
|
+
* Arbitrary JS, reads, value branching, arithmetic, coercion, or string
|
|
2574
|
+
* transforms on a param / element placeholder are therefore rejected at build
|
|
2575
|
+
* time, in **both** key and value positions — **never silently emitted as a
|
|
2576
|
+
* wrong spec**. Unlike key positions, value positions have **no residual class**:
|
|
2577
|
+
* because a faithful value ref is never coerced, even an identity-shaped
|
|
2578
|
+
* transform (`String(p.x).slice(0)`, `.normalize()`, `.trim()`, `.replace('§',…)`
|
|
2579
|
+
* on an absent substring) is rejected (it coerced the ref). The only operations
|
|
2580
|
+
* that *do not* coerce a ref are (a) storing it directly and (b) the brand read;
|
|
2581
|
+
* (a) is the supported faithful reference and (b) is internal — so the ledger has
|
|
2582
|
+
* no false positives and no escape.
|
|
3087
2583
|
*/
|
|
3088
|
-
|
|
2584
|
+
|
|
2585
|
+
declare const TX_REF_BRAND: unique symbol;
|
|
3089
2586
|
/**
|
|
3090
|
-
*
|
|
3091
|
-
*
|
|
3092
|
-
*
|
|
3093
|
-
* `remove`: a model or a `() => Model` thunk) and binds `key` / `input` with `$.field`
|
|
3094
|
-
* placeholders. Insertion order is execution order, and a later descriptor may read
|
|
3095
|
-
* an earlier one's written field via `$.alias.field`.
|
|
3096
|
-
*
|
|
3097
|
-
* ```ts
|
|
3098
|
-
* const JoinGroup = mutation($ => ({
|
|
3099
|
-
* membership: { create: GroupMembership, key: { groupId: $.groupId, userId: $.userId }, input: { role: $.role } },
|
|
3100
|
-
* permission: { create: Permission, key: { id: $.permissionId }, input: { groupId: $.membership.groupId } },
|
|
3101
|
-
* }));
|
|
3102
|
-
* ```
|
|
3103
|
-
*
|
|
3104
|
-
* A leading **name** is optional (it surfaces only in compiler error messages — the
|
|
3105
|
-
* alias map already names each fragment): `mutation('JoinGroup', $ => ({ … }))`.
|
|
3106
|
-
*
|
|
3107
|
-
* The result is a branded {@link CommandPlan} (the mutation IR) — *not* public; it
|
|
3108
|
-
* implements a Command IF via `publicCommandModel(...).plan(mutation)`. The IR shape
|
|
3109
|
-
* (`{ name, fragments }`) is unchanged from the legacy recorder form, so the compiler,
|
|
3110
|
-
* serializer, and every runtime consume it identically.
|
|
3111
|
-
*
|
|
3112
|
-
* @throws if the body returns a non-object / empty map, a descriptor has no (or
|
|
3113
|
-
* multiple) intent key(s), a binding is non-declarative, or a `$.alias.field`
|
|
3114
|
-
* reference names an unknown alias.
|
|
2587
|
+
* A captured reference to a parameter (`{name}`) or a `forEach` element field
|
|
2588
|
+
* (`{item.<field>}`). Branded so the planner can distinguish a reference from a
|
|
2589
|
+
* concrete literal, and so a stray reference cannot be silently coerced.
|
|
3115
2590
|
*/
|
|
3116
|
-
|
|
3117
|
-
|
|
2591
|
+
interface TransactionRef {
|
|
2592
|
+
readonly [TX_REF_BRAND]: true;
|
|
2593
|
+
/** The template token this reference renders to, e.g. `{groupId}`. */
|
|
2594
|
+
readonly token: string;
|
|
2595
|
+
/** Human-readable origin (for error messages), e.g. `param 'groupId'`. */
|
|
2596
|
+
readonly origin: string;
|
|
2597
|
+
}
|
|
2598
|
+
/** A declarative comparison used by `when` (the only branching allowed). */
|
|
2599
|
+
type WhenComparison = {
|
|
2600
|
+
readonly op: 'eq';
|
|
2601
|
+
readonly left: TransactionRef;
|
|
2602
|
+
readonly right: unknown;
|
|
2603
|
+
} | {
|
|
2604
|
+
readonly op: 'ne';
|
|
2605
|
+
readonly left: TransactionRef;
|
|
2606
|
+
readonly right: unknown;
|
|
2607
|
+
};
|
|
3118
2608
|
/**
|
|
3119
|
-
*
|
|
3120
|
-
* `
|
|
3121
|
-
*
|
|
2609
|
+
* A single captured instruction inside a transaction. The three write operations
|
|
2610
|
+
* (`put` / `update` / `delete`) mutate an item; `conditionCheck` (issue #81) is a
|
|
2611
|
+
* **read-only assertion** on another keyed item — it carries a `key` and a
|
|
2612
|
+
* **required** `condition` and never mutates, but its failure rolls the whole
|
|
2613
|
+
* transaction back. It is the foundation for referential-integrity derivation
|
|
2614
|
+
* (`requires <Entity> exists` → a `conditionCheck` with `attributeExists`).
|
|
3122
2615
|
*/
|
|
3123
|
-
|
|
2616
|
+
interface TxWriteInstruction {
|
|
2617
|
+
readonly kind: 'write';
|
|
2618
|
+
readonly operation: 'put' | 'update' | 'delete' | 'conditionCheck';
|
|
2619
|
+
readonly entity: EntityRef;
|
|
2620
|
+
/** Put: the item structure (field → ref / literal). */
|
|
2621
|
+
readonly item?: Record<string, unknown>;
|
|
2622
|
+
/** Update / delete / conditionCheck: the key structure. */
|
|
2623
|
+
readonly key?: Record<string, unknown>;
|
|
2624
|
+
/** Update: the changes structure. */
|
|
2625
|
+
readonly changes?: Record<string, unknown>;
|
|
2626
|
+
/**
|
|
2627
|
+
* The write / assertion condition (subset). Optional on `put` / `update` /
|
|
2628
|
+
* `delete`; **required** on a `conditionCheck` (the assertion it makes).
|
|
2629
|
+
*
|
|
2630
|
+
* A tx condition operand may be a `p.*` field reference ({@link
|
|
2631
|
+
* TransactionRef}) — the serializer renders it to a `{ $param }` marker — so
|
|
2632
|
+
* the recorded leaf type is widened with `TransactionRef` in the tx context
|
|
2633
|
+
* (#246), matching what the recorder captures from {@link TxWriteOptions}.
|
|
2634
|
+
*/
|
|
2635
|
+
readonly condition?: ConditionInput<TransactionRef>;
|
|
2636
|
+
/** Optional declarative guard (skip this item when it does not hold). */
|
|
2637
|
+
readonly when?: WhenComparison;
|
|
2638
|
+
/**
|
|
2639
|
+
* Optional SCP expression guard (issue #261, Phase 3 S1): a behavior-contracts
|
|
2640
|
+
* Expression IR `{ exprVersion: 1, expr }` envelope, carried beside the legacy
|
|
2641
|
+
* `when`. Validated + canonicalized by the spec serializer; a bundle carrying
|
|
2642
|
+
* one is stamped spec version 1.2 and loud-rejected by runtimes whose
|
|
2643
|
+
* expression evaluation has not landed yet (fail-closed).
|
|
2644
|
+
*/
|
|
2645
|
+
readonly guard?: ExpressionSpec;
|
|
2646
|
+
}
|
|
2647
|
+
/** A `forEach` block expanding `body` once per element of an array param. */
|
|
2648
|
+
interface TxForEachInstruction {
|
|
2649
|
+
readonly kind: 'forEach';
|
|
2650
|
+
/** The array-param name iterated. */
|
|
2651
|
+
readonly source: string;
|
|
2652
|
+
/** Optional per-element declarative guard. */
|
|
2653
|
+
readonly when?: WhenComparison;
|
|
2654
|
+
/** The (write-only) instructions emitted per element. */
|
|
2655
|
+
readonly body: TxWriteInstruction[];
|
|
2656
|
+
}
|
|
2657
|
+
type TxInstruction = TxWriteInstruction | TxForEachInstruction;
|
|
2658
|
+
/** A small comparison helper: `when.eq(ref, value)` / `when.ne(ref, value)`. */
|
|
2659
|
+
declare const when: {
|
|
2660
|
+
readonly eq: (left: TransactionRef, right: unknown) => WhenComparison;
|
|
2661
|
+
readonly ne: (left: TransactionRef, right: unknown) => WhenComparison;
|
|
2662
|
+
};
|
|
2663
|
+
/** The typed IR node produced by {@link defineTransaction}. */
|
|
2664
|
+
interface TransactionDefinition {
|
|
2665
|
+
/** @internal Marks this object as a transaction definition IR node. */
|
|
2666
|
+
readonly __isTransactionDefinition: true;
|
|
2667
|
+
/** Collected parameters: name → descriptor (array params carry `element`). */
|
|
2668
|
+
readonly params: Readonly<Record<string, ParamDescriptor>>;
|
|
2669
|
+
/** The captured, declarative instruction list. */
|
|
2670
|
+
readonly instructions: readonly TxInstruction[];
|
|
2671
|
+
/**
|
|
2672
|
+
* The **atomicity mode** the author declared, sourced from the authoring call
|
|
2673
|
+
* — the `mutate(body, { mode })` alias records it here. **Never defaulted**
|
|
2674
|
+
* (Phase 4 S3.5): graphddb does not decide/default atomicity; whether a
|
|
2675
|
+
* command body is a single `TransactWriteItems` is up to the model definition
|
|
2676
|
+
* (whether its `mutate` asked for `{ mode: 'transaction' }`). `defineTransaction`
|
|
2677
|
+
* / `defineScpTransaction` leave it `undefined`; the `mutate` alias stamps it
|
|
2678
|
+
* from the authoring `{ mode }` option; `publishCommand`'s SCP-body branch reads
|
|
2679
|
+
* it to decide the wire `single` resolution (undefined ⇒ non-atomic, atomicity
|
|
2680
|
+
* being the sole home of `{ mode: 'transaction' }`).
|
|
2681
|
+
*/
|
|
2682
|
+
readonly mode?: MutateMode;
|
|
2683
|
+
}
|
|
3124
2684
|
|
|
3125
2685
|
/**
|
|
3126
|
-
* Contract DSL — `
|
|
2686
|
+
* Contract DSL — `publishQuery` / `publishCommand` (issue #58, CQRS
|
|
3127
2687
|
* Contract layer, Epic #57; spec `docs/cqrs-contract.md`).
|
|
3128
2688
|
*
|
|
3129
2689
|
* A **QueryModel / CommandModel** is the public, storage-independent interface
|
|
@@ -3132,15 +2692,15 @@ declare const definePlan: typeof mutation;
|
|
|
3132
2692
|
* `get` + `summary` over the same key, differing only in projection). The Key is
|
|
3133
2693
|
* the access pattern; the Method is the use case.
|
|
3134
2694
|
*
|
|
3135
|
-
* This module
|
|
3136
|
-
*
|
|
3137
|
-
*
|
|
3138
|
-
*
|
|
2695
|
+
* This module is the SOLE read/write authoring surface (#251: the define*
|
|
2696
|
+
* single-op DSL was removed). It builds on the shared operation IR
|
|
2697
|
+
* ({@link OperationDefinition}) that the spec builders (`buildQuerySpec` /
|
|
2698
|
+
* `buildCommandSpec`) consume. A contract method body resolves to a single
|
|
3139
2699
|
* declarative operation on the underlying model; the closure is **never**
|
|
3140
2700
|
* serialized — only the resolved declaration is captured into the IR.
|
|
3141
2701
|
*
|
|
3142
2702
|
* ```ts
|
|
3143
|
-
* const ArticleById =
|
|
2703
|
+
* const ArticleById = publishQuery<ArticleIdKey>()({
|
|
3144
2704
|
* get: (keys, params) =>
|
|
3145
2705
|
* Article.query(keys, { articleId: true, title: true, body: true }, params),
|
|
3146
2706
|
* });
|
|
@@ -3276,7 +2836,6 @@ type QueryMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params:
|
|
|
3276
2836
|
*/
|
|
3277
2837
|
type CommandMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
|
|
3278
2838
|
declare const KEY_REF_BRAND: unique symbol;
|
|
3279
|
-
declare const KEY_FIELD_REF_BRAND: unique symbol;
|
|
3280
2839
|
/**
|
|
3281
2840
|
* The captured `keys` argument of a contract method body. A branded sentinel that
|
|
3282
2841
|
* supports the two faithful uses the proposal's examples need:
|
|
@@ -3295,80 +2854,7 @@ declare const KEY_FIELD_REF_BRAND: unique symbol;
|
|
|
3295
2854
|
interface ContractKeyRef {
|
|
3296
2855
|
readonly [KEY_REF_BRAND]: true;
|
|
3297
2856
|
}
|
|
3298
|
-
/**
|
|
3299
|
-
* A captured reference to a single field of the method's `keys` argument
|
|
3300
|
-
* (`keys.<field>`), minted when a range body rebuilds a partition key. Branded so
|
|
3301
|
-
* the verifier can distinguish a faithful key-field reference from a literal, and
|
|
3302
|
-
* so it cannot be silently coerced.
|
|
3303
|
-
*/
|
|
3304
|
-
interface ContractKeyFieldRef {
|
|
3305
|
-
readonly [KEY_FIELD_REF_BRAND]: true;
|
|
3306
|
-
/** The key field name, e.g. `categoryId`. */
|
|
3307
|
-
readonly field: string;
|
|
3308
|
-
/** The template token this reference renders to, e.g. `{key.categoryId}`. */
|
|
3309
|
-
readonly token: string;
|
|
3310
|
-
}
|
|
3311
|
-
/** Runtime guard: is `value` a {@link ContractKeyRef} (the whole key sentinel)? */
|
|
3312
|
-
declare function isContractKeyRef(value: unknown): value is ContractKeyRef;
|
|
3313
|
-
/**
|
|
3314
|
-
* Mint a faithful {@link ContractKeyFieldRef} for a named key field — a plain
|
|
3315
|
-
* (non-Proxy) reference rendering the `{key.field}` differential token but, at a
|
|
3316
|
-
* **value** position (a `put` item field), serialized as `{field}` (see
|
|
3317
|
-
* `templateLeaf`, `src/spec/operations.ts`). Used by the **mutation compiler**
|
|
3318
|
-
* (#83) to mark which of a `create` fragment's item fields are the model's
|
|
3319
|
-
* primary-key (contract Key) fields, so the existing serializer recovers the
|
|
3320
|
-
* contract Key from the put item (`keyFieldsOf`) exactly as it does for a
|
|
3321
|
-
* hand-written #64 `put` that binds `keys.<field>` into the item. Mirrors
|
|
3322
|
-
* {@link mintContractParamRef}.
|
|
3323
|
-
*
|
|
3324
|
-
* @param field The key field name (rendered `{field}` at a value position).
|
|
3325
|
-
*/
|
|
3326
|
-
declare function mintContractKeyFieldRef(field: string): ContractKeyFieldRef;
|
|
3327
|
-
/**
|
|
3328
|
-
* The whole-`keys` sentinel value an `update` / `delete` op records in its key
|
|
3329
|
-
* slot when the body passed `keys` whole into the op's key position. Exposed so
|
|
3330
|
-
* the **mutation compiler** (#83) can build an equivalent planned `update` /
|
|
3331
|
-
* `delete` op without re-entering the hardening machinery (a mutation has no
|
|
3332
|
-
* `keys` argument — its key fields are bound from `$.input.*`, captured via
|
|
3333
|
-
* {@link ContractMethodOp.keyFields}).
|
|
3334
|
-
*/
|
|
3335
|
-
declare function wholeKeysSentinel(): ContractKeyRef;
|
|
3336
|
-
/** Runtime guard: is `value` a {@link ContractKeyFieldRef}? */
|
|
3337
|
-
declare function isContractKeyFieldRef(value: unknown): value is ContractKeyFieldRef;
|
|
3338
|
-
declare const PARAM_REF_BRAND$1: unique symbol;
|
|
3339
|
-
/**
|
|
3340
|
-
* A captured reference to a field of the method's `params` argument (`{<field>}`).
|
|
3341
|
-
* Branded so the recorder can distinguish a faithful reference from a concrete
|
|
3342
|
-
* literal, and so a stray reference cannot be silently coerced.
|
|
3343
|
-
*/
|
|
3344
|
-
interface ContractParamRef {
|
|
3345
|
-
readonly [PARAM_REF_BRAND$1]: true;
|
|
3346
|
-
/** The template token this reference renders to, e.g. `{limit}`. */
|
|
3347
|
-
readonly token: string;
|
|
3348
|
-
/** The field name on `params`, e.g. `limit`. */
|
|
3349
|
-
readonly field: string;
|
|
3350
|
-
}
|
|
3351
|
-
/** Runtime guard: is `value` a {@link ContractParamRef}? */
|
|
3352
|
-
declare function isContractParamRef(value: unknown): value is ContractParamRef;
|
|
3353
|
-
/**
|
|
3354
|
-
* Mint a faithful {@link ContractParamRef} for a named field — a concrete
|
|
3355
|
-
* (non-Proxy) reference rendering the `{field}` token. Unlike the hardening
|
|
3356
|
-
* sentinels {@link makeParamFieldRef} builds during a contract method body (which
|
|
3357
|
-
* throw on any transform to detect non-declarative use), this is a *plain* ref
|
|
3358
|
-
* used by the **mutation compiler** (#83, `src/spec/mutation-command.ts`): a
|
|
3359
|
-
* mutation fragment's `input` binding is captured as a {@link MutationInputRef}
|
|
3360
|
-
* (its own declarative sentinel), and the compiler translates each one into this
|
|
3361
|
-
* `ContractParamRef` so the **existing** contract serializer
|
|
3362
|
-
* ({@link opToDefinition} → {@link buildCommandSpec}) and TS runtime
|
|
3363
|
-
* ({@link renderLeaf}) consume it with **zero** new template / param machinery —
|
|
3364
|
-
* the param name and `{token}` are the input field name, exactly as a hand-written
|
|
3365
|
-
* `params.field` reference would render.
|
|
3366
|
-
*
|
|
3367
|
-
* @param field The param / input field name (the rendered token is `{field}`).
|
|
3368
|
-
*/
|
|
3369
|
-
declare function mintContractParamRef(field: string): ContractParamRef;
|
|
3370
2857
|
declare const FROM_REF_BRAND: unique symbol;
|
|
3371
|
-
declare const COMPOSE_NODE_BRAND: unique symbol;
|
|
3372
2858
|
/**
|
|
3373
2859
|
* A parent-result source path produced by {@link from} (proposal "External
|
|
3374
2860
|
* Query"). It names the field of the **parent** method's result that supplies a
|
|
@@ -3386,91 +2872,6 @@ interface ContractFromRef {
|
|
|
3386
2872
|
/** The `$`-rooted source path on the parent result, e.g. `$.billingAccountId`. */
|
|
3387
2873
|
readonly path: string;
|
|
3388
2874
|
}
|
|
3389
|
-
/** Runtime guard: is `value` a {@link ContractFromRef} (a `from(...)` binding)? */
|
|
3390
|
-
declare function isContractFromRef(value: unknown): value is ContractFromRef;
|
|
3391
|
-
/**
|
|
3392
|
-
* A resolved External Query composition node produced by {@link query} and placed
|
|
3393
|
-
* at a `select` property of a contract method body (proposal "External Query"):
|
|
3394
|
-
*
|
|
3395
|
-
* ```ts
|
|
3396
|
-
* Account.query(keys, {
|
|
3397
|
-
* accountId: true,
|
|
3398
|
-
* billing: query(BillingPlanQueries.get, { accountId: from("$.billingAccountId") }),
|
|
3399
|
-
* })
|
|
3400
|
-
* ```
|
|
3401
|
-
*
|
|
3402
|
-
* It carries a faithful reference to the **referenced method spec** (so the
|
|
3403
|
-
* serializer can recover the referenced contract's *name* by identity against the
|
|
3404
|
-
* contract map, and its derived `resolution` / `cardinality` facts) plus the
|
|
3405
|
-
* declarative child-key binding (child key field → a {@link ContractFromRef}).
|
|
3406
|
-
* The owning `select` key becomes the composition's `as` property at record time
|
|
3407
|
-
* (it is not known to {@link query} itself).
|
|
3408
|
-
*/
|
|
3409
|
-
interface ContractComposeNode {
|
|
3410
|
-
readonly [COMPOSE_NODE_BRAND]: true;
|
|
3411
|
-
/** The referenced query method spec (`OtherContract.method`). */
|
|
3412
|
-
readonly method: QueryMethodSpec<unknown, unknown, unknown>;
|
|
3413
|
-
/** The child-key binding: child key field → parent-result `from` path. */
|
|
3414
|
-
readonly bind: Readonly<Record<string, ContractFromRef>>;
|
|
3415
|
-
}
|
|
3416
|
-
/** Runtime guard: is `value` a {@link ContractComposeNode} (a `query(...)` node)? */
|
|
3417
|
-
declare function isContractComposeNode(value: unknown): value is ContractComposeNode;
|
|
3418
|
-
/**
|
|
3419
|
-
* Declare a parent-result source path for an External Query child key binding
|
|
3420
|
-
* (proposal "External Query"). Used **only** inside a {@link query} binding:
|
|
3421
|
-
*
|
|
3422
|
-
* ```ts
|
|
3423
|
-
* query(BillingPlanQueries.get, { accountId: from("$.billingAccountId") })
|
|
3424
|
-
* ```
|
|
3425
|
-
*
|
|
3426
|
-
* The path is `$`-rooted (the parent record) and dotted; the bound child-key
|
|
3427
|
-
* field is read from that path of each resolved parent record at execution time.
|
|
3428
|
-
* The binding is **declarative** — `from` accepts only a literal path string and
|
|
3429
|
-
* rejects any other shape, so no arbitrary JS can sneak into a key binding.
|
|
3430
|
-
*
|
|
3431
|
-
* @param path A `$`-rooted dotted path, e.g. `"$.billingAccountId"` or `"$.a.b"`.
|
|
3432
|
-
* @throws if `path` is not a non-empty `$`-rooted dotted field path.
|
|
3433
|
-
*/
|
|
3434
|
-
declare function from(path: string): ContractFromRef;
|
|
3435
|
-
/**
|
|
3436
|
-
* Reference another query contract's method as an **External Query** composition
|
|
3437
|
-
* child, bound to the parent result by a declarative `from` mapping (proposal
|
|
3438
|
-
* "Query Composition" / "External Query"). Place the result at a `select`
|
|
3439
|
-
* property of a contract method body; the property name becomes the composed
|
|
3440
|
-
* value's `as`:
|
|
3441
|
-
*
|
|
3442
|
-
* ```ts
|
|
3443
|
-
* export const AccountAccess = publicQueryModel<AccountKey>()({
|
|
3444
|
-
* get: (keys, params) => Account.query(keys, {
|
|
3445
|
-
* accountId: true,
|
|
3446
|
-
* billing: query(BillingPlanQueries.get, { accountId: from("$.billingAccountId") }),
|
|
3447
|
-
* }, params),
|
|
3448
|
-
* });
|
|
3449
|
-
* ```
|
|
3450
|
-
*
|
|
3451
|
-
* This is **build-time-resolved, in-process** contract chaining — the relation
|
|
3452
|
-
* primitive extended to point at another contract's method instead of a model
|
|
3453
|
-
* relation. There is no protocol / transport: the runtime collects every bound
|
|
3454
|
-
* key produced by the parent step and resolves the referenced contract **once,
|
|
3455
|
-
* batched** (proposal "External Query"). The child MUST be `point` (the parent
|
|
3456
|
-
* may yield N records, so a `range` child would be an N+1 fan-out) — that rule is
|
|
3457
|
-
* owned by the N+1 checker (#60), which rejects a `range` child at build time.
|
|
3458
|
-
*
|
|
3459
|
-
* @param method The referenced query method (`OtherContract.method`), a resolved
|
|
3460
|
-
* {@link QueryMethodSpec}.
|
|
3461
|
-
* @param bind The child-key binding: child key field → a {@link from} path on
|
|
3462
|
-
* the parent result. Must be non-empty and every value a `from(...)` ref.
|
|
3463
|
-
* @throws if `method` is not a query method spec, or `bind` is empty / carries a
|
|
3464
|
-
* non-`from` value.
|
|
3465
|
-
*/
|
|
3466
|
-
declare function query(method: QueryMethodSpec<any, any, any>, bind: Record<string, ContractFromRef>): ContractComposeNode;
|
|
3467
|
-
/**
|
|
3468
|
-
* The {@link QueryModelContract} that owns a resolved query method spec, or
|
|
3469
|
-
* `undefined` if the spec was not produced by {@link publicQueryModel} (so it has
|
|
3470
|
-
* no registered owner). Used by the serializer to resolve a composition's
|
|
3471
|
-
* referenced contract by identity.
|
|
3472
|
-
*/
|
|
3473
|
-
declare function contractOfMethodSpec(method: QueryMethodSpec<unknown, unknown, unknown>): QueryModelContract<unknown, Record<string, QueryMethodSpec<unknown, unknown, unknown>>> | undefined;
|
|
3474
2875
|
/**
|
|
3475
2876
|
* The declarative internal operation a contract method body resolves to — the
|
|
3476
2877
|
* resolved declaration captured into the IR (the closure itself is discarded).
|
|
@@ -3497,7 +2898,7 @@ interface ContractMethodOp<Op extends OperationKind = OperationKind> {
|
|
|
3497
2898
|
readonly keys: ContractKeyRef | Readonly<Record<string, unknown>>;
|
|
3498
2899
|
/**
|
|
3499
2900
|
* The **explicit contract Key field names** captured from the factory's
|
|
3500
|
-
* key-field argument (`
|
|
2901
|
+
* key-field argument (`publishQuery<EmailKey>(['email'])(...)`, issue #71).
|
|
3501
2902
|
* Present only for a **whole-`keys`** op (the {@link keys} slot is the whole
|
|
3502
2903
|
* {@link ContractKeyRef} sentinel) when the author supplied a field list, or
|
|
3503
2904
|
* when the model-derived factory form defaulted it to the model's primary-key
|
|
@@ -3509,7 +2910,7 @@ interface ContractMethodOp<Op extends OperationKind = OperationKind> {
|
|
|
3509
2910
|
*
|
|
3510
2911
|
* Absent when the body rebuilds a partition key from `keys.<field>` references
|
|
3511
2912
|
* (the range form already captures field names in the {@link keys} record) or
|
|
3512
|
-
* when no explicit list was given to a `
|
|
2913
|
+
* when no explicit list was given to a `publishQuery<TKey>()` whole-key form
|
|
3513
2914
|
* (the legacy backward-compatible path, which then defaults to the primary key).
|
|
3514
2915
|
*/
|
|
3515
2916
|
readonly keyFields?: readonly string[];
|
|
@@ -3536,6 +2937,15 @@ interface ContractMethodOp<Op extends OperationKind = OperationKind> {
|
|
|
3536
2937
|
* recorded operation, key, or runtime behaviour.
|
|
3537
2938
|
*/
|
|
3538
2939
|
readonly description?: string;
|
|
2940
|
+
/**
|
|
2941
|
+
* Optional per-param descriptions (issue #154), keyed by param / key-field name,
|
|
2942
|
+
* captured from a descriptor leaf's `param.*({ description })`. Pure documentation
|
|
2943
|
+
* — carried on the op so the serializer ({@link import('../spec/contracts.js')})
|
|
2944
|
+
* can seat each into the flattened definition's `ParamDescriptor.description`, so a
|
|
2945
|
+
* documented contract param surfaces in `operations.json` (`params.field.description`)
|
|
2946
|
+
* exactly as the removed define* param did. Never affects the recorded operation.
|
|
2947
|
+
*/
|
|
2948
|
+
readonly paramDescriptions?: Readonly<Record<string, string>>;
|
|
3539
2949
|
}
|
|
3540
2950
|
/**
|
|
3541
2951
|
* A recorded External Query composition edge on a contract method op (#63). The
|
|
@@ -3554,33 +2964,6 @@ interface RecordedCompose {
|
|
|
3554
2964
|
/** The child-key binding: child key field → parent-result `from` path. */
|
|
3555
2965
|
readonly bind: Readonly<Record<string, ContractFromRef>>;
|
|
3556
2966
|
}
|
|
3557
|
-
/**
|
|
3558
|
-
* The **call signature** a contract method exposes to a caller, narrowed by its
|
|
3559
|
-
* decided {@link InputArity} — the type-level half of N+1 rule (a) ("array into a
|
|
3560
|
-
* `range` method", #60). Given a method's `inputArity`:
|
|
3561
|
-
*
|
|
3562
|
-
* - `'single'` (a `range` method) — the key argument is a **single** `TKey` only;
|
|
3563
|
-
* passing an array (`readonly TKey[]`) is a **type error** by construction, so a
|
|
3564
|
-
* `range` method's array overload does not type-check.
|
|
3565
|
-
* - `'either'` (a `point` read / known-key write) — a single key **or** an array
|
|
3566
|
-
* (the array form coalesces to one `BatchGetItem` / batched write).
|
|
3567
|
-
* - `'array'` — an array only (reserved; not produced by the current resolvers).
|
|
3568
|
-
*
|
|
3569
|
-
* This mirrors the proposal's "the generated binding's types encode `inputArity`
|
|
3570
|
-
* (single → bare argument, array → array argument)". It is the type-level narrowing
|
|
3571
|
-
* used by the runtime call surface (`executeQueryMethod`, `src/runtime/contract-runtime.ts`):
|
|
3572
|
-
* the executor keys its key-argument type on the named method's **literal** arity
|
|
3573
|
-
* `A` (carried on {@link QueryMethodSpec} when the body is tagged with
|
|
3574
|
-
* {@link point} / {@link range}), so feeding an array into a `'single'` method is a
|
|
3575
|
-
* `tsc` compile error — the **primary** N+1 rule (a) layer, complementing the
|
|
3576
|
-
* build-time SSoT check ({@link assertContractN1Safe}) and the runtime backstop.
|
|
3577
|
-
*
|
|
3578
|
-
* @typeParam A - The decided input arity (a literal `InputArity`).
|
|
3579
|
-
* @typeParam TKey - The contract Key.
|
|
3580
|
-
* @typeParam TParams - The method's params type.
|
|
3581
|
-
* @typeParam TResult - The method's result type.
|
|
3582
|
-
*/
|
|
3583
|
-
type ContractCallSignature<A extends InputArity, TKey, TParams, TResult> = (key: A extends 'single' ? TKey : A extends 'array' ? readonly TKey[] : TKey | readonly TKey[], params: TParams) => TResult;
|
|
3584
2967
|
/**
|
|
3585
2968
|
* The resolved IR of one **query** method. Carries the formal method type at the
|
|
3586
2969
|
* type level (`QueryMethod<TKey, TParams, TResult>`) and, at runtime, the resolved
|
|
@@ -3638,8 +3021,29 @@ interface QueryMethodSpec<TKey, TParams, TResult, A extends InputArity = InputAr
|
|
|
3638
3021
|
interface CommandMethodSpec<TKey, TParams, TResult> {
|
|
3639
3022
|
/** @internal Marks this object as a command method spec. */
|
|
3640
3023
|
readonly __methodKind: 'command';
|
|
3641
|
-
/**
|
|
3642
|
-
|
|
3024
|
+
/**
|
|
3025
|
+
* The resolved internal write operation (closure discarded). Present on every
|
|
3026
|
+
* descriptor- / mutation-authored method (the entity the method is keyed by /
|
|
3027
|
+
* reads back from). **Absent** on an SCP-transaction-authored method (Phase 4
|
|
3028
|
+
* S1, issue #278): a native-syntax multi-write body has no single primary
|
|
3029
|
+
* entity, so it carries a pre-built {@link transaction} instead — the
|
|
3030
|
+
* serializer branches on `transaction` before ever reading `op`.
|
|
3031
|
+
*/
|
|
3032
|
+
readonly op?: ContractMethodOp;
|
|
3033
|
+
/**
|
|
3034
|
+
* The pre-built {@link TransactionSpec} of an **SCP-transaction-authored**
|
|
3035
|
+
* command method (Phase 4 S1, issue #278). When a `publishCommand` method
|
|
3036
|
+
* body is a {@link TransactionDefinition} (produced by `defineScpTransaction`
|
|
3037
|
+
* — whose native `$`/`?:`/`&&`/`.map` body the `transformScpTransactionSource`
|
|
3038
|
+
* pass has already lowered onto the recorder seams), it is compiled here by the
|
|
3039
|
+
* SAME {@link import('../spec/transaction.js').buildTransactionSpec} the
|
|
3040
|
+
* standalone `defineScpTransaction` path uses, so the emitted `TransactionSpec`
|
|
3041
|
+
* is **byte-identical** to the equivalent standalone transaction. Present only
|
|
3042
|
+
* for this authoring form; a descriptor / mutation method omits it (and carries
|
|
3043
|
+
* {@link op} instead). The atomicity **mode** flows through {@link mode}
|
|
3044
|
+
* (default `'transaction'`); the full verb/mode consolidation is Phase 4 S6.
|
|
3045
|
+
*/
|
|
3046
|
+
readonly transaction?: TransactionSpec;
|
|
3643
3047
|
/**
|
|
3644
3048
|
* Derived input arity: a single key maps to one write op; an array maps to a
|
|
3645
3049
|
* batched write (a transaction / `BatchWriteItem`). Writes accept `'either'`.
|
|
@@ -3821,10 +3225,6 @@ interface CommandModelContract<TKey, M extends Record<string, CommandMethodSpec<
|
|
|
3821
3225
|
/** @internal Phantom carrier retaining the contract Key type `TKey`. */
|
|
3822
3226
|
readonly __keyType?: (value: TKey) => void;
|
|
3823
3227
|
}
|
|
3824
|
-
/** Runtime guard: is `value` a {@link QueryModelContract}? */
|
|
3825
|
-
declare function isQueryModelContract(value: unknown): value is QueryModelContract<unknown, Record<string, QueryMethodSpec<unknown, unknown, unknown>>>;
|
|
3826
|
-
/** Runtime guard: is `value` a {@link CommandModelContract}? */
|
|
3827
|
-
declare function isCommandModelContract(value: unknown): value is CommandModelContract<unknown, Record<string, CommandMethodSpec<unknown, unknown, unknown>>>;
|
|
3828
3228
|
/** The entity type a descriptor {@link ModelRef} (model class / `.asModel()` / thunk) targets. */
|
|
3829
3229
|
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;
|
|
3830
3230
|
/** The projected item type for an entity `T` and a select `S` (relation-nest aware). */
|
|
@@ -3835,6 +3235,82 @@ type LeafValueOf<L> = L extends Param<infer V> ? V : L;
|
|
|
3835
3235
|
type KeyOf<KeyRec> = {
|
|
3836
3236
|
readonly [F in keyof KeyRec]: LeafValueOf<KeyRec[F]>;
|
|
3837
3237
|
};
|
|
3238
|
+
/** String-like scalar leaf types that accept a `Param<string>` placeholder (a Date /
|
|
3239
|
+
* binary field crosses the param boundary as a string — `param` has no `datetime`
|
|
3240
|
+
* builder, so it is authored via `param.string()`). */
|
|
3241
|
+
type StringLike = Date | Uint8Array;
|
|
3242
|
+
/** Scalar leaf types (a `param.*` placeholder replaces the leaf, never recursed). */
|
|
3243
|
+
type ScalarLeaf = string | number | boolean | bigint | symbol | StringLike;
|
|
3244
|
+
/** `null` / `undefined` — peeled off an optional leaf before the scalar test. */
|
|
3245
|
+
type Nullish = null | undefined;
|
|
3246
|
+
/**
|
|
3247
|
+
* The relaxed leaf type for a scalar `V`: the concrete value plus a matching
|
|
3248
|
+
* {@link Param} placeholder. A `string` / `number` / literal-union leaf `V` yields
|
|
3249
|
+
* `V | Param<V>` (tuple-wrapped below so a literal union is NOT distributed — the
|
|
3250
|
+
* whole-union `Param<'a' | 'b'>` fits, the #246 fix). A `Date` / `Uint8Array` leaf
|
|
3251
|
+
* additionally admits a `string` / `Param<string>` (its param-boundary form).
|
|
3252
|
+
*/
|
|
3253
|
+
type ParamLeaf<V> = [V] extends [StringLike] ? V | string | Param<string> : V | Param<V>;
|
|
3254
|
+
/**
|
|
3255
|
+
* Relaxes a real input type `K` so each scalar leaf additionally accepts a
|
|
3256
|
+
* {@link Param} standing in for that leaf's value type, while field names and value
|
|
3257
|
+
* types stay checked. The scalar test uses a **tuple wrap** so it does NOT distribute
|
|
3258
|
+
* over a literal-union leaf — a `'a' | 'b'` field yields the whole-union placeholder
|
|
3259
|
+
* `Param<'a' | 'b'>` (what `param.literal('a','b')` produces), the #246 fix. `Param`
|
|
3260
|
+
* covariance still admits a single-member subset placeholder, while a value outside
|
|
3261
|
+
* the union / a wider placeholder / a wrong scalar kind stays a type error.
|
|
3262
|
+
*/
|
|
3263
|
+
type Parameterize<K> = [Exclude<K, Nullish>] extends [ScalarLeaf] ? [Exclude<K, Nullish>] extends [never] ? K : ParamLeaf<Exclude<K, Nullish>> | Extract<K, Nullish> : K extends object ? {
|
|
3264
|
+
[P in keyof K]: Parameterize<K[P]>;
|
|
3265
|
+
} : K;
|
|
3266
|
+
/** Union of all keys across every member of a (possibly union) `Real` type. */
|
|
3267
|
+
type KnownKeyOf<Real> = Real extends object ? keyof Real : never;
|
|
3268
|
+
/** The value type of key `P` across the members of a (possibly union) `Real`, non-nullable. */
|
|
3269
|
+
type NonNullableMember<Real, P extends PropertyKey> = NonNullable<Real extends object ? (P extends keyof Real ? Real[P] : never) : never>;
|
|
3270
|
+
/**
|
|
3271
|
+
* Call-site excess-property guard for a parameterized structure (issue #41): maps
|
|
3272
|
+
* every key of the inferred `K` that is NOT a known key of `Real` to `never`, so a
|
|
3273
|
+
* stray field forces a compile error. Applied as `input & ExactParam<Real, input>`.
|
|
3274
|
+
*/
|
|
3275
|
+
type ExactParam<Real, K> = Real extends object ? {
|
|
3276
|
+
[P in keyof K]: P extends KnownKeyOf<Real> ? K[P] extends Param<unknown> ? K[P] : K[P] extends object ? K[P] & ExactParam<NonNullableMember<Real, P>, K[P]> : K[P] : never;
|
|
3277
|
+
} : K;
|
|
3278
|
+
/**
|
|
3279
|
+
* The model-aware constraint for a write descriptor's `input` given the descriptor
|
|
3280
|
+
* `D` — `Parameterize<Partial<EntityInput<Entity>>>` + `ExactParam` when `D` targets
|
|
3281
|
+
* a model via `create` / `update` / `remove`, else the inferred `input` unchanged. A
|
|
3282
|
+
* `Partial` because a write descriptor's `input` may set only a subset of fields (an
|
|
3283
|
+
* update patch / a create with defaulted fields).
|
|
3284
|
+
*/
|
|
3285
|
+
type WriteInputConstraint<D> = D extends {
|
|
3286
|
+
readonly input: infer I;
|
|
3287
|
+
} ? Parameterize<Partial<EntityInput<WriteDescriptorEntity<D>>>> & ExactParam<Partial<EntityInput<WriteDescriptorEntity<D>>>, I> : unknown;
|
|
3288
|
+
/** The entity a write descriptor targets, from its `create` / `update` / `remove` / `upsert` model ref. */
|
|
3289
|
+
type WriteDescriptorEntity<D> = D extends {
|
|
3290
|
+
readonly create: infer M;
|
|
3291
|
+
} ? EntityOfRef<M> : D extends {
|
|
3292
|
+
readonly update: infer M;
|
|
3293
|
+
} ? EntityOfRef<M> : D extends {
|
|
3294
|
+
readonly remove: infer M;
|
|
3295
|
+
} ? EntityOfRef<M> : D extends {
|
|
3296
|
+
readonly upsert: infer M;
|
|
3297
|
+
} ? EntityOfRef<M> : unknown;
|
|
3298
|
+
/**
|
|
3299
|
+
* The per-method validation overlay for {@link publishCommand} (issue #246): for
|
|
3300
|
+
* each **write descriptor** method it re-asserts `input` against the target model
|
|
3301
|
+
* (via {@link WriteInputConstraint}); non-write-descriptor methods (a bare
|
|
3302
|
+
* `mutation(...)` plan, a `PublicComposeDescriptor`, an SCP `TransactionDefinition`)
|
|
3303
|
+
* carry no model-checkable `input` here and pass through. Intersected into the
|
|
3304
|
+
* `publishCommand` generic constraint so the model narrowing bites at the call site
|
|
3305
|
+
* without widening the inferred `M`.
|
|
3306
|
+
*/
|
|
3307
|
+
type ValidateWriteDescriptors<M> = {
|
|
3308
|
+
readonly [K in keyof M]: M[K] extends PublicWriteDescriptor ? Omit<M[K], 'input'> & (M[K] extends {
|
|
3309
|
+
readonly input: unknown;
|
|
3310
|
+
} ? {
|
|
3311
|
+
readonly input: WriteInputConstraint<M[K]>;
|
|
3312
|
+
} : Record<never, never>) : M[K];
|
|
3313
|
+
};
|
|
3838
3314
|
/** The params a descriptor method accepts: the union of its `key` + `input` + `condition` leaf value types. */
|
|
3839
3315
|
type DescriptorParamsOf<D> = (D extends {
|
|
3840
3316
|
readonly key: infer K;
|
|
@@ -3921,7 +3397,7 @@ type WriteDescriptorKeyOf<D> = D extends {
|
|
|
3921
3397
|
} ? KeyOf<K> : unknown;
|
|
3922
3398
|
/**
|
|
3923
3399
|
* The query-method map accepted by the #101 **direct** form
|
|
3924
|
-
* (`
|
|
3400
|
+
* (`publishQuery({ get: descriptor, … })`): each value is a
|
|
3925
3401
|
* {@link PublicReadDescriptor}. The contract Key and per-method result are inferred
|
|
3926
3402
|
* from each descriptor's `key` / `select`.
|
|
3927
3403
|
*/
|
|
@@ -3932,12 +3408,13 @@ type ResolvedReadDescriptors<M extends ReadDescriptorMap> = QueryModelContract<M
|
|
|
3932
3408
|
}>;
|
|
3933
3409
|
/**
|
|
3934
3410
|
* The command-method map accepted by the #101 **direct** form
|
|
3935
|
-
* (`
|
|
3411
|
+
* (`publishCommand({ create: descriptor, … })`): each value is a
|
|
3936
3412
|
* {@link PublicWriteDescriptor}, a {@link PublicComposeDescriptor} (a `mutation()`
|
|
3937
|
-
* wrapped with `input` / `result` / `mode`),
|
|
3938
|
-
* {@link CommandPlan}
|
|
3413
|
+
* wrapped with `input` / `result` / `mode`), a bare composite `mutation()`
|
|
3414
|
+
* {@link CommandPlan}, or an SCP native-syntax `defineScpTransaction(...)`
|
|
3415
|
+
* multi-write body ({@link TransactionDefinition}, Phase 4 S1 / issue #278).
|
|
3939
3416
|
*/
|
|
3940
|
-
type WriteDescriptorMap = Record<string, PublicWriteDescriptor | PublicComposeDescriptor | CommandPlan>;
|
|
3417
|
+
type WriteDescriptorMap = Record<string, PublicWriteDescriptor | PublicComposeDescriptor | CommandPlan | TransactionDefinition>;
|
|
3941
3418
|
/** Resolve a write-descriptor map to its {@link CommandModelContract}. */
|
|
3942
3419
|
type ResolvedWriteDescriptors<M extends WriteDescriptorMap> = CommandModelContract<M[keyof M] extends infer D ? WriteDescriptorKeyOf<D> : unknown, {
|
|
3943
3420
|
readonly [K in keyof M]: CommandMethodSpec<WriteDescriptorKeyOf<M[K]>, DescriptorParamsOf<M[K]>, WriteDescriptorResultOf<M[K]>>;
|
|
@@ -3946,7 +3423,7 @@ type ResolvedWriteDescriptors<M extends WriteDescriptorMap> = CommandModelContra
|
|
|
3946
3423
|
* Create a **QueryModel** from a **descriptor map** (issue #101, the only form):
|
|
3947
3424
|
*
|
|
3948
3425
|
* ```ts
|
|
3949
|
-
* export const UserQueries =
|
|
3426
|
+
* export const UserQueries = publishQuery({
|
|
3950
3427
|
* get: { query: User, key: { userId: param.string() }, select: { name: true } },
|
|
3951
3428
|
* list: { list: GroupMembership, key: { groupId: param.string() }, select: { role: true } },
|
|
3952
3429
|
* });
|
|
@@ -3955,7 +3432,7 @@ type ResolvedWriteDescriptors<M extends WriteDescriptorMap> = CommandModelContra
|
|
|
3955
3432
|
* The single argument is the method map itself; the contract Key and each method's
|
|
3956
3433
|
* result type are **inferred** from the descriptors' `key` / `select` — there is no
|
|
3957
3434
|
* generic key parameter, no curried factory call, and no explicit Key-field list
|
|
3958
|
-
* (the legacy `
|
|
3435
|
+
* (the legacy `publishQuery<TKey>(['email'])` / model-derived overloads were
|
|
3959
3436
|
* withdrawn in #101). A GSI-keyed point read is expressed by its descriptor `key`
|
|
3960
3437
|
* (e.g. `key: { email: param.string() }`); the build resolves the access pattern
|
|
3961
3438
|
* from the model metadata, exactly as the curried key-field list once did. Each
|
|
@@ -3963,12 +3440,12 @@ type ResolvedWriteDescriptors<M extends WriteDescriptorMap> = CommandModelContra
|
|
|
3963
3440
|
* a `select` is the existing query projection (relation-nest aware), and may carry
|
|
3964
3441
|
* External Query `query(...)` nodes.
|
|
3965
3442
|
*/
|
|
3966
|
-
declare function
|
|
3443
|
+
declare function publishQuery<const M extends ReadDescriptorMap>(methods: M): ResolvedReadDescriptors<M>;
|
|
3967
3444
|
/**
|
|
3968
3445
|
* Create a **CommandModel** from a **descriptor map** (issue #101, the only form):
|
|
3969
3446
|
*
|
|
3970
3447
|
* ```ts
|
|
3971
|
-
* export const MembershipCommands =
|
|
3448
|
+
* export const MembershipCommands = publishCommand({
|
|
3972
3449
|
* create: { create: GroupMembership, key: { groupId: param.string(), userId: param.string() },
|
|
3973
3450
|
* input: { role: param.string() }, result: { select: { role: true } } },
|
|
3974
3451
|
* addMany: { create: GroupMembership, key: { … }, input: { … }, mode: 'parallel' },
|
|
@@ -3979,53 +3456,46 @@ declare function publicQueryModel<const M extends ReadDescriptorMap>(methods: M)
|
|
|
3979
3456
|
* The single argument is the method map itself; the contract Key and each method's
|
|
3980
3457
|
* read-back result are **inferred** from the descriptors' `key` / `result.select` —
|
|
3981
3458
|
* there is no generic key parameter, no curried factory call, and no explicit
|
|
3982
|
-
* Key-field list (the legacy `
|
|
3459
|
+
* Key-field list (the legacy `publishCommand<TKey>()` / model-derived overloads
|
|
3983
3460
|
* were withdrawn in #101). A descriptor's `mode` declares the per-call execution
|
|
3984
3461
|
* mode (`'transaction'` default | `'parallel'`); `result` presence drives whether
|
|
3985
3462
|
* the method reads back and returns the written entity.
|
|
3986
3463
|
*/
|
|
3987
|
-
declare function
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
/**
|
|
3999
|
-
* The return projection a {@link command} declares (proposal §3: "return = read
|
|
4000
|
-
* projection"): a JSON-safe boolean field map applied to the written entity as a
|
|
4001
|
-
* **consistent read-back** after the write commits. Omit for a fire-and-forget
|
|
4002
|
-
* command (no projected item is returned).
|
|
4003
|
-
*/
|
|
4004
|
-
type CommandSelectShape = Readonly<Record<string, boolean>>;
|
|
4005
|
-
/**
|
|
4006
|
-
* A **planned command method** (#83): the branded result of
|
|
4007
|
-
* `command({ input, select }).plan(mutation)`. It carries the captured fragment
|
|
4008
|
-
* IR (the {@link CommandPlan}), the public input param shape, and the return
|
|
4009
|
-
* `select`. {@link buildCommandContract} detects the brand and routes it to the
|
|
4010
|
-
* single-fragment mutation compiler (NOT through #64's closure path).
|
|
4011
|
-
*
|
|
4012
|
-
* The external surface is **params in / result out only** — the internal mutation
|
|
4013
|
-
* document is never exposed.
|
|
4014
|
-
*
|
|
4015
|
-
* @typeParam TInput - The input param shape (`{ field: Param<…> }`).
|
|
4016
|
-
* @typeParam TSelect - The return projection (`{ field: boolean }`), or `undefined`.
|
|
4017
|
-
*/
|
|
4018
|
-
interface PlannedCommandMethod<TInput extends CommandInputShape = CommandInputShape, TSelect extends CommandSelectShape | undefined = CommandSelectShape | undefined> {
|
|
4019
|
-
readonly [PLANNED_COMMAND_BRAND]: true;
|
|
4020
|
-
/** The captured mutation IR (the fragment list). */
|
|
4021
|
-
readonly plan: CommandPlan;
|
|
4022
|
-
/** The public input param shape. */
|
|
4023
|
-
readonly input: TInput;
|
|
4024
|
-
/** The return projection (consistent read-back), or `undefined`. */
|
|
4025
|
-
readonly select: TSelect;
|
|
3464
|
+
declare function publishCommand<const M extends WriteDescriptorMap & ValidateWriteDescriptors<M>>(methods: M): ResolvedWriteDescriptors<M>;
|
|
3465
|
+
/** Options accepted by the {@link mutate} authoring alias. */
|
|
3466
|
+
interface MutateAuthoringOptions {
|
|
3467
|
+
/**
|
|
3468
|
+
* The **atomicity mode** for this write body. This is the ONLY home of
|
|
3469
|
+
* atomicity (Phase 4 S3.5): `'transaction'` composes the body's writes into one
|
|
3470
|
+
* atomic `TransactWriteItems`; `'parallel'` runs them non-atomically. Omitted ⇒
|
|
3471
|
+
* non-atomic — graphddb never defaults atomicity, so a multi-write body with no
|
|
3472
|
+
* `mode` is a definition-time error (see the fail-closed guard).
|
|
3473
|
+
*/
|
|
3474
|
+
readonly mode?: MutateMode;
|
|
4026
3475
|
}
|
|
4027
|
-
/**
|
|
4028
|
-
|
|
3476
|
+
/**
|
|
3477
|
+
* The **`mutate` authoring alias** (Phase 4 S3.5) — the authoring twin of the
|
|
3478
|
+
* runtime `DDBModel.mutate` / `mutate({ mode })`, usable as a `publishCommand`
|
|
3479
|
+
* method value. It wraps an existing recorder / SCP-lowered write body (a
|
|
3480
|
+
* {@link TransactionDefinition} produced by `defineScpTransaction` /
|
|
3481
|
+
* `defineTransaction`, or a {@link CommandPlan} produced by `mutation(...)`) and
|
|
3482
|
+
* records the authoring `{ mode }` onto the produced IR — the SOLE place
|
|
3483
|
+
* atomicity is decided.
|
|
3484
|
+
*
|
|
3485
|
+
* It adds **no new lowering path**: the transform still recognizes only
|
|
3486
|
+
* `tx.put/update/delete/forEach`, and the body flows through the SAME
|
|
3487
|
+
* `buildTransactionSpec` path as the standalone form. The name + `{ mode }`
|
|
3488
|
+
* option deliberately mirror the runtime `mutate`; the runtime/authoring fold is
|
|
3489
|
+
* Phase 4 S6 (this only makes the authoring alias exist and carry `mode`).
|
|
3490
|
+
*
|
|
3491
|
+
* - `mutate(txDef, { mode })` → the same {@link TransactionDefinition} with
|
|
3492
|
+
* `mode` stamped (undefined ⇒ non-atomic; a multi-write body then throws in
|
|
3493
|
+
* `publishCommand`'s SCP-body branch — declare a mode).
|
|
3494
|
+
* - `mutate(plan, { mode })` → a {@link PublicComposeDescriptor} (`{ command,
|
|
3495
|
+
* mode }`), the closure-free composite form.
|
|
3496
|
+
*/
|
|
3497
|
+
declare function mutate(body: TransactionDefinition, options?: MutateAuthoringOptions): TransactionDefinition;
|
|
3498
|
+
declare function mutate(body: CommandPlan, options?: MutateAuthoringOptions): PublicComposeDescriptor;
|
|
4029
3499
|
/** The per-call execution mode of a command method (#101): replaces `batch`. */
|
|
4030
3500
|
type CommandMode = 'transaction' | 'parallel';
|
|
4031
3501
|
/** A public **read** descriptor (issue #101): `{ query | list: Model, key, select, options? }`. */
|
|
@@ -4043,11 +3513,21 @@ interface PublicReadDescriptor {
|
|
|
4043
3513
|
*/
|
|
4044
3514
|
readonly description?: string;
|
|
4045
3515
|
}
|
|
4046
|
-
/** A public **write** descriptor (issue #101): `{ create | update | remove: Model, key, input?, condition?, result?, mode? }`. */
|
|
3516
|
+
/** A public **write** descriptor (issue #101): `{ create | update | remove | upsert: Model, key, input?, condition?, result?, mode? }`. */
|
|
4047
3517
|
interface PublicWriteDescriptor {
|
|
4048
3518
|
readonly create?: ModelRef;
|
|
4049
3519
|
readonly update?: ModelRef;
|
|
4050
3520
|
readonly remove?: ModelRef;
|
|
3521
|
+
/**
|
|
3522
|
+
* `upsert` — a `PutItem` with **no** `attribute_not_exists(PK)` guard (an
|
|
3523
|
+
* unconditional overwrite; the semantics of the removed `definePut`). Unlike
|
|
3524
|
+
* `create` (always guarded against clobbering an existing row), an `upsert` writes
|
|
3525
|
+
* whether or not the row exists. An author-supplied `condition` is honored VERBATIM
|
|
3526
|
+
* — it is NOT ANDed with a `notExists` guard (that absence is what distinguishes an
|
|
3527
|
+
* upsert from a create). The compiled base op is a `Put` whose only condition is the
|
|
3528
|
+
* author's (or none), i.e. the ORIGINAL `definePut` wire shape.
|
|
3529
|
+
*/
|
|
3530
|
+
readonly upsert?: ModelRef;
|
|
4051
3531
|
readonly key: Readonly<Record<string, unknown>>;
|
|
4052
3532
|
readonly input?: Readonly<Record<string, unknown>>;
|
|
4053
3533
|
readonly condition?: Readonly<Record<string, unknown>> | RawCondition<any, any>;
|
|
@@ -4203,963 +3683,36 @@ interface MaintainItem {
|
|
|
4203
3683
|
* {@link destinationRowKey} write the same row — the material #125/#126 uses to
|
|
4204
3684
|
* enforce 論点2 = b ("1 mutation × 1 target row = 1 effect"; multiple is a
|
|
4205
3685
|
* reject). Built deterministically (sorted), so it is a directly comparable
|
|
4206
|
-
* string.
|
|
4207
|
-
*/
|
|
4208
|
-
readonly destinationRowKey: string;
|
|
4209
|
-
/** The A-defined maintenance IR this relation lowers to. */
|
|
4210
|
-
readonly effect: MaintainEffect;
|
|
4211
|
-
}
|
|
4212
|
-
/**
|
|
4213
|
-
* The maintenance graph: the trigger → effects index plus the validation surface.
|
|
4214
|
-
*
|
|
4215
|
-
* `byTrigger` is the core index: a `MaintainTrigger` (`"Post.created"`) maps to
|
|
4216
|
-
* every {@link MaintainItem} that fires on it, in a stable order (owner entity,
|
|
4217
|
-
* then relation property). Empty (no entry) for a trigger nothing maintains on.
|
|
4218
|
-
*/
|
|
4219
|
-
interface MaintenanceGraph {
|
|
4220
|
-
/** Every maintenance item, flattened, in deterministic order. */
|
|
4221
|
-
readonly items: readonly MaintainItem[];
|
|
4222
|
-
/** The core index: trigger → the effects it fires. */
|
|
4223
|
-
readonly byTrigger: ReadonlyMap<MaintainTrigger, readonly MaintainItem[]>;
|
|
4224
|
-
/** Look up the effects fired by a trigger (empty array if none). */
|
|
4225
|
-
effectsFor(trigger: MaintainTrigger): readonly MaintainItem[];
|
|
4226
|
-
/**
|
|
4227
|
-
* Items that, under the SAME trigger, write the SAME target row — grouped by
|
|
4228
|
-
* `"<trigger><targetRowKey>"`. Only groups with **more than one** item are
|
|
4229
|
-
* present, so an empty map means no trigger has a multi-maintainer collision.
|
|
4230
|
-
* This is the detection material for 論点2 = b; #124 surfaces it, #125/#126
|
|
4231
|
-
* decide the reject. (Build-time we do NOT reject here — a model may legitimately
|
|
4232
|
-
* declare overlapping shapes that a later phase reconciles; #124's contract is
|
|
4233
|
-
* to make the collision *detectable*.)
|
|
4234
|
-
*/
|
|
4235
|
-
readonly multiMaintainerTargets: ReadonlyMap<string, readonly MaintainItem[]>;
|
|
4236
|
-
}
|
|
4237
|
-
/**
|
|
4238
|
-
* Build the {@link MaintenanceGraph} from every registered model's relation-side
|
|
4239
|
-
* maintenance declarations (`MetadataRegistry.getAll()`), validating each as it is
|
|
4240
|
-
* collected (round-trip, unresolved trigger) and the whole graph (cycle) before
|
|
4241
|
-
* returning. Throws a loud, actionable error on any invalid declaration.
|
|
4242
|
-
*
|
|
4243
|
-
* @param registry The models to scan. Defaults to `MetadataRegistry.getAll()`;
|
|
4244
|
-
* an explicit map is accepted for tests / scoped builds.
|
|
4245
|
-
*/
|
|
4246
|
-
declare function buildMaintenanceGraph(registry?: ReadonlyMap<Function, EntityMetadata>, views?: readonly ViewDefinition[]): MaintenanceGraph;
|
|
4247
|
-
|
|
4248
|
-
/**
|
|
4249
|
-
* Serializable static operation specs and manifest (issue #42, Python-bridge
|
|
4250
|
-
* Phase 0b).
|
|
4251
|
-
*
|
|
4252
|
-
* Everything in this module is **JSON-serializable** by construction (only
|
|
4253
|
-
* strings / numbers / booleans / arrays / plain objects — no functions, classes,
|
|
4254
|
-
* `Date`, `Set`, or `undefined`-valued fields are emitted). These types mirror
|
|
4255
|
-
* the `graphddb_manifest.json` / `graphddb_operations.json` shapes sketched in
|
|
4256
|
-
* `docs/python-bridge.md`. The generator (#43) writes them to disk; the
|
|
4257
|
-
* Python runtime (#44+) interprets them.
|
|
4258
|
-
*
|
|
4259
|
-
* ## Templates and placeholders
|
|
4260
|
-
*
|
|
4261
|
-
* Key-condition / range / item / change *values* are **template strings** with
|
|
4262
|
-
* two placeholder forms:
|
|
4263
|
-
*
|
|
4264
|
-
* - `{paramName}` — bound from the caller-supplied params at execution time.
|
|
4265
|
-
* - `{result.sourceField}` — bound from a field of the **prior** operation's
|
|
4266
|
-
* result item(s), used to chain relation operations.
|
|
4267
|
-
*
|
|
4268
|
-
* A value with no `{...}` is a literal (e.g. a fixed sort-key discriminator
|
|
4269
|
-
* `PROFILE`).
|
|
4270
|
-
*/
|
|
4271
|
-
|
|
4272
|
-
/**
|
|
4273
|
-
* The schema version emitted in every manifest / operations document.
|
|
4274
|
-
*
|
|
4275
|
-
* `1.1` (issue #208, B案): the operation IR gained the **list fan-out binding
|
|
4276
|
-
* form** ({@link SourceListSpec} on a `BatchGetItem` relation op), lifting the
|
|
4277
|
-
* #197 `refs` loud-reject. The op vocabulary (the physical DynamoDB API surface)
|
|
4278
|
-
* is unchanged — only the dataflow (source→key) binding grammar grew. Purely
|
|
4279
|
-
* additive: every `1.0` document is a valid `1.1` document, and a document that
|
|
4280
|
-
* uses no `refs` relation serializes byte-identically apart from this version.
|
|
4281
|
-
*/
|
|
4282
|
-
declare const SPEC_VERSION: "1.1";
|
|
4283
|
-
/** Field type as carried in the manifest (derived from the DynamoDB type). */
|
|
4284
|
-
type ManifestFieldType = 'string' | 'number' | 'boolean' | 'binary' | 'stringSet' | 'numberSet' | 'list' | 'map';
|
|
4285
|
-
interface ManifestField {
|
|
4286
|
-
readonly type: ManifestFieldType;
|
|
4287
|
-
/**
|
|
4288
|
-
* Semantic format for `string` fields that carry a serialized temporal value.
|
|
4289
|
-
* `datetime` → ISO 8601 instant, `date` → `YYYY-MM-DD`. Absent for plain
|
|
4290
|
-
* fields. The Python runtime uses this to restore `datetime` on hydration,
|
|
4291
|
-
* mirroring the TS hydrator's `format`-driven `Date` reconstruction.
|
|
4292
|
-
*/
|
|
4293
|
-
readonly format?: 'datetime' | 'date';
|
|
4294
|
-
/**
|
|
4295
|
-
* Optional human-readable description of the field (issue #154), from a field
|
|
4296
|
-
* decorator option (`@string({ description })`). Pure documentation — absent
|
|
4297
|
-
* unless declared, so a field with no description serializes byte-identically
|
|
4298
|
-
* to the pre-#154 manifest. Surfaces as a field comment in the generated Python.
|
|
4299
|
-
*/
|
|
4300
|
-
readonly description?: string;
|
|
4301
|
-
}
|
|
4302
|
-
/** A key (PK or GSI) template descriptor in the manifest. */
|
|
4303
|
-
interface ManifestKey {
|
|
4304
|
-
/** Input field names the key is composed from (in declaration order). */
|
|
4305
|
-
readonly inputFields: readonly string[];
|
|
4306
|
-
/** Template for the partition-key value, e.g. `EMAIL#{email}`. */
|
|
4307
|
-
readonly pkTemplate: string;
|
|
4308
|
-
/** Template for the sort-key value, or `null` when the key has no sort key. */
|
|
4309
|
-
readonly skTemplate: string | null;
|
|
4310
|
-
}
|
|
4311
|
-
interface ManifestGsi extends ManifestKey {
|
|
4312
|
-
readonly indexName: string;
|
|
4313
|
-
readonly unique: boolean;
|
|
4314
|
-
/**
|
|
4315
|
-
* Optional human-readable description of the index (issue #166, follow-up of #154),
|
|
4316
|
-
* from `gsi(name, key, { description })`. Pure documentation — absent unless declared,
|
|
4317
|
-
* so a GSI with no description serializes byte-identically to the pre-#166 manifest.
|
|
4318
|
-
* The Python codegen surfaces it as the docstring of a generated query method that
|
|
4319
|
-
* reads through this index.
|
|
4320
|
-
*/
|
|
4321
|
-
readonly description?: string;
|
|
4322
|
-
}
|
|
4323
|
-
interface ManifestRelation {
|
|
4324
|
-
readonly type: 'hasMany' | 'hasOne' | 'belongsTo' | 'refs';
|
|
4325
|
-
/** Target entity (model class) name. */
|
|
4326
|
-
readonly target: string;
|
|
4327
|
-
/** target field → source field (on this entity). */
|
|
4328
|
-
readonly keyBinding: Readonly<Record<string, string>>;
|
|
4329
|
-
/**
|
|
4330
|
-
* List-valued source descriptor for a `refs` relation (issue #197). Present IFF
|
|
4331
|
-
* `type === 'refs'`: `from` is the parent LIST attribute holding the inline
|
|
4332
|
-
* reference elements (e.g. `'tagRefs'`), `key` the field read off each element
|
|
4333
|
-
* (e.g. `'tagId'`). Absent for every scalar-keyed relation (byte-identical to
|
|
4334
|
-
* the pre-#197 manifest). Carried so a manifest consumer can reconstruct where a
|
|
4335
|
-
* `refs` relation's BatchGet keys come from.
|
|
4336
|
-
*/
|
|
4337
|
-
readonly refs?: Readonly<{
|
|
4338
|
-
from: string;
|
|
4339
|
-
key: string;
|
|
4340
|
-
}>;
|
|
4341
|
-
/**
|
|
4342
|
-
* Optional human-readable description of the relation (issue #166, follow-up of
|
|
4343
|
-
* #154), from `@hasMany/@belongsTo/@hasOne(..., { description })`. Pure documentation
|
|
4344
|
-
* — absent unless declared, so a relation with no description serializes
|
|
4345
|
-
* byte-identically to the pre-#166 manifest. The Python codegen surfaces it as a
|
|
4346
|
-
* trailing `# …` comment on the relation's field in the generated result type.
|
|
4347
|
-
*/
|
|
4348
|
-
readonly description?: string;
|
|
4349
|
-
}
|
|
4350
|
-
interface ManifestEntity {
|
|
4351
|
-
/** Declared (logical) table name. */
|
|
4352
|
-
readonly table: string;
|
|
4353
|
-
/** Physical table name (after `TableMapping` resolution). */
|
|
4354
|
-
readonly physicalName: string;
|
|
4355
|
-
/** PK prefix, e.g. `USER#`. */
|
|
4356
|
-
readonly prefix: string;
|
|
4357
|
-
readonly fields: Readonly<Record<string, ManifestField>>;
|
|
4358
|
-
/** Primary key, or `null` when the entity has none. */
|
|
4359
|
-
readonly key: ManifestKey | null;
|
|
4360
|
-
readonly gsis: readonly ManifestGsi[];
|
|
4361
|
-
readonly relations: Readonly<Record<string, ManifestRelation>>;
|
|
4362
|
-
/**
|
|
4363
|
-
* Optional human-readable description of the entity (issue #154), from
|
|
4364
|
-
* `@model({ description })`. Pure documentation — absent unless declared, so an
|
|
4365
|
-
* entity with no description serializes byte-identically to the pre-#154
|
|
4366
|
-
* manifest. Surfaces as the generated Python class docstring.
|
|
4367
|
-
*/
|
|
4368
|
-
readonly description?: string;
|
|
4369
|
-
/**
|
|
4370
|
-
* The physical attribute name of the model's DynamoDB **Time-To-Live** field,
|
|
4371
|
-
* when one is declared via `@ttl` (issue #172, Epic #167 — CFn generator C4).
|
|
4372
|
-
* Unlike the maintenance/stream signals (a maintenance-IR concern kept OFF the
|
|
4373
|
-
* manifest), TTL is a **physical schema** fact, and the CloudFormation emitter
|
|
4374
|
-
* consumes the manifest — so the TTL attribute name flows here and the emitter
|
|
4375
|
-
* renders `TimeToLiveSpecification { AttributeName: <this>, Enabled: true }`. The
|
|
4376
|
-
* attribute is deliberately NOT added to the table's key `AttributeDefinitions`
|
|
4377
|
-
* (that list holds ONLY key/index attributes; `TimeToLiveSpecification` legitimately
|
|
4378
|
-
* names a non-key attribute). **Absent** when the model declares no `@ttl`, so a
|
|
4379
|
-
* TTL-free model serializes byte-identically to the pre-#172 manifest (backward
|
|
4380
|
-
* compatible). DynamoDB permits exactly one TTL attribute per physical table; the
|
|
4381
|
-
* single-per-table rule is enforced at manifest build.
|
|
4382
|
-
*/
|
|
4383
|
-
readonly ttlAttribute?: string;
|
|
4384
|
-
}
|
|
4385
|
-
interface ManifestTable {
|
|
4386
|
-
readonly physicalName: string;
|
|
4387
|
-
}
|
|
4388
|
-
interface Manifest {
|
|
4389
|
-
readonly version: typeof SPEC_VERSION;
|
|
4390
|
-
readonly tables: Readonly<Record<string, ManifestTable>>;
|
|
4391
|
-
readonly entities: Readonly<Record<string, ManifestEntity>>;
|
|
4392
|
-
}
|
|
4393
|
-
interface ParamSpec {
|
|
4394
|
-
readonly type: ParamKind;
|
|
4395
|
-
readonly required: boolean;
|
|
4396
|
-
/** Allowed literal values for `literal` params; omitted otherwise. */
|
|
4397
|
-
readonly literals?: readonly (string | number)[];
|
|
4398
|
-
/**
|
|
4399
|
-
* For an `array` param (transaction `forEach` source), the per-element field
|
|
4400
|
-
* descriptors (field name → element param spec). Omitted for scalar params.
|
|
4401
|
-
*/
|
|
4402
|
-
readonly element?: Readonly<Record<string, ParamSpec>>;
|
|
4403
|
-
/**
|
|
4404
|
-
* Optional human-readable description of the parameter (issue #154), from a
|
|
4405
|
-
* `param.*({ description })` placeholder. Pure documentation — absent unless
|
|
4406
|
-
* declared, so a param with no description serializes byte-identically to the
|
|
4407
|
-
* pre-#154 spec. The Python runtime never reads it for execution.
|
|
4408
|
-
*/
|
|
4409
|
-
readonly description?: string;
|
|
4410
|
-
}
|
|
4411
|
-
/** A `begins_with` range condition on a sort key (templated value). */
|
|
4412
|
-
interface RangeConditionSpec {
|
|
4413
|
-
readonly operator: 'begins_with';
|
|
4414
|
-
readonly key: string;
|
|
4415
|
-
/** Template value, e.g. `GROUP#` or `GROUP#{result.groupId}`. */
|
|
4416
|
-
readonly value: string;
|
|
4417
|
-
}
|
|
4418
|
-
/** Server-side declarative filter, carried verbatim for the runtime to compile. */
|
|
4419
|
-
interface FilterSpec {
|
|
4420
|
-
/** The declarative {@link FilterInput} tree (JSON-safe). */
|
|
4421
|
-
readonly declarative: unknown;
|
|
4422
|
-
}
|
|
4423
|
-
type ReadOperationType = 'Query' | 'GetItem' | 'BatchGetItem';
|
|
4424
|
-
/**
|
|
4425
|
-
* A single static read operation. Relation chains are expressed as multiple
|
|
4426
|
-
* operations whose `resultPath` / templated key conditions wire them together.
|
|
4427
|
-
*/
|
|
4428
|
-
interface OperationSpec {
|
|
4429
|
-
readonly type: ReadOperationType;
|
|
4430
|
-
readonly tableName: string;
|
|
4431
|
-
readonly indexName?: string;
|
|
4432
|
-
/**
|
|
4433
|
-
* Key condition: attribute name → template value. For a `BatchGetItem` this is
|
|
4434
|
-
* the per-key shape (one key per resolved source item at runtime).
|
|
4435
|
-
*/
|
|
4436
|
-
readonly keyCondition: Readonly<Record<string, string>>;
|
|
4437
|
-
readonly rangeCondition?: RangeConditionSpec;
|
|
4438
|
-
/** Projected fields (entity field names). */
|
|
4439
|
-
readonly projection: readonly string[];
|
|
4440
|
-
readonly limit?: number;
|
|
4441
|
-
readonly filter?: FilterSpec;
|
|
4442
|
-
/**
|
|
4443
|
-
* Where the result of this operation is placed in the assembled result.
|
|
4444
|
-
* `$` is the root; `$.groups.items` a relation connection's items, etc.
|
|
4445
|
-
*/
|
|
4446
|
-
readonly resultPath: string;
|
|
4447
|
-
/**
|
|
4448
|
-
* For relation operations: the source field on the prior result whose value(s)
|
|
4449
|
-
* drive this operation's `{result.*}` placeholders. Absent on the root op.
|
|
4450
|
-
*/
|
|
4451
|
-
readonly sourceField?: string;
|
|
4452
|
-
/**
|
|
4453
|
-
* The **list fan-out binding form** (issue #208, B案 — the shared-IR
|
|
4454
|
-
* generalization that lifted the #197 `refs` loud-reject). Present only on a
|
|
4455
|
-
* `BatchGetItem` relation operation that resolves a `refs` relation: instead of
|
|
4456
|
-
* reading ONE scalar `parent[sourceField]`, the runtime reads the parent **list
|
|
4457
|
-
* attribute** {@link SourceListSpec.from} and binds `{result.<sourceField>}`
|
|
4458
|
-
* once per element — `element[key]` for an object element, the element itself
|
|
4459
|
-
* for a bare scalar — skipping null/undefined refs. All element keys across all
|
|
4460
|
-
* parents fan into ONE deduped `BatchGetItem` (the existing chunk/retry
|
|
4461
|
-
* machinery), and the operation's `resultPath` (`…<prop>.items`) receives a
|
|
4462
|
-
* connection `{ items, cursor: null }` whose items are the resolved child
|
|
4463
|
-
* bodies in **first-seen element order**, deduped, with missing bodies dropped
|
|
4464
|
-
* — byte-matching the TS in-process `fetchRefsList` semantics. Absent on every
|
|
4465
|
-
* scalar-bound relation op (a pre-#208 document is byte-identical).
|
|
4466
|
-
*/
|
|
4467
|
-
readonly sourceList?: SourceListSpec;
|
|
4468
|
-
}
|
|
4469
|
-
/**
|
|
4470
|
-
* The list-valued source descriptor of a fan-out `BatchGetItem` operation (issue
|
|
4471
|
-
* #208 B案; the operation-IR mirror of the manifest's `ManifestRelation.refs`).
|
|
4472
|
-
*/
|
|
4473
|
-
interface SourceListSpec {
|
|
4474
|
-
/** The parent LIST attribute holding the inline reference elements (e.g. `tagRefs`). */
|
|
4475
|
-
readonly from: string;
|
|
4476
|
-
/**
|
|
4477
|
-
* The field read off each element (e.g. `tagId`). Always equals the operation's
|
|
4478
|
-
* `sourceField` (the `{result.<sourceField>}` token name); carried explicitly so
|
|
4479
|
-
* the op is self-describing.
|
|
4480
|
-
*/
|
|
4481
|
-
readonly key: string;
|
|
4482
|
-
/**
|
|
4483
|
-
* `true` when the parent list attribute was NOT selected by the query and was
|
|
4484
|
-
* projected onto the parent operation **solely** to drive this fan-out. The
|
|
4485
|
-
* runtime must then delete `from` from every parent node after ALL relation
|
|
4486
|
-
* operations are applied — mirroring the TS hydrator, which projects implicit
|
|
4487
|
-
* relation-source fields but strips them from the assembled result. Absent when
|
|
4488
|
-
* the query's select projects the attribute itself (it stays in the result).
|
|
4489
|
-
*/
|
|
4490
|
-
readonly implicit?: boolean;
|
|
4491
|
-
}
|
|
4492
|
-
/**
|
|
4493
|
-
* The **execution plan** for a multi-operation read (issue #70a). It is the
|
|
4494
|
-
* Single Source of Truth for *how* a query's {@link OperationSpec}[] is staged:
|
|
4495
|
-
* which operations are independent (may run concurrently) and which must wait for
|
|
4496
|
-
* a prior operation's result. Every runtime that honors it produces identical
|
|
4497
|
-
* effects with identical concurrency discipline — the staging is **serialized,
|
|
4498
|
-
* not re-derived** (cf. the contract decided-facts discipline).
|
|
4499
|
-
*
|
|
4500
|
-
* - `groups` is an ordered list of **stages**; each stage is a list of indices
|
|
4501
|
-
* into the query's `operations[]`. Operations **within** a stage are mutually
|
|
4502
|
-
* independent and may be issued concurrently; stages run **in order** because a
|
|
4503
|
-
* later stage reads a `{result.*}` value produced by an earlier one. Stage 0 is
|
|
4504
|
-
* always `[0]` (the root). Every operation index appears in exactly one stage,
|
|
4505
|
-
* and the stages are a topological layering of the result-dependency graph.
|
|
4506
|
-
* - `concurrency` is the declared in-flight bound a runtime applies **within**
|
|
4507
|
-
* each stage (the shared {@link RELATION_TRAVERSAL_CONCURRENCY} = 16): at most
|
|
4508
|
-
* this many operations of a stage are issued at once.
|
|
4509
|
-
*
|
|
4510
|
-
* ## Derivation (see `buildReadOperations` / `deriveExecutionPlan`)
|
|
4511
|
-
*
|
|
4512
|
-
* The planner emits `operations[]` in a deterministic pre-order: the root at
|
|
4513
|
-
* index 0, then each relation child immediately followed by its own descendants.
|
|
4514
|
-
* A child's key templates reference `{result.<sourceField>}` of the **operation
|
|
4515
|
-
* whose `resultPath` is the child's parent path** — that producer is the child's
|
|
4516
|
-
* sole dependency. The stage of an operation is therefore `1 + stage(parent)`:
|
|
4517
|
-
* the root is stage 0; sibling relations that share a parent path land in the
|
|
4518
|
-
* **same** stage (no inter-`{result.*}` dependency between siblings, so they are
|
|
4519
|
-
* independent and concurrency-eligible); a grandchild that reads its parent's
|
|
4520
|
-
* `{result.*}` lands one stage later. This mirrors exactly the level-by-level
|
|
4521
|
-
* fan-out the TS relation runtime performs at runtime (`resolveRelations` issues
|
|
4522
|
-
* sibling relations together under the same bound) — #70a only *records* it.
|
|
4523
|
-
*
|
|
4524
|
-
* Absent on a single-operation spec and on specs produced before #70a; a consumer
|
|
4525
|
-
* without a plan falls back to one-operation-per-stage **sequential** execution
|
|
4526
|
-
* (the pre-#70 behavior), so an absent plan never changes results.
|
|
4527
|
-
*/
|
|
4528
|
-
interface ExecutionPlanSpec {
|
|
4529
|
-
/** Ordered stages; each stage is indices into `operations[]` (stage 0 = `[0]`). */
|
|
4530
|
-
readonly groups: readonly (readonly number[])[];
|
|
4531
|
-
/** The declared in-flight bound applied within each stage (16). */
|
|
4532
|
-
readonly concurrency: number;
|
|
4533
|
-
}
|
|
4534
|
-
/** A query (read) definition's full execution spec. */
|
|
4535
|
-
interface QuerySpec {
|
|
4536
|
-
readonly params: Readonly<Record<string, ParamSpec>>;
|
|
4537
|
-
readonly operations: readonly OperationSpec[];
|
|
4538
|
-
/**
|
|
4539
|
-
* Result cardinality of the **root** (`$`) operation, derived from the
|
|
4540
|
-
* definition kind: `'one'` for `defineQuery` (a single entity object, with any
|
|
4541
|
-
* relations attached directly), `'many'` for `defineList` (a `{ items, cursor }`
|
|
4542
|
-
* connection). The relation runtime (#45) uses this to shape the root result:
|
|
4543
|
-
* a `'one'` Query root takes the first matched item rather than returning a
|
|
4544
|
-
* connection. Absent in specs produced before #45; consumers should treat an
|
|
4545
|
-
* absent value as `'many'` for a `Query`/`BatchGetItem` root and `'one'` for a
|
|
4546
|
-
* `GetItem` root (the pre-#45 behavior).
|
|
4547
|
-
*/
|
|
4548
|
-
readonly cardinality?: 'one' | 'many';
|
|
4549
|
-
/**
|
|
4550
|
-
* The staged execution plan (issue #70a): which {@link operations} are
|
|
4551
|
-
* independent (concurrency-eligible) vs. result-dependent. Present only when the
|
|
4552
|
-
* query has more than one operation (a relation chain); a single-operation read
|
|
4553
|
-
* needs no plan. See {@link ExecutionPlanSpec} for the derivation and the
|
|
4554
|
-
* backward-compatible (plan-absent → sequential) fallback.
|
|
4555
|
-
*/
|
|
4556
|
-
readonly executionPlan?: ExecutionPlanSpec;
|
|
4557
|
-
/**
|
|
4558
|
-
* Optional human-readable description of the query (issue #154), from
|
|
4559
|
-
* `defineQuery(..., { description })`. Pure documentation — absent unless
|
|
4560
|
-
* declared, so a query with no description serializes byte-identically to the
|
|
4561
|
-
* pre-#154 spec. Surfaces as the generated Python repository-method docstring.
|
|
4562
|
-
*/
|
|
4563
|
-
readonly description?: string;
|
|
4564
|
-
}
|
|
4565
|
-
type WriteOperationType = 'PutItem' | 'UpdateItem' | 'DeleteItem';
|
|
4566
|
-
/**
|
|
4567
|
-
* A condition expression on a write, as a JSON-serializable spec (issues #46,
|
|
4568
|
-
* #81). The supported subset:
|
|
4569
|
-
*
|
|
4570
|
-
* - `{ kind: 'notExists' }` → `attribute_not_exists(PK)` (legacy whole-row guard).
|
|
4571
|
-
* - `{ kind: 'attributeExists'; field }` → `attribute_exists(<field>)` — the named
|
|
4572
|
-
* attribute (any field, incl. PK/SK) must be present. The foundation for
|
|
4573
|
-
* referential-integrity derivation.
|
|
4574
|
-
* - `{ kind: 'attributeNotExists'; field }` → `attribute_not_exists(<field>)` — the
|
|
4575
|
-
* named attribute must be absent (field-level uniqueness / first-write guard).
|
|
4576
|
-
* - `{ kind: 'equals'; fields }` → `#f = :v AND …` field equality (each value a
|
|
4577
|
-
* `{param}` / literal template).
|
|
4578
|
-
* - `{ kind: 'expr'; declarative }` → the full declarative operator tree (issue
|
|
4579
|
-
* #114-A): comparison (`gt`/`ge`/`lt`/`le`/`ne`), `between`, `in`,
|
|
4580
|
-
* `begins_with`/`contains`/`notContains`, `attributeType`, and the logical
|
|
4581
|
-
* `and`/`or`/`not` groups, mirroring the read-side {@link FilterSpec}. The tree
|
|
4582
|
-
* is JSON-safe: each leaf value is either a native literal (string / number /
|
|
4583
|
-
* boolean) or a {@link ConditionParamLeaf} (`{ $param }`) marking a caller
|
|
4584
|
-
* param bound at execution time. The runtime resolves the param leaves against
|
|
4585
|
-
* the caller params, then compiles the tree to a DynamoDB `ConditionExpression`
|
|
4586
|
-
* with the SAME mechanics the filter compiler uses (so TS and Python emit an
|
|
4587
|
-
* identical expression / semantics).
|
|
4588
|
-
*/
|
|
4589
|
-
type ConditionSpec = {
|
|
4590
|
-
readonly kind: 'notExists';
|
|
4591
|
-
} | {
|
|
4592
|
-
readonly kind: 'attributeExists';
|
|
4593
|
-
readonly field: string;
|
|
4594
|
-
} | {
|
|
4595
|
-
readonly kind: 'attributeNotExists';
|
|
4596
|
-
readonly field: string;
|
|
4597
|
-
} | {
|
|
4598
|
-
readonly kind: 'equals';
|
|
4599
|
-
readonly fields: Readonly<Record<string, string>>;
|
|
4600
|
-
} | {
|
|
4601
|
-
readonly kind: 'expr';
|
|
4602
|
-
readonly declarative: unknown;
|
|
4603
|
-
} | {
|
|
4604
|
-
/**
|
|
4605
|
-
* A raw DynamoDB condition produced by the `cond` escape hatch (issue
|
|
4606
|
-
* #114-B), for write conditions that the declarative operator subset
|
|
4607
|
-
* cannot express. It is **pre-compiled and deterministic**: the
|
|
4608
|
-
* `expression` is a finished DynamoDB `ConditionExpression` whose name
|
|
4609
|
-
* placeholders are stable `#cr_<field>` aliases (reused per distinct
|
|
4610
|
-
* column) and whose value placeholders are sequential `:cr0`, `:cr1`, …
|
|
4611
|
-
* (assigned in template order), so the serialized golden is stable. The
|
|
4612
|
-
* names map binds each `#cr_<field>` alias to its entity field; the values
|
|
4613
|
-
* map binds each `:crN` alias to either a native literal (an embedded
|
|
4614
|
-
* `cond` value — in-process the concrete value) or a {@link ConditionParamLeaf}
|
|
4615
|
-
* (`{ $param }`) marker bound from a caller param at execution time (the
|
|
4616
|
-
* public-contract slot). The runtime substitutes the param leaves, then
|
|
4617
|
-
* attaches `expression` / `names` / serialized `values` to the write —
|
|
4618
|
-
* TS and Python build the identical DynamoDB expression.
|
|
4619
|
-
*/
|
|
4620
|
-
readonly kind: 'raw';
|
|
4621
|
-
readonly expression: string;
|
|
4622
|
-
readonly names: Readonly<Record<string, string>>;
|
|
4623
|
-
readonly values: Readonly<Record<string, unknown>>;
|
|
4624
|
-
};
|
|
4625
|
-
interface CommandSpec {
|
|
4626
|
-
readonly type: WriteOperationType;
|
|
4627
|
-
readonly tableName: string;
|
|
4628
|
-
readonly entity: string;
|
|
4629
|
-
readonly params: Readonly<Record<string, ParamSpec>>;
|
|
4630
|
-
/**
|
|
4631
|
-
* Key condition for `UpdateItem` / `DeleteItem` (attribute name → template).
|
|
4632
|
-
* Absent for `PutItem` (the whole item is built from `item`).
|
|
4633
|
-
*/
|
|
4634
|
-
readonly keyCondition?: Readonly<Record<string, string>>;
|
|
4635
|
-
/** The full item template for `PutItem` (field name → template / literal). */
|
|
4636
|
-
readonly item?: Readonly<Record<string, string>>;
|
|
4637
|
-
/** Field-level changes for `UpdateItem` (field name → template / literal). */
|
|
4638
|
-
readonly changes?: Readonly<Record<string, string>>;
|
|
4639
|
-
/** Optional write condition (subset). */
|
|
4640
|
-
readonly condition?: ConditionSpec;
|
|
4641
|
-
/**
|
|
4642
|
-
* Optional human-readable description of the command (issue #154), from
|
|
4643
|
-
* `definePut`/`defineUpdate`/`defineDelete(..., { description })`. Pure
|
|
4644
|
-
* documentation — absent unless declared, so a command with no description
|
|
4645
|
-
* serializes byte-identically to the pre-#154 spec. Surfaces as the generated
|
|
4646
|
-
* Python repository-method docstring.
|
|
4647
|
-
*/
|
|
4648
|
-
readonly description?: string;
|
|
4649
|
-
}
|
|
4650
|
-
/**
|
|
4651
|
-
* A declarative `when` guard on a transaction item (issue #46). Compares a
|
|
4652
|
-
* templated left-hand value against a templated right-hand value; the item is
|
|
4653
|
-
* skipped when the comparison does not hold. Both values are template strings
|
|
4654
|
-
* (`{param}` / `{item.<field>}` / literal), resolved by the runtime before the
|
|
4655
|
-
* comparison. The runtime compares the resolved **string** values.
|
|
4656
|
-
*/
|
|
4657
|
-
interface WhenSpec {
|
|
4658
|
-
readonly op: 'eq' | 'ne';
|
|
4659
|
-
/** Templated left-hand value (typically an `{item.<field>}` or `{param}`). */
|
|
4660
|
-
readonly left: string;
|
|
4661
|
-
/** Templated right-hand value (literal or another reference). */
|
|
4662
|
-
readonly right: string;
|
|
4663
|
-
}
|
|
4664
|
-
/**
|
|
4665
|
-
* The kind of a transaction item. The three write kinds (`Put` / `Update` /
|
|
4666
|
-
* `Delete`) mutate an item; `ConditionCheck` (issue #81) is a **read-only
|
|
4667
|
-
* assertion** on another item — it asserts a {@link ConditionSpec} holds for the
|
|
4668
|
-
* keyed row without modifying it, and its failure cancels the **whole**
|
|
4669
|
-
* `TransactWriteItems` atomically. It is the foundation for referential-integrity
|
|
4670
|
-
* derivation (proposal: `requires <Entity> exists` → a `ConditionCheck` with
|
|
4671
|
-
* `attribute_exists`).
|
|
4672
|
-
*/
|
|
4673
|
-
type TransactionItemType = 'Put' | 'Update' | 'Delete' | 'ConditionCheck';
|
|
4674
|
-
/**
|
|
4675
|
-
* A value leaf of a transaction `item` / `changes` / `add` map (issue #245).
|
|
4676
|
-
*
|
|
4677
|
-
* - a **string** carries a template: a `{param}` / `{item.field}` placeholder, a
|
|
4678
|
-
* composite (`PREFIX#{param}`), or a plain string literal — resolved by the
|
|
4679
|
-
* runtimes' template machinery (a whole-placeholder string keeps the bound
|
|
4680
|
-
* param's *type*, a composite / plain string resolves to a string);
|
|
4681
|
-
* - a **number** / **boolean** carries a *typed literal* verbatim, so a numeric
|
|
4682
|
-
* literal in a `put` item / `update` changes (`version: 0`) survives to DynamoDB
|
|
4683
|
-
* as an `N` / `BOOL` instead of being stringified to an `S` (the pre-#245 bug).
|
|
4684
|
-
*
|
|
4685
|
-
* A `Date` literal is serialized to its ISO-8601 **string** at build time (a
|
|
4686
|
-
* `@datetime` / `@date` field is stored as `S`), so it stays a string leaf.
|
|
4687
|
-
*/
|
|
4688
|
-
type TransactionValueLeaf = string | number | boolean;
|
|
4689
|
-
/**
|
|
4690
|
-
* A single templated item in a transaction. When `forEach` is present, the item
|
|
4691
|
-
* is expanded **once per element** of the named array param, with each element's
|
|
4692
|
-
* fields bound to the item's `{item.<field>}` placeholders.
|
|
4693
|
-
*
|
|
4694
|
-
* Field presence by `type`:
|
|
4695
|
-
*
|
|
4696
|
-
* | type | item | keyCondition | changes | add | condition |
|
|
4697
|
-
* |----------------|------|--------------|---------|-----|--------------------------|
|
|
4698
|
-
* | `Put` | ✓ | — | — | — | optional |
|
|
4699
|
-
* | `Update` | — | ✓ | ✓ / — | ✓ / — | optional |
|
|
4700
|
-
* | `Delete` | — | ✓ | — | — | optional |
|
|
4701
|
-
* | `ConditionCheck` | — | ✓ | — | — | **required** (the assert) |
|
|
4702
|
-
*
|
|
4703
|
-
* An `Update` carries `changes` (a `SET` of named fields) and/or `add` (an atomic
|
|
4704
|
-
* `ADD` of named numeric fields, issue #85) — at least one of the two. The two are
|
|
4705
|
-
* distinct DynamoDB update actions: `SET #f = :v` **overwrites**, while
|
|
4706
|
-
* `ADD #f :delta` **atomically increments** without a read (concurrency-safe), so
|
|
4707
|
-
* a derived counter (`User.postCount += 1`) MUST be an `add`, never a `changes`
|
|
4708
|
-
* `SET` (which would clobber a concurrent increment).
|
|
4709
|
-
*/
|
|
4710
|
-
interface TransactionItemSpec {
|
|
4711
|
-
readonly type: TransactionItemType;
|
|
4712
|
-
readonly tableName: string;
|
|
4713
|
-
readonly entity: string;
|
|
4714
|
-
/**
|
|
4715
|
-
* Put: the full item template (field → template / typed literal). A string
|
|
4716
|
-
* value is a template ({@link TransactionValueLeaf}); a `number` / `boolean` is
|
|
4717
|
-
* a typed literal carried verbatim (issue #245).
|
|
4718
|
-
*/
|
|
4719
|
-
readonly item?: Readonly<Record<string, TransactionValueLeaf>>;
|
|
4720
|
-
/** Update / Delete / ConditionCheck: the key template (attribute → template). */
|
|
4721
|
-
readonly keyCondition?: Readonly<Record<string, string>>;
|
|
4722
|
-
/**
|
|
4723
|
-
* Update: field-level `SET` change templates (overwrite). A string value is a
|
|
4724
|
-
* template; a `number` / `boolean` is a typed literal carried verbatim (#245).
|
|
4725
|
-
*/
|
|
4726
|
-
readonly changes?: Readonly<Record<string, TransactionValueLeaf>>;
|
|
4727
|
-
/**
|
|
4728
|
-
* Update: field-level atomic `ADD` deltas (issue #85, derived counters). Each
|
|
4729
|
-
* value is a numeric template (`{param}` / a literal number string) or a typed
|
|
4730
|
-
* numeric literal (issue #245); the runtime applies `ADD #field :delta`, an
|
|
4731
|
-
* atomic increment that needs no prior read and is safe under concurrency. A
|
|
4732
|
-
* negative delta decrements (e.g. `-1` on a remove).
|
|
4733
|
-
*/
|
|
4734
|
-
readonly add?: Readonly<Record<string, TransactionValueLeaf>>;
|
|
4735
|
-
/**
|
|
4736
|
-
* The write / assertion condition (subset). Optional on `Put` / `Update` /
|
|
4737
|
-
* `Delete`; **required** on a `ConditionCheck` (the assertion it makes).
|
|
4738
|
-
*/
|
|
4739
|
-
readonly condition?: ConditionSpec;
|
|
4740
|
-
/** Optional declarative guard; the item is skipped when it does not hold. */
|
|
4741
|
-
readonly when?: WhenSpec;
|
|
4742
|
-
/** When present, expand this item once per element of the named array param. */
|
|
4743
|
-
readonly forEach?: {
|
|
4744
|
-
readonly source: string;
|
|
4745
|
-
};
|
|
4746
|
-
/**
|
|
4747
|
-
* Marks a **raw marker-row** write (issue #86 uniqueness guard): the row is NOT a
|
|
4748
|
-
* modeled entity, so its primary key is carried **literally** rather than derived
|
|
4749
|
-
* from manifest metadata. When `true`:
|
|
4750
|
-
*
|
|
4751
|
-
* - a `Put` writes its `item` record **verbatim** as the stored item — the `item`
|
|
4752
|
-
* already carries the synthetic `PK` / `SK` templates (`UNIQUE#…`), so the
|
|
4753
|
-
* runtimes do NOT prepend a model prefix or compute GSI attributes;
|
|
4754
|
-
* - a `Delete` uses its `keyCondition` (the `PK` / `SK` templates) as the row Key
|
|
4755
|
-
* verbatim.
|
|
4756
|
-
*
|
|
4757
|
-
* The companion {@link entity} is the {@link MARKER_ROW_ENTITY} sentinel (there is
|
|
4758
|
-
* no manifest entity to resolve). Absent / `false` on every modeled write (a base
|
|
4759
|
-
* entity / edge / counter / ConditionCheck item), so those paths are unchanged.
|
|
4760
|
-
*/
|
|
4761
|
-
readonly literalKey?: boolean;
|
|
4762
|
-
/**
|
|
4763
|
-
* A **relation-side maintenance write** (Epic #118, issue #129 — the JSON-SSoT /
|
|
4764
|
-
* Python mirror of the TS in-process `renderMaintainWriteItem`). Present ONLY on an
|
|
4765
|
-
* `Update` item that materializes a maintained access path: a projected
|
|
4766
|
-
* {@link DerivedMaintainWrite} of a just-written source row into a SEPARATE owner
|
|
4767
|
-
* (destination) row, in the SAME atomic transaction as the source write.
|
|
4768
|
-
*
|
|
4769
|
-
* The item's {@link keyCondition} carries the owner row's key templates (bound from
|
|
4770
|
-
* the source payload — `{sourceInputField}`), so the same key derivation / collapse
|
|
4771
|
-
* signature the other `Update` items use applies unchanged. This `maintain` payload
|
|
4772
|
-
* carries the projection transform IR (`identity` / `preview`) the runtimes apply
|
|
4773
|
-
* IDENTICALLY when rendering the `SET … = :v` (snapshot) / `list_append(…)`
|
|
4774
|
-
* (collection) `UpdateExpression`, so the maintained row is byte-consistent across
|
|
4775
|
-
* TS and Python. The `changes` / `add` fields are NOT used when this is present.
|
|
4776
|
-
*/
|
|
4777
|
-
readonly maintain?: MaintainItemSpec;
|
|
4778
|
-
}
|
|
4779
|
-
/**
|
|
4780
|
-
* The transform op applied to one projected maintenance attribute (Epic #118).
|
|
4781
|
-
* Mirrors `ProjectionTransformOp` from `src/define/entity-writes.ts`: `identity`
|
|
4782
|
-
* copies the source value through unchanged; `preview` keeps the first `n`
|
|
4783
|
-
* characters of (the string form of) the source value. The runtimes apply this
|
|
4784
|
-
* IDENTICALLY so the maintained row is byte-consistent.
|
|
4785
|
-
*/
|
|
4786
|
-
type MaintainProjectionOp = 'identity' | 'preview';
|
|
4787
|
-
/**
|
|
4788
|
-
* One projected maintenance attribute: the source value is read from the mutation
|
|
4789
|
-
* input field {@link inputField} (payload 同梱 — the just-written source row image),
|
|
4790
|
-
* then run through {@link op} with {@link args} (e.g. `preview`'s length bound).
|
|
4791
|
-
*/
|
|
4792
|
-
interface MaintainProjectionEntry {
|
|
4793
|
-
/** The transform op applied to the source value (`identity` / `preview`). */
|
|
4794
|
-
readonly op: MaintainProjectionOp;
|
|
4795
|
-
/** The op's positional arguments (`[n]` for `preview`; empty for `identity`). */
|
|
4796
|
-
readonly args: readonly unknown[];
|
|
4797
|
-
/** The mutation-input field the source value is read from (`{inputField}`). */
|
|
4798
|
-
readonly inputField: string;
|
|
4799
|
-
}
|
|
4800
|
-
/**
|
|
4801
|
-
* The bounded-collection options a `collection` maintenance write carries. Phase 1
|
|
4802
|
-
* is **append-only**: `maxItems` / `orderBy` are recorded for the future async trim
|
|
4803
|
-
* (#130) but NOT applied synchronously — a single `UpdateExpression` cannot
|
|
4804
|
-
* read-modify-write a bounded/ordered list.
|
|
4805
|
-
*/
|
|
4806
|
-
interface MaintainCollectionSpec {
|
|
4807
|
-
/** The target attribute that holds the maintained collection. */
|
|
4808
|
-
readonly field: string;
|
|
4809
|
-
/** Cap on the collection size (recorded for #130; not trimmed in Phase 1). */
|
|
4810
|
-
readonly maxItems?: number;
|
|
4811
|
-
/** The mutation-input field the items are ordered by (recorded for #130). */
|
|
4812
|
-
readonly orderBy?: string;
|
|
4813
|
-
/** The direction `orderBy` sorts by, from the relation's `read.order` (recorded for #130). */
|
|
4814
|
-
readonly orderDir?: 'ASC' | 'DESC';
|
|
4815
|
-
}
|
|
4816
|
-
/**
|
|
4817
|
-
* The maintenance payload an `Update` {@link TransactionItemSpec} carries when it
|
|
4818
|
-
* materializes a maintained access path (Epic #118 / #129). A `snapshot` mirrors
|
|
4819
|
-
* the projection onto the owner row via `SET`; a `collection` appends the projection
|
|
4820
|
-
* as an item into the bounded list named by {@link collection} via `list_append`.
|
|
4821
|
-
*/
|
|
4822
|
-
interface MaintainItemSpec {
|
|
4823
|
-
/**
|
|
4824
|
-
* Whether the write mirrors a single row (`snapshot`), appends a collection item
|
|
4825
|
-
* (`collection`), or applies a scalar `ADD` counter (`counter`, Epic #118 / #141).
|
|
4826
|
-
*/
|
|
4827
|
-
readonly kind: 'snapshot' | 'collection' | 'counter';
|
|
4828
|
-
/** The relation property / `@aggregate` field that declared this maintenance (documentary). */
|
|
4829
|
-
readonly relationProperty: string;
|
|
4830
|
-
/** The trigger (`<sourceLogicalName>.<event>`) that fired this maintenance. */
|
|
4831
|
-
readonly trigger: string;
|
|
4832
|
-
/** target attribute → its projection transform (source field + op + args). Empty for `counter`. */
|
|
4833
|
-
readonly projection: Readonly<Record<string, MaintainProjectionEntry>>;
|
|
4834
|
-
/** The bounded-collection options; present only for `kind: 'collection'`. */
|
|
4835
|
-
readonly collection?: MaintainCollectionSpec;
|
|
4836
|
-
/**
|
|
4837
|
-
* The scalar counter `ADD`; present only for `kind: 'counter'` (#141). The runtimes
|
|
4838
|
-
* render `ADD #attr :delta` — an atomic, concurrency-safe increment that MERGES with
|
|
4839
|
-
* a same-row counter ADD (#92), the maintenance-pipeline analogue of the
|
|
4840
|
-
* self-lifecycle derived counter's `add` slot. `delta` is a numeric template string
|
|
4841
|
-
* (the compile-time constant `+1` created / `-1` removed).
|
|
4842
|
-
*/
|
|
4843
|
-
readonly counter?: MaintainCounterSpec;
|
|
4844
|
-
}
|
|
4845
|
-
/**
|
|
4846
|
-
* The scalar counter a `counter` maintenance write applies (Epic #118 / #141): an
|
|
4847
|
-
* atomic `ADD #attribute :delta`. `delta` is a literal numeric template string (the
|
|
4848
|
-
* compile-time constant the trigger fixed — `"1"` created / `"-1"` removed); the
|
|
4849
|
-
* runtimes serialize it as a number. Only `count()` is realized synchronously
|
|
4850
|
-
* (`max(field)` is rejected for the synchronous path — #130 / Phase 2).
|
|
4851
|
-
*/
|
|
4852
|
-
interface MaintainCounterSpec {
|
|
4853
|
-
/** The target attribute the counter increments (e.g. `postCount`). */
|
|
4854
|
-
readonly attribute: string;
|
|
4855
|
-
/** The signed `ADD` delta as a numeric template string (`"1"` / `"-1"`). */
|
|
4856
|
-
readonly delta: string;
|
|
4857
|
-
}
|
|
4858
|
-
/** A declarative transaction definition's full execution spec. */
|
|
4859
|
-
interface TransactionSpec {
|
|
4860
|
-
readonly params: Readonly<Record<string, ParamSpec>>;
|
|
4861
|
-
readonly items: readonly TransactionItemSpec[];
|
|
4862
|
-
/**
|
|
4863
|
-
* A static upper bound on the expanded item count when computable (no `forEach`
|
|
4864
|
-
* present → the exact count; with `forEach` it is left absent because the
|
|
4865
|
-
* element count is only known at execution — the runtime then enforces the
|
|
4866
|
-
* DynamoDB ≤25 limit after expansion).
|
|
4867
|
-
*/
|
|
4868
|
-
readonly maxItems?: number;
|
|
4869
|
-
}
|
|
4870
|
-
/**
|
|
4871
|
-
* Whether a contract is a public **read** (`'query'`) or **write** (`'command'`)
|
|
4872
|
-
* interface. Mirrors {@link QueryModelContract} / {@link CommandModelContract}'s
|
|
4873
|
-
* `kind` discriminant from the Contract IR (#58).
|
|
4874
|
-
*/
|
|
4875
|
-
type ContractKind = 'query' | 'command';
|
|
4876
|
-
/**
|
|
4877
|
-
* The resolution kind of a contract query method, **derived** by the planner from
|
|
4878
|
-
* the internal op (never hand-written; see proposal "N+1 Safety"):
|
|
4879
|
-
*
|
|
4880
|
-
* - `'point'` — target keys are known (unique-key `query` / `GetItem`); a key
|
|
4881
|
-
* array coalesces to one `BatchGetItem`.
|
|
4882
|
-
* - `'range'` — target key set is unknown (partition `list` / `Query`); one
|
|
4883
|
-
* request per partition key, so it is only ever safe for a single key.
|
|
4884
|
-
*/
|
|
4885
|
-
type ContractResolution = 'point' | 'range';
|
|
4886
|
-
/**
|
|
4887
|
-
* What input arity a contract method accepts, **derived** from its resolution:
|
|
4888
|
-
*
|
|
4889
|
-
* - `'either'` — a single key **or** an array (a `point` read / a known-key
|
|
4890
|
-
* write; the array form is one `BatchGetItem` / batched write).
|
|
4891
|
-
* - `'single'` — a single key only (a `range` read; an array would be an N+1
|
|
4892
|
-
* fan-out and is rejected by construction).
|
|
4893
|
-
* - `'array'` — an array only (reserved; not produced by the current resolvers).
|
|
4894
|
-
*/
|
|
4895
|
-
type ContractInputArity = 'single' | 'array' | 'either';
|
|
4896
|
-
/**
|
|
4897
|
-
* The per-key result cardinality of a contract query method, **derived** from the
|
|
4898
|
-
* internal op kind (proposal "Cardinality matrix"):
|
|
4899
|
-
*
|
|
4900
|
-
* - `'one'` — at most one item per key (a `point` read).
|
|
4901
|
-
* - `'many'` — a connection (`{ items, cursor }`) per key (a `range` read).
|
|
4902
|
-
*
|
|
4903
|
-
* This is the per-key shape; the input arity (single vs. array) then decides
|
|
4904
|
-
* whether the overall result is bare or keyed.
|
|
4905
|
-
*/
|
|
4906
|
-
type ContractCardinality = 'one' | 'many';
|
|
4907
|
-
/**
|
|
4908
|
-
* The category of a contract command method's declared result, part of the
|
|
4909
|
-
* contract (it surfaces in OpenAPI and every binding; proposal "Return values"):
|
|
4910
|
-
*
|
|
4911
|
-
* - `'void'` — fire-and-forget write (no body).
|
|
4912
|
-
* - `'result'` — an outcome / status object (e.g. `{ ok, version }`).
|
|
4913
|
-
* - `'entity'` — the updated entity (the post-write projection).
|
|
4914
|
-
*/
|
|
4915
|
-
type ContractCommandResult = 'void' | 'result' | 'entity';
|
|
4916
|
-
/** The Key of a contract: the field names that compose its access / join key. */
|
|
4917
|
-
interface ContractKeySpec {
|
|
4918
|
-
/** The key field names, in declaration order (e.g. `["articleId"]`). */
|
|
4919
|
-
readonly fields: readonly string[];
|
|
4920
|
-
}
|
|
4921
|
-
/**
|
|
4922
|
-
* One External Query (Query Composition) binding on a contract query method
|
|
4923
|
-
* (proposal "External Query"). It is a **build-time, in-process** read dependency
|
|
4924
|
-
* on another contract referenced by name in the same SSoT — there is no protocol
|
|
4925
|
-
* or transport. The runtime collects every parent key produced at this level and
|
|
4926
|
-
* resolves the referenced contract **once, batched**, for all of them.
|
|
4927
|
-
*
|
|
4928
|
-
* A composed child MUST be `point` (proposal "N+1 Safety"): the parent step may
|
|
4929
|
-
* yield N records, so a `range` child would be an N+1 fan-out.
|
|
4930
|
-
*/
|
|
4931
|
-
interface ComposeSpec {
|
|
4932
|
-
/** The result property the composed value is attached to (e.g. `billing`). */
|
|
4933
|
-
readonly as: string;
|
|
4934
|
-
/** The referenced contract name (a symbol in the same SSoT). */
|
|
4935
|
-
readonly contract: string;
|
|
4936
|
-
/** The referenced method on that contract (e.g. `get`). */
|
|
4937
|
-
readonly method: string;
|
|
4938
|
-
/**
|
|
4939
|
-
* An optional **logical** owner label (the bounded context that owns the
|
|
4940
|
-
* referenced contract) — not a network address. Omitted when unlabeled.
|
|
4941
|
-
*/
|
|
4942
|
-
readonly context?: string;
|
|
4943
|
-
/**
|
|
4944
|
-
* The child-key binding: child key field → a `from` source path on the parent
|
|
4945
|
-
* result, e.g. `{ accountId: "$.billingAccountId" }`.
|
|
4946
|
-
*/
|
|
4947
|
-
readonly bind: Readonly<Record<string, string>>;
|
|
4948
|
-
/**
|
|
4949
|
-
* The composed child's resolution. It MUST be `'point'` in a valid contract — a
|
|
4950
|
-
* composed child coalesces to one `BatchGetItem` across all parent keys, so a
|
|
4951
|
-
* `range` child (a partition `Query` that cannot be batched across the parent's
|
|
4952
|
-
* keys) is an N+1 fan-out. The N+1 static checker (#60,
|
|
4953
|
-
* {@link assertContractN1Safe}) rejects a `'range'` value on the build path; the
|
|
4954
|
-
* field is widened to {@link ContractResolution} so the offending node can be
|
|
4955
|
-
* carried into the assembled {@link ContractSpec} for the checker to inspect and
|
|
4956
|
-
* reject with a clear message, rather than being silently unrepresentable.
|
|
4957
|
-
*/
|
|
4958
|
-
readonly resolution: ContractResolution;
|
|
4959
|
-
/** The composed child's per-key cardinality (`'one'` for a valid `point`). */
|
|
4960
|
-
readonly cardinality: ContractCardinality;
|
|
4961
|
-
}
|
|
4962
|
-
/**
|
|
4963
|
-
* The **composition execution plan** for one query contract method (issue #70a),
|
|
4964
|
-
* the contract-layer mirror of {@link ExecutionPlanSpec}. It records how a method's
|
|
4965
|
-
* External Query compositions (`compose[]`) are staged relative to the parent read
|
|
4966
|
-
* and to one another, and the batching discipline each composed child obeys:
|
|
4967
|
-
*
|
|
4968
|
-
* - `stages` is an ordered list whose entries are indices into the method's
|
|
4969
|
-
* `compose[]`. The parent read (the referenced `operation`) is the implicit
|
|
4970
|
-
* stage before all of these. Every compose child binds its child key purely from
|
|
4971
|
-
* the **parent** result (`from('$.…')` paths), so sibling compositions are
|
|
4972
|
-
* mutually independent: they form a single stage `[0, 1, …]` and may be resolved
|
|
4973
|
-
* concurrently. (A composed child cannot, today, bind from another composed
|
|
4974
|
-
* child — the #63 DSL only exposes parent `from` paths — so there is never a
|
|
4975
|
-
* cross-composition dependency; the field is a list of stages so the shape can
|
|
4976
|
-
* carry one if a future primitive introduces it.)
|
|
4977
|
-
* - `concurrency` is the declared in-flight bound applied **within** a stage (the
|
|
4978
|
-
* shared {@link RELATION_TRAVERSAL_CONCURRENCY} = 16).
|
|
4979
|
-
* - `batchChunkSize` is the per-request key cap for a composed child read (100,
|
|
4980
|
-
* the DynamoDB `BatchGetItem` limit): a composition resolves **one batched
|
|
4981
|
-
* call** across all parent records (chunked at this size), never an N+1 fan-out
|
|
4982
|
-
* (the composed child is `point`, enforced by the #60 N+1 checker).
|
|
4983
|
-
*
|
|
4984
|
-
* Emitted only when the method declares at least one composition. A method without
|
|
4985
|
-
* compositions carries no plan, and a runtime without one resolves any
|
|
4986
|
-
* compositions sequentially (the pre-#70 behavior) — so an absent plan never
|
|
4987
|
-
* changes results.
|
|
4988
|
-
*/
|
|
4989
|
-
interface CompositionPlanSpec {
|
|
4990
|
-
/** Ordered stages of indices into `compose[]` (independent siblings share a stage). */
|
|
4991
|
-
readonly stages: readonly (readonly number[])[];
|
|
4992
|
-
/** The declared in-flight bound applied within each composition stage (16). */
|
|
4993
|
-
readonly concurrency: number;
|
|
4994
|
-
/** Per-request key cap for a composed child's batched read (100, BatchGetItem limit). */
|
|
4995
|
-
readonly batchChunkSize: number;
|
|
4996
|
-
}
|
|
4997
|
-
/**
|
|
4998
|
-
* The serialized spec of one **query** contract method. Carries the decided facts
|
|
4999
|
-
* (`resolution` / `inputArity` / `cardinality`) the planner derived in #58, a
|
|
5000
|
-
* reference into `queries` by name (the internal op), and any External Query
|
|
5001
|
-
* compositions. The decided facts are **serialized, not re-derived** — every
|
|
5002
|
-
* runtime honors them.
|
|
5003
|
-
*/
|
|
5004
|
-
interface QueryContractMethodSpec {
|
|
5005
|
-
/** Derived: `'point'` (unique-key query) or `'range'` (partition list). */
|
|
5006
|
-
readonly resolution: ContractResolution;
|
|
5007
|
-
/** Derived: `'either'` for `point`, `'single'` for `range`. */
|
|
5008
|
-
readonly inputArity: ContractInputArity;
|
|
5009
|
-
/** Derived per-key result shape: `'one'` (point) or `'many'` (range). */
|
|
5010
|
-
readonly cardinality: ContractCardinality;
|
|
5011
|
-
/** The referenced read op name in `queries` (e.g. `ArticleById__get`). */
|
|
5012
|
-
readonly operation: string;
|
|
5013
|
-
/** External Query compositions on this method; omitted when there are none. */
|
|
5014
|
-
readonly compose?: readonly ComposeSpec[];
|
|
5015
|
-
/**
|
|
5016
|
-
* The composition staging + batch plan (issue #70a); present only when `compose`
|
|
5017
|
-
* is. See {@link CompositionPlanSpec}: independent compositions form one
|
|
5018
|
-
* concurrency-eligible stage after the parent read, each resolved as one batched
|
|
5019
|
-
* call (chunk ≤100). Absent → resolve compositions sequentially (pre-#70).
|
|
5020
|
-
*/
|
|
5021
|
-
readonly compositionPlan?: CompositionPlanSpec;
|
|
5022
|
-
/**
|
|
5023
|
-
* Optional human-readable description of the read use case (issue #154), from the
|
|
5024
|
-
* descriptor's `description`. Pure documentation — absent unless declared, so a
|
|
5025
|
-
* method with no description serializes byte-identically to the pre-#154 spec.
|
|
5026
|
-
* The same string appears in the TS contract IR and (via the SSoT) in any
|
|
5027
|
-
* generated binding, so TS↔Python carry an identical description.
|
|
5028
|
-
*/
|
|
5029
|
-
readonly description?: string;
|
|
5030
|
-
}
|
|
5031
|
-
/**
|
|
5032
|
-
* How a contract command method resolves a single key vs. an array of keys to the
|
|
5033
|
-
* underlying write surface (proposal "Consistency with the existing write
|
|
5034
|
-
* surface"). A single key → one write op (or one transaction); an array → a
|
|
5035
|
-
* batched write — a `TransactWriteItems` when atomicity / per-item conditions are
|
|
5036
|
-
* required, or a `BatchWriteItem` when neither is.
|
|
5037
|
-
*/
|
|
5038
|
-
type CommandResolutionTarget =
|
|
5039
|
-
/** One write op, referenced by name in `commands`. */
|
|
5040
|
-
{
|
|
5041
|
-
readonly mode: 'op';
|
|
5042
|
-
readonly operation: string;
|
|
5043
|
-
}
|
|
5044
|
-
/** One transaction, referenced by name in `transactions`. */
|
|
5045
|
-
| {
|
|
5046
|
-
readonly mode: 'transaction';
|
|
5047
|
-
readonly transaction: string;
|
|
5048
|
-
}
|
|
5049
|
-
/**
|
|
5050
|
-
* A non-atomic **parallel** fan-out over the per-key write op (#101). The op
|
|
5051
|
-
* MAY carry a condition (guarded create / conditioned update). Unconditioned
|
|
5052
|
-
* `put`/`delete` ops coalesce into a `BatchWriteItem` (with `UnprocessedItems`
|
|
5053
|
-
* retry); conditioned/update ops issue individual conditional writes
|
|
5054
|
-
* concurrently. Per-op success/failure is reported (partial success); a
|
|
5055
|
-
* per-op failure never aborts the others. References the per-key write op in
|
|
5056
|
-
* `commands`.
|
|
5057
|
-
*/
|
|
5058
|
-
| {
|
|
5059
|
-
readonly mode: 'parallel';
|
|
5060
|
-
readonly operation: string;
|
|
5061
|
-
};
|
|
5062
|
-
/**
|
|
5063
|
-
* The serialized spec of one **command** contract method. Symmetric to
|
|
5064
|
-
* {@link QueryContractMethodSpec}: it carries the declared `result` type and the
|
|
5065
|
-
* `single` / `batch` resolution targets into the existing write specs.
|
|
5066
|
-
*/
|
|
5067
|
-
interface CommandContractMethodSpec {
|
|
5068
|
-
/** Derived input arity: writes accept `'either'` (single key or key array). */
|
|
5069
|
-
readonly inputArity: ContractInputArity;
|
|
5070
|
-
/** The declared result type (`void` | a `Result` | the updated entity). */
|
|
5071
|
-
readonly result: ContractCommandResult;
|
|
5072
|
-
/** How a single key resolves (one op / one transaction). */
|
|
5073
|
-
readonly single: CommandResolutionTarget;
|
|
5074
|
-
/**
|
|
5075
|
-
* How an array of keys resolves (a transaction / `BatchWriteItem`); omitted
|
|
5076
|
-
* when the contract does not declare a batched write form.
|
|
5077
|
-
*/
|
|
5078
|
-
readonly batch?: CommandResolutionTarget;
|
|
5079
|
-
/**
|
|
5080
|
-
* The **return projection** of a `mutation`-derived command method (issue #83;
|
|
5081
|
-
* proposal §3, "return selection as a read projection"). A JSON-safe boolean
|
|
5082
|
-
* field map: after the write commits, the runtime issues a **`GetItem` with
|
|
5083
|
-
* `ConsistentRead`** of the written entity's primary key and applies this
|
|
5084
|
-
* projection via the existing read-projection machinery, returning the projected
|
|
5085
|
-
* item. This is the **uniform** return mechanism for both a single-op command
|
|
5086
|
-
* and a future transaction (a `TransactWriteItems` cannot return item images),
|
|
5087
|
-
* so TS and Python return an **identical** projected item.
|
|
5088
|
-
*
|
|
5089
|
-
* The consistent read-back is issued against the read op named
|
|
5090
|
-
* `<Contract>__<method>__readback` (a synthesized {@link QuerySpec}: a `GetItem`
|
|
5091
|
-
* on the written entity's primary key projecting these fields). Present only on
|
|
5092
|
-
* a `.plan(mutation)` method; a hand-written #64 command omits it (its
|
|
5093
|
-
* {@link result} stays `'void'` / `'entity'` and it returns no projected item).
|
|
5094
|
-
*/
|
|
5095
|
-
readonly returnSelection?: Readonly<Record<string, boolean>>;
|
|
5096
|
-
/**
|
|
5097
|
-
* Optional human-readable description of the write use case (issue #154), from
|
|
5098
|
-
* the descriptor's `description`. Pure documentation — absent unless declared, so
|
|
5099
|
-
* a method with no description serializes byte-identically to the pre-#154 spec.
|
|
5100
|
-
*/
|
|
5101
|
-
readonly description?: string;
|
|
5102
|
-
}
|
|
5103
|
-
/**
|
|
5104
|
-
* A serialized contract — a keyed public read (`'query'`) or write (`'command'`)
|
|
5105
|
-
* interface holding one or more named methods. Each method references an existing
|
|
5106
|
-
* operation spec by name and adds the decided contract facts. The existing
|
|
5107
|
-
* operation-spec shapes are unchanged, so the current runtime keeps working.
|
|
5108
|
-
*/
|
|
5109
|
-
interface ContractSpec {
|
|
5110
|
-
/** Whether this is a read (`'query'`) or write (`'command'`) contract. */
|
|
5111
|
-
readonly kind: ContractKind;
|
|
5112
|
-
/** The contract Key (the access pattern / join / batch key). */
|
|
5113
|
-
readonly key: ContractKeySpec;
|
|
5114
|
-
/**
|
|
5115
|
-
* The named methods (use cases over the same Key). A query contract's methods
|
|
5116
|
-
* are {@link QueryContractMethodSpec}; a command contract's are
|
|
5117
|
-
* {@link CommandContractMethodSpec}. The map is keyed by contract `kind`.
|
|
3686
|
+
* string.
|
|
5118
3687
|
*/
|
|
5119
|
-
readonly
|
|
3688
|
+
readonly destinationRowKey: string;
|
|
3689
|
+
/** The A-defined maintenance IR this relation lowers to. */
|
|
3690
|
+
readonly effect: MaintainEffect;
|
|
5120
3691
|
}
|
|
5121
3692
|
/**
|
|
5122
|
-
*
|
|
5123
|
-
*
|
|
5124
|
-
*
|
|
5125
|
-
*
|
|
5126
|
-
*
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
*/
|
|
5132
|
-
|
|
5133
|
-
/**
|
|
5134
|
-
|
|
5135
|
-
/** Contract names owned by this context (sorted; `contracts` map keys). */
|
|
5136
|
-
readonly contracts: readonly string[];
|
|
5137
|
-
}
|
|
5138
|
-
interface OperationsDocument {
|
|
5139
|
-
readonly version: typeof SPEC_VERSION;
|
|
5140
|
-
readonly queries: Readonly<Record<string, QuerySpec>>;
|
|
5141
|
-
readonly commands: Readonly<Record<string, CommandSpec>>;
|
|
5142
|
-
/** Declarative transactions (issue #46). Absent in pre-#46 specs. */
|
|
5143
|
-
readonly transactions?: Readonly<Record<string, TransactionSpec>>;
|
|
5144
|
-
/**
|
|
5145
|
-
* The CQRS Contract layer (issue #59): contract name → {@link ContractSpec}.
|
|
5146
|
-
* Layered **on top of** the existing `queries` / `commands` / `transactions`
|
|
5147
|
-
* (each method references one of them by name). **Absent** when the input
|
|
5148
|
-
* defines no contracts — so a contract-free input produces a byte-identical
|
|
5149
|
-
* pre-#59 operations document (backward compatibility).
|
|
5150
|
-
*/
|
|
5151
|
-
readonly contracts?: Readonly<Record<string, ContractSpec>>;
|
|
3693
|
+
* The maintenance graph: the trigger → effects index plus the validation surface.
|
|
3694
|
+
*
|
|
3695
|
+
* `byTrigger` is the core index: a `MaintainTrigger` (`"Post.created"`) maps to
|
|
3696
|
+
* every {@link MaintainItem} that fires on it, in a stable order (owner entity,
|
|
3697
|
+
* then relation property). Empty (no entry) for a trigger nothing maintains on.
|
|
3698
|
+
*/
|
|
3699
|
+
interface MaintenanceGraph {
|
|
3700
|
+
/** Every maintenance item, flattened, in deterministic order. */
|
|
3701
|
+
readonly items: readonly MaintainItem[];
|
|
3702
|
+
/** The core index: trigger → the effects it fires. */
|
|
3703
|
+
readonly byTrigger: ReadonlyMap<MaintainTrigger, readonly MaintainItem[]>;
|
|
3704
|
+
/** Look up the effects fired by a trigger (empty array if none). */
|
|
3705
|
+
effectsFor(trigger: MaintainTrigger): readonly MaintainItem[];
|
|
5152
3706
|
/**
|
|
5153
|
-
*
|
|
5154
|
-
*
|
|
5155
|
-
*
|
|
3707
|
+
* Items that, under the SAME trigger, write the SAME target row — grouped by
|
|
3708
|
+
* `"<trigger><targetRowKey>"`. Only groups with **more than one** item are
|
|
3709
|
+
* present, so an empty map means no trigger has a multi-maintainer collision.
|
|
3710
|
+
* This is the detection material for 論点2 = b; #124 surfaces it, #125/#126
|
|
3711
|
+
* decide the reject. (Build-time we do NOT reject here — a model may legitimately
|
|
3712
|
+
* declare overlapping shapes that a later phase reconciles; #124's contract is
|
|
3713
|
+
* to make the collision *detectable*.)
|
|
5156
3714
|
*/
|
|
5157
|
-
readonly
|
|
5158
|
-
}
|
|
5159
|
-
/** The full `{ manifest, operations }` bundle produced by the static planner. */
|
|
5160
|
-
interface BridgeBundle {
|
|
5161
|
-
readonly manifest: Manifest;
|
|
5162
|
-
readonly operations: OperationsDocument;
|
|
3715
|
+
readonly multiMaintainerTargets: ReadonlyMap<string, readonly MaintainItem[]>;
|
|
5163
3716
|
}
|
|
5164
3717
|
|
|
5165
3718
|
/**
|
|
@@ -5789,225 +4342,6 @@ interface CompiledMutationPlan {
|
|
|
5789
4342
|
*/
|
|
5790
4343
|
declare function compileMutationPlan(plan: CommandPlan): CompiledMutationPlan;
|
|
5791
4344
|
|
|
5792
|
-
/**
|
|
5793
|
-
* Single-service contract **Runtime** — execution of CQRS query-contract methods
|
|
5794
|
-
* (issue #62, Epic #57; spec `docs/cqrs-contract.md`, "Query Method
|
|
5795
|
-
* — Common Interface" + "Cardinality matrix"). READS only; command/write
|
|
5796
|
-
* execution is #64 and single-contract composition / External Query is #63 — both
|
|
5797
|
-
* out of scope here.
|
|
5798
|
-
*
|
|
5799
|
-
* The Runtime is the **execution engine**: given a resolved query contract (a
|
|
5800
|
-
* {@link QueryModelContract} from #58, carrying per-method `resolution` /
|
|
5801
|
-
* `inputArity` and a resolved {@link ContractMethodOp}) it executes one method
|
|
5802
|
-
* for a single key **or** an array of keys and returns the correct result shape
|
|
5803
|
-
* from the proposal's cardinality matrix:
|
|
5804
|
-
*
|
|
5805
|
-
* | Input | `resolution` | Result shape | Notation |
|
|
5806
|
-
* | -------------- | ------------ | ------------------------------------- | ---------------- |
|
|
5807
|
-
* | `key` (single) | `point` | `Item \| null` | `key1 : result1` |
|
|
5808
|
-
* | `key` (single) | `range` | `Connection` (`{ items, cursor }`) | `key1 : resultM` |
|
|
5809
|
-
* | `keys[]` (N) | `point` | keyed `Map<keyId, Item \| null>` | `keyN : resultN` |
|
|
5810
|
-
* | `keys[]` (N) | `range` | **unreachable for a single contract** | `keyN : resultN×M` |
|
|
5811
|
-
*
|
|
5812
|
-
* ## On `keyN : resultN×M` (range + array) — unreachable here, by design
|
|
5813
|
-
*
|
|
5814
|
-
* The fourth matrix cell — a keyed map of connections — is **not reachable for a
|
|
5815
|
-
* single contract** at the direct-call surface. The proposal's N+1 Safety rule
|
|
5816
|
-
* fixes a `range` method's `inputArity` to `'single'` ("A `range` method is
|
|
5817
|
-
* necessarily `'single'`"; "feeding an array of keys into a range method … is an
|
|
5818
|
-
* N+1 fan-out and is rejected at build time, with no opt-in escape hatch"). #58's
|
|
5819
|
-
* {@link inputArityOf} derives exactly that, and #60's N+1 checker rejects any
|
|
5820
|
-
* contract that violates it before it reaches the SSoT. So a range method only
|
|
5821
|
-
* ever accepts one key, and this runtime **rejects** an array fed into a `range`
|
|
5822
|
-
* method ({@link executeQueryMethod}) rather than fanning out. `keyN : resultN×M`
|
|
5823
|
-
* arises only under cross-contract composition (a `point` parent referencing a
|
|
5824
|
-
* child whose per-key result is a connection) — that is #63's External Query
|
|
5825
|
-
* territory, explicitly out of scope for #62. We therefore implement the **three
|
|
5826
|
-
* reachable shapes** and guard the fourth with a clear error, rather than
|
|
5827
|
-
* silently fanning out a forbidden N+1.
|
|
5828
|
-
*
|
|
5829
|
-
* ## Reuse, not reinvention
|
|
5830
|
-
*
|
|
5831
|
-
* Execution delegates to the proven machinery:
|
|
5832
|
-
*
|
|
5833
|
-
* - `point` single → {@link executeQuery} (GetItem / unique Query).
|
|
5834
|
-
* - `point` array → {@link executeKeyedBatchGet} below, which reuses the
|
|
5835
|
-
* `batch-retry` primitives (chunk ≤100, dedup, UnprocessedKeys retry) and
|
|
5836
|
-
* builds the **keyed** map (BatchGet neither preserves order nor returns absent
|
|
5837
|
-
* keys, so missing keys are filled with explicit `null`).
|
|
5838
|
-
* - `range` single → {@link executeListInternal} for a `{ items, cursor }`
|
|
5839
|
-
* connection, with the cursor wrapped in a per-key envelope
|
|
5840
|
-
* ({@link encodePerKeyCursor}) so it carries the key it belongs to.
|
|
5841
|
-
*
|
|
5842
|
-
* The result-shape typing (single → bare, array → keyed) is expressed as call
|
|
5843
|
-
* overloads on {@link executeQueryMethod}.
|
|
5844
|
-
*/
|
|
5845
|
-
|
|
5846
|
-
/**
|
|
5847
|
-
* The **structural** shape of a resolved query contract the runtime executes.
|
|
5848
|
-
* It depends only on the facts the executor reads — the `query` discriminant and
|
|
5849
|
-
* the per-method `resolution` and internal `op` — and is deliberately
|
|
5850
|
-
* **independent of the phantom Key / params / result types** carried by
|
|
5851
|
-
* {@link QueryModelContract}. Those phantom types are invariant /
|
|
5852
|
-
* contravariant on the full interface, so a concrete `QueryModelContract<{…}>`
|
|
5853
|
-
* is not assignable to `QueryModelContract<unknown, …>`; this minimal structural
|
|
5854
|
-
* type sidesteps that variance while accepting every real contract (a
|
|
5855
|
-
* {@link QueryModelContract} is structurally assignable to it). The runtime never
|
|
5856
|
-
* needs the method param / result types — it returns the dynamic
|
|
5857
|
-
* cardinality-matrix shapes ({@link ContractItem} / {@link Connection} /
|
|
5858
|
-
* {@link KeyedResult}).
|
|
5859
|
-
*/
|
|
5860
|
-
interface ExecutableQueryContract {
|
|
5861
|
-
readonly kind: 'query';
|
|
5862
|
-
readonly methods: Readonly<Record<string, {
|
|
5863
|
-
readonly resolution: Resolution;
|
|
5864
|
-
/**
|
|
5865
|
-
* The method's decided input arity (issue #71): `'either'` for a
|
|
5866
|
-
* coalescible base-table `point` (a key array → one `BatchGetItem`),
|
|
5867
|
-
* `'single'` for a `range` (partition `Query`) **or** a unique-GSI `point`
|
|
5868
|
-
* (a per-key GSI `Query`, not coalescible). The executor honors it to
|
|
5869
|
-
* reject an array fed into any `'single'` method rather than fanning out.
|
|
5870
|
-
*/
|
|
5871
|
-
readonly inputArity: InputArity;
|
|
5872
|
-
readonly op: ContractMethodOp;
|
|
5873
|
-
}>>;
|
|
5874
|
-
}
|
|
5875
|
-
/** A plain contract key (the access pattern / join key), e.g. `{ articleId }`. */
|
|
5876
|
-
type ContractKeyInput = Record<string, unknown>;
|
|
5877
|
-
/**
|
|
5878
|
-
* The literal {@link InputArity} the contract `C` carries for method `K`. A
|
|
5879
|
-
* {@link QueryMethodSpec} carries the method's literal arity in its `inputArity`
|
|
5880
|
-
* type when the body was tagged with `point` / `range` (`src/define/contract.ts`);
|
|
5881
|
-
* an untagged method keeps the wide `InputArity`. Falls back to `InputArity` for a
|
|
5882
|
-
* structurally-typed contract (e.g. the bare {@link ExecutableQueryContract}),
|
|
5883
|
-
* which keeps the call surface array-capable (the runtime backstop still rejects an
|
|
5884
|
-
* array fed into a `'single'` method).
|
|
5885
|
-
*/
|
|
5886
|
-
type ArityOfMethod<C extends ExecutableQueryContract, K extends keyof C['methods']> = C['methods'][K] extends {
|
|
5887
|
-
readonly inputArity: infer A extends InputArity;
|
|
5888
|
-
} ? A : InputArity;
|
|
5889
|
-
/**
|
|
5890
|
-
* The **array** key-argument type accepted by `executeQueryMethod` for method `K`
|
|
5891
|
-
* of contract `C`. A method whose literal arity is `'single'` (a `range` read **or**
|
|
5892
|
-
* a unique-GSI `point` — both one request per key) accepts **no** array: the type
|
|
5893
|
-
* collapses to `never`, so passing an array is a `tsc` compile error at the
|
|
5894
|
-
* user-facing surface (issue #71, N+1 rule (a), the **primary** enforcement layer).
|
|
5895
|
-
* Any other arity (`'either'` / `'array'`, or the wide `InputArity` for an untagged
|
|
5896
|
-
* / structural contract) accepts an array of keys (it coalesces to one batched
|
|
5897
|
-
* call). This is the array-overload half of {@link ContractCallSignature}'s
|
|
5898
|
-
* narrowing, applied at the real execution surface.
|
|
5899
|
-
*/
|
|
5900
|
-
type ArrayKeyArg<C extends ExecutableQueryContract, K extends keyof C['methods']> = ArityOfMethod<C, K> extends 'single' ? never : readonly ContractKeyInput[];
|
|
5901
|
-
/** A single hydrated result item (only the method's projected fields). */
|
|
5902
|
-
type ContractItem = Record<string, unknown>;
|
|
5903
|
-
/**
|
|
5904
|
-
* A pagination connection: a page of items plus an opaque `cursor` for the next
|
|
5905
|
-
* page (`null` when the page is the last). For a `range` contract method the
|
|
5906
|
-
* cursor is a **per-key envelope** ({@link encodePerKeyCursor}) that identifies
|
|
5907
|
-
* the key it paginates.
|
|
5908
|
-
*/
|
|
5909
|
-
interface Connection<TItem = ContractItem> {
|
|
5910
|
-
readonly items: TItem[];
|
|
5911
|
-
readonly cursor: string | null;
|
|
5912
|
-
}
|
|
5913
|
-
/**
|
|
5914
|
-
* The keyed result of a batched contract read: input-key identity (the canonical
|
|
5915
|
-
* {@link serializeContractKey} string) → per-key result. Backed by a `Map` so
|
|
5916
|
-
* the association is explicit and order-independent; missing keys are present
|
|
5917
|
-
* with an explicit `null` value (`point`) rather than omitted.
|
|
5918
|
-
*
|
|
5919
|
-
* @typeParam TValue The per-key value (`Item | null` for `point`).
|
|
5920
|
-
*/
|
|
5921
|
-
type KeyedResult<TValue> = Map<string, TValue>;
|
|
5922
|
-
/**
|
|
5923
|
-
* Retrieval options accepted by a contract query method (the serializable subset
|
|
5924
|
-
* from the proposal). All optional; a `point` method ignores the pagination
|
|
5925
|
-
* fields, a `range` method honors them.
|
|
5926
|
-
*/
|
|
5927
|
-
interface ContractQueryParams {
|
|
5928
|
-
/** Strongly-consistent read (point reads on the base table only). */
|
|
5929
|
-
readonly consistentRead?: boolean;
|
|
5930
|
-
/** Page size for a `range` (`list`) method. */
|
|
5931
|
-
readonly limit?: number;
|
|
5932
|
-
/** Resume token — a per-key cursor envelope from a prior `range` page. */
|
|
5933
|
-
readonly after?: string;
|
|
5934
|
-
/** Sort direction for a `range` (`list`) method. */
|
|
5935
|
-
readonly order?: 'ASC' | 'DESC';
|
|
5936
|
-
}
|
|
5937
|
-
/**
|
|
5938
|
-
* Maximum number of in-flight per-key queries for a batched `range` fan-out.
|
|
5939
|
-
* Reuses the relation-traversal cap — the same rationale (bound in-flight
|
|
5940
|
-
* requests, ride out throttling via UnprocessedKeys retry beneath the cap). Note
|
|
5941
|
-
* that a single-contract `range` method is `inputArity: 'single'`, so the
|
|
5942
|
-
* fan-out path is only exercised by the (out-of-scope, #63) composition runtime;
|
|
5943
|
-
* the bound is defined here so the seam is ready and tested in isolation.
|
|
5944
|
-
*/
|
|
5945
|
-
declare const CONTRACT_RANGE_FANOUT_CONCURRENCY = 16;
|
|
5946
|
-
/**
|
|
5947
|
-
* Keyed BatchGet: resolve `keys` against `modelClass` in one batched call and
|
|
5948
|
-
* return a `Map<keyId, Item | null>` with every input key present (misses →
|
|
5949
|
-
* `null`). Reuses the `batch-retry` primitives (chunk ≤100, dedup, UnprocessedKeys
|
|
5950
|
-
* retry) — never reinvented.
|
|
5951
|
-
*
|
|
5952
|
-
* Implementation notes:
|
|
5953
|
-
* - **Dedup**: the same key supplied twice is fetched once; both map entries
|
|
5954
|
-
* point at the same resolved item (or `null`).
|
|
5955
|
-
* - **Order / absence**: BatchGet returns matched items unordered and omits
|
|
5956
|
-
* misses, so each returned raw item is indexed by its `PK::SK` and looked up
|
|
5957
|
-
* per input key; an unmatched key gets `null`.
|
|
5958
|
-
* - **Projection**: items are fetched whole (DynamoDB BatchGet has no reliable
|
|
5959
|
-
* server-side projection for mixed keys here) and hydrated against the
|
|
5960
|
-
* method's `select`, so the per-key item carries exactly the projected fields.
|
|
5961
|
-
*/
|
|
5962
|
-
declare function executeKeyedBatchGet(modelClass: Function, keys: readonly ContractKeyInput[], select: Record<string, unknown>): Promise<KeyedResult<ContractItem | null>>;
|
|
5963
|
-
/**
|
|
5964
|
-
* Execute a query-contract method for a **single** key.
|
|
5965
|
-
*
|
|
5966
|
-
* The result shape is determined by the method's derived `resolution`:
|
|
5967
|
-
* - `point` → `Item | null` (`key1 : result1`);
|
|
5968
|
-
* - `range` → `Connection` (`key1 : resultM`).
|
|
5969
|
-
*
|
|
5970
|
-
* A single key is accepted for **every** method regardless of arity.
|
|
5971
|
-
*
|
|
5972
|
-
* @param contract The resolved query contract (a {@link QueryModelContract}).
|
|
5973
|
-
* @param methodName The method to execute (a method name of `contract`).
|
|
5974
|
-
* @param key A single contract key.
|
|
5975
|
-
* @param params Retrieval options (pagination / consistency).
|
|
5976
|
-
*/
|
|
5977
|
-
declare function executeQueryMethod<C extends ExecutableQueryContract, K extends keyof C['methods'] & string>(contract: C, methodName: K, key: ContractKeyInput, params?: ContractQueryParams): Promise<ContractItem | null | Connection>;
|
|
5978
|
-
/**
|
|
5979
|
-
* Execute a query-contract method for an **array** of keys (a single batched
|
|
5980
|
-
* call). Valid **only** for a method whose literal arity is not `'single'` — a
|
|
5981
|
-
* coalescible base-table `point` (`inputArity: 'either'`, a key array → one
|
|
5982
|
-
* `BatchGetItem`). A `'single'` method (a `range` read **or** a unique-GSI `point`)
|
|
5983
|
-
* makes the array key argument `never`, so passing an array is a **`tsc` compile
|
|
5984
|
-
* error** by construction (issue #71, N+1 rule (a) — the primary enforcement
|
|
5985
|
-
* layer; the build-time checker and runtime backstop remain). The result is a keyed
|
|
5986
|
-
* `Map<keyId, Item | null>` (`keyN : resultN`).
|
|
5987
|
-
*
|
|
5988
|
-
* @param contract The resolved query contract.
|
|
5989
|
-
* @param methodName The method to execute (a method name of `contract`).
|
|
5990
|
-
* @param keys An array of contract keys (rejected for a `'single'` method).
|
|
5991
|
-
* @param params Retrieval options.
|
|
5992
|
-
*/
|
|
5993
|
-
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>>;
|
|
5994
|
-
/**
|
|
5995
|
-
* The runtime fan-out seam for a batched `range` (`keyN : resultN×M`). NOT
|
|
5996
|
-
* reachable for a single contract under #62 (a `range` method's `inputArity` is
|
|
5997
|
-
* `'single'`, so {@link executeQueryMethod} rejects an array). It is the
|
|
5998
|
-
* cross-contract composition runtime (#63) that drives a per-key range fan-out;
|
|
5999
|
-
* this function implements that fan-out — N bounded-parallel Queries grouped by
|
|
6000
|
-
* input key, each connection carrying its own per-key cursor — so the seam is
|
|
6001
|
-
* defined, reused (via {@link mapWithConcurrency}), and unit-tested in isolation
|
|
6002
|
-
* ahead of #63, rather than left as a stub. #62 does not call it on any contract
|
|
6003
|
-
* surface.
|
|
6004
|
-
*
|
|
6005
|
-
* @param op The `range` method op to run per key.
|
|
6006
|
-
* @param keys The parent keys to fan out over.
|
|
6007
|
-
* @param params Retrieval options applied to **every** key's connection.
|
|
6008
|
-
*/
|
|
6009
|
-
declare function executeRangeFanout(op: ContractMethodOp, keys: readonly ContractKeyInput[], params?: ContractQueryParams): Promise<KeyedResult<Connection>>;
|
|
6010
|
-
|
|
6011
4345
|
/**
|
|
6012
4346
|
* Single-service contract **Runtime** — execution of CQRS command-contract
|
|
6013
4347
|
* methods (issue #64, Epic #57; spec `docs/cqrs-contract.md`,
|
|
@@ -6060,188 +4394,12 @@ declare function executeRangeFanout(op: ContractMethodOp, keys: readonly Contrac
|
|
|
6060
4394
|
* The TS and Python runtimes produce identical effects for the same SSoT.
|
|
6061
4395
|
*/
|
|
6062
4396
|
|
|
6063
|
-
/**
|
|
6064
|
-
* The **structural** shape of a resolved command contract the runtime executes.
|
|
6065
|
-
* It depends only on the facts the executor reads — the `command` discriminant
|
|
6066
|
-
* and the per-method `op` / `batch` / `result` — and is deliberately
|
|
6067
|
-
* **independent of the phantom Key / params / result types** carried by
|
|
6068
|
-
* {@link CommandModelContract} (those are invariant on the full interface, so a
|
|
6069
|
-
* concrete `CommandModelContract<{…}>` is not assignable to
|
|
6070
|
-
* `CommandModelContract<unknown, …>`). Every real command contract is
|
|
6071
|
-
* structurally assignable to this minimal type.
|
|
6072
|
-
*/
|
|
6073
|
-
interface ExecutableCommandContract {
|
|
6074
|
-
readonly kind: 'command';
|
|
6075
|
-
readonly methods: Readonly<Record<string, {
|
|
6076
|
-
readonly op: ContractMethodOp;
|
|
6077
|
-
readonly result: CommandResultKind;
|
|
6078
|
-
/**
|
|
6079
|
-
* The #101 first-class per-call execution mode (`'transaction'` |
|
|
6080
|
-
* `'parallel'`). Drives the key-array bulk form directly (`'transaction'`
|
|
6081
|
-
* → atomic `TransactWriteItems`; `'parallel'` → non-atomic per-key fan-out
|
|
6082
|
-
* with partial success, coalescing unconditioned put/delete into a
|
|
6083
|
-
* `BatchWriteItem`).
|
|
6084
|
-
*/
|
|
6085
|
-
readonly mode?: CommandMode;
|
|
6086
|
-
/**
|
|
6087
|
-
* The composed write ops of a **multi-fragment** `mutation`-derived method
|
|
6088
|
-
* (issue #90). When present (N≥2), a **single-key** call executes ALL of
|
|
6089
|
-
* them as **one atomic `TransactWriteItems`** (not sequential writes), so a
|
|
6090
|
-
* condition failure on any fragment rolls back the whole transaction. Absent
|
|
6091
|
-
* for a single-fragment / #83 / hand-written #64 method (which executes its
|
|
6092
|
-
* one {@link op}). The read-back stays keyed on the primary op ({@link op}).
|
|
6093
|
-
*/
|
|
6094
|
-
readonly ops?: readonly ContractMethodOp[];
|
|
6095
|
-
/**
|
|
6096
|
-
* The referential-integrity assertions derived from the mutation's
|
|
6097
|
-
* `requires` effects (issue #84). When present (non-empty), a
|
|
6098
|
-
* **single-key** call composes the base write(s) **and** one read-only
|
|
6099
|
-
* `ConditionCheck` (`attribute_exists`) per assertion into **one atomic
|
|
6100
|
-
* `TransactWriteItems`** — so a single-op write is **promoted** to a
|
|
6101
|
-
* transaction. If any referenced entity is absent, its assertion fails and
|
|
6102
|
-
* the WHOLE transaction rolls back (the entity write never persists). Absent
|
|
6103
|
-
* on a mutation with no `requires` / a #64 hand-written method.
|
|
6104
|
-
*/
|
|
6105
|
-
readonly conditionChecks?: readonly DerivedConditionCheck[];
|
|
6106
|
-
/**
|
|
6107
|
-
* The adjacency edge writes derived from the mutation's `edges` effects
|
|
6108
|
-
* (issue #85). When present, a **single-key** call composes each edge's
|
|
6109
|
-
* adjacency `Put` / `Delete` item(s) into the **same atomic
|
|
6110
|
-
* `TransactWriteItems`** as the entity write (and any ConditionChecks /
|
|
6111
|
-
* derived updates), so the edge is created / removed / moved atomically with
|
|
6112
|
-
* the entity. Absent on a mutation with no `edges` / a #64 hand-written method.
|
|
6113
|
-
*/
|
|
6114
|
-
readonly edgeWrites?: readonly DerivedEdgeWrite[];
|
|
6115
|
-
/**
|
|
6116
|
-
* The derived counter updates from the mutation's `derive` effects (issue
|
|
6117
|
-
* #85): each an atomic `ADD` (`UpdateItem`) on a target counter (e.g.
|
|
6118
|
-
* `User.postCount += 1`). When present, a **single-key** call composes each
|
|
6119
|
-
* `ADD` into the same atomic `TransactWriteItems` as the entity write, so the
|
|
6120
|
-
* counter moves atomically (and never clobbers a concurrent increment — an
|
|
6121
|
-
* `ADD` needs no prior read). Absent on a mutation with no `derive`.
|
|
6122
|
-
*/
|
|
6123
|
-
readonly derivedUpdates?: readonly DerivedUpdate[];
|
|
6124
|
-
/**
|
|
6125
|
-
* The uniqueness guards derived from the mutation's `unique` effects (issue
|
|
6126
|
-
* #86): each a marker-row `Put` (`attribute_not_exists`) / `Delete` /
|
|
6127
|
-
* `Delete(old)+Put(new)` swap. When present, a **single-key** call composes
|
|
6128
|
-
* the guard item(s) into the same atomic `TransactWriteItems` as the entity
|
|
6129
|
-
* write, so a duplicate scoped value (the guard `Put`'s `attribute_not_exists`
|
|
6130
|
-
* failing) rolls the WHOLE transaction back — the entity is never created.
|
|
6131
|
-
* Absent on a mutation with no `unique` / a #64 hand-written method.
|
|
6132
|
-
*/
|
|
6133
|
-
readonly uniqueGuards?: readonly DerivedUniqueGuard[];
|
|
6134
|
-
/**
|
|
6135
|
-
* The outbox events derived from the mutation's `emits` effects (issue #87):
|
|
6136
|
-
* each a transactional-outbox `Put`. When present, a **single-key** call
|
|
6137
|
-
* composes each event's `Put` into the same atomic `TransactWriteItems` as the
|
|
6138
|
-
* entity write, so the event RECORD is written ATOMICALLY with the state it
|
|
6139
|
-
* announces (they can never diverge). The row is drainable from real
|
|
6140
|
-
* DynamoDB Streams (captured at the table layer regardless of code path);
|
|
6141
|
-
* delivery / handlers are OUT of scope. Since #94 the derived-command
|
|
6142
|
-
* transaction also feeds every committed item (including this outbox `Put`)
|
|
6143
|
-
* into the in-process `ChangeCaptureRegistry` seam, so the dev/test
|
|
6144
|
-
* `src/cdc/` emulator now observes this path too — production Streams and the
|
|
6145
|
-
* emulator agree. Absent on a mutation with no `emits`.
|
|
6146
|
-
*/
|
|
6147
|
-
readonly outboxEvents?: readonly DerivedOutboxEvent[];
|
|
6148
|
-
/**
|
|
6149
|
-
* The client-token idempotency guard derived from the mutation's `idempotency`
|
|
6150
|
-
* effect (issue #87): a marker-row `Put` (`attribute_not_exists`). When present,
|
|
6151
|
-
* a **single-key** call composes the guard `Put` into the same atomic
|
|
6152
|
-
* `TransactWriteItems` as the entity write, so a same-token re-execution fails
|
|
6153
|
-
* the guard and rolls the WHOLE transaction back — no effect is double-applied.
|
|
6154
|
-
* Absent on a mutation with no `idempotency` / a #64 hand-written method.
|
|
6155
|
-
*/
|
|
6156
|
-
readonly idempotencyGuard?: DerivedIdempotencyGuard;
|
|
6157
|
-
/**
|
|
6158
|
-
* The maintenance writes derived from the relation-side `write.maintainedOn`
|
|
6159
|
-
* declarations (issue #125 → #127; the D/#124 → compile lowering). When
|
|
6160
|
-
* present (non-empty), a **single-key** call composes each maintenance write
|
|
6161
|
-
* — a projected snapshot (`SET`) or a bounded-collection append
|
|
6162
|
-
* (`SET … = list_append(…)`) on a SEPARATE owner row — into the same atomic
|
|
6163
|
-
* `TransactWriteItems` as the source entity write (and every other effect),
|
|
6164
|
-
* so the maintained row moves ATOMICALLY with the source: a failure on any
|
|
6165
|
-
* item rolls the WHOLE transaction back (the snapshot / collection never
|
|
6166
|
-
* partially persists). The owner row is upserted (a bare `UpdateItem`
|
|
6167
|
-
* creates it if absent — the additive maintenance semantics). This is the
|
|
6168
|
-
* synchronous (`updateMode: 'mutation'`) path only; the async stream path is
|
|
6169
|
-
* #130 (loud-rejected at compile time). Absent on a mutation that fires no
|
|
6170
|
-
* maintainer / a #64 hand-written method.
|
|
6171
|
-
*/
|
|
6172
|
-
readonly maintainWrites?: readonly DerivedMaintainWrite[];
|
|
6173
|
-
/**
|
|
6174
|
-
* The maintenance-outbox events for `updateMode: 'stream'` maintainers (issue
|
|
6175
|
-
* #130). Each is a transactional-outbox `Put` (the same `literalKey` marker
|
|
6176
|
-
* mechanism as #87's outbox) composed into the method's atomic
|
|
6177
|
-
* `TransactWriteItems` — recording the maintenance intent ATOMICALLY with the
|
|
6178
|
-
* source write. The `src/cdc/` drain selects these (`PK begins_with
|
|
6179
|
-
* 'OUTBOX#MAINT#'`) and applies the owner-row write asynchronously (so a
|
|
6180
|
-
* snapshot SET / collection append+trim / counter ADD / running-max conditional
|
|
6181
|
-
* SET happens off the write path, at-least-once, per-shard ordered). Absent on a
|
|
6182
|
-
* mutation with no stream maintainer.
|
|
6183
|
-
*/
|
|
6184
|
-
readonly maintainOutbox?: readonly DerivedMaintainOutbox[];
|
|
6185
|
-
/**
|
|
6186
|
-
* The return projection of a `mutation`-derived method (issue #83). When
|
|
6187
|
-
* present, a **single-key** call performs the write then issues a
|
|
6188
|
-
* **consistent read-back** (`GetItem` with `ConsistentRead`) of the
|
|
6189
|
-
* written entity's primary key and returns the projected item via the
|
|
6190
|
-
* existing read-projection machinery. Absent on a #64 hand-written method.
|
|
6191
|
-
*/
|
|
6192
|
-
readonly returnSelection?: Readonly<Record<string, boolean>>;
|
|
6193
|
-
}>>;
|
|
6194
|
-
}
|
|
6195
|
-
/** The params a command method consumes (the body's `params` argument). */
|
|
6196
|
-
type ContractCommandParams = Record<string, unknown>;
|
|
6197
4397
|
/**
|
|
6198
4398
|
* The projected read-back item a `mutation`-derived command method returns, or
|
|
6199
4399
|
* `null` when the written row was absent at read-back (e.g. a `remove`). A plain
|
|
6200
4400
|
* record of the projected fields.
|
|
6201
4401
|
*/
|
|
6202
4402
|
type CommandReturn = Record<string, unknown> | null;
|
|
6203
|
-
/**
|
|
6204
|
-
* #101 Phase 3 — the per-op outcome of a `mode: 'parallel'` key-array bulk write.
|
|
6205
|
-
* A non-atomic fan-out reports each key's result independently: `{ ok: true }` on
|
|
6206
|
-
* success, `{ ok: false, error }` on a per-op failure (a failed condition / a
|
|
6207
|
-
* `BatchWriteItem` item that stayed unprocessed after retry). The array index
|
|
6208
|
-
* matches the input key index, and a per-op failure NEVER aborts the others.
|
|
6209
|
-
*/
|
|
6210
|
-
interface ParallelOpResult$1 {
|
|
6211
|
-
readonly ok: boolean;
|
|
6212
|
-
readonly error?: string;
|
|
6213
|
-
}
|
|
6214
|
-
/** The partial-success return of a `mode: 'parallel'` key-array bulk write (#101). */
|
|
6215
|
-
interface CommandParallelReturn {
|
|
6216
|
-
readonly results: readonly ParallelOpResult$1[];
|
|
6217
|
-
}
|
|
6218
|
-
/**
|
|
6219
|
-
* Execute a command-contract method for a **single** key — one write op
|
|
6220
|
-
* (`put` / `update` / `delete`).
|
|
6221
|
-
*
|
|
6222
|
-
* @param contract The resolved command contract (a {@link CommandModelContract}).
|
|
6223
|
-
* @param methodName The method to execute.
|
|
6224
|
-
* @param key A single contract key (the mutation target).
|
|
6225
|
-
* @param params The mutation params (the body's `params` argument).
|
|
6226
|
-
*/
|
|
6227
|
-
declare function executeCommandMethod(contract: ExecutableCommandContract, methodName: string, key: ContractKeyInput, params?: ContractCommandParams): Promise<void | CommandReturn>;
|
|
6228
|
-
/**
|
|
6229
|
-
* Execute a command-contract method for an **array** of keys — a batched write
|
|
6230
|
-
* per the method's declared {@link CommandBatchMode}:
|
|
6231
|
-
*
|
|
6232
|
-
* - `'transact'` → one atomic `TransactWriteItems` (≤25, condition-capable); a
|
|
6233
|
-
* >25-key array is **rejected** (an atomic transaction cannot be split).
|
|
6234
|
-
* - `'batchWrite'` → a `BatchWriteItem` (no conditions; chunked ≤25 with
|
|
6235
|
-
* `UnprocessedItems` retry).
|
|
6236
|
-
*
|
|
6237
|
-
* A method that declared no batched form rejects an array with a clear error.
|
|
6238
|
-
*
|
|
6239
|
-
* @param contract The resolved command contract.
|
|
6240
|
-
* @param methodName The method to execute.
|
|
6241
|
-
* @param keys An array of contract keys.
|
|
6242
|
-
* @param params The mutation params shared across every key.
|
|
6243
|
-
*/
|
|
6244
|
-
declare function executeCommandMethod(contract: ExecutableCommandContract, methodName: string, keys: readonly ContractKeyInput[], params?: ContractCommandParams): Promise<void | CommandParallelReturn>;
|
|
6245
4403
|
|
|
6246
4404
|
/**
|
|
6247
4405
|
* The **unified envelope** in-process execution engine (issue #101).
|
|
@@ -6299,17 +4457,30 @@ interface ReadRouteOptions {
|
|
|
6299
4457
|
interface ReadRouteDescriptor {
|
|
6300
4458
|
readonly query?: ModelRef;
|
|
6301
4459
|
readonly list?: ModelRef;
|
|
6302
|
-
|
|
4460
|
+
/**
|
|
4461
|
+
* The route's primary-key binding. A single key object reads once (result:
|
|
4462
|
+
* `Item | null` for a `query` route, a connection for a `list` route). On a
|
|
4463
|
+
* `query` route a **key ARRAY** (#283 — the batch-read fold that supersedes the
|
|
4464
|
+
* removed `DDBModel.batchGet`) reads every key and the route result is an ARRAY
|
|
4465
|
+
* of `Item | null` aligned to the input key order (a miss → `null`).
|
|
4466
|
+
*
|
|
4467
|
+
* The array read is HYBRID-routed by whether the select / key can use a chunked
|
|
4468
|
+
* server-side `BatchGetItem` (see {@link executeReadRoute}): a pure point-read
|
|
4469
|
+
* batch (no relation traversal, full-primary-key keys) runs through the chunked
|
|
4470
|
+
* `BatchGetItem` machinery (⌈N/100⌉ ops, `UnprocessedKeys` retry) for batchGet's
|
|
4471
|
+
* efficiency; a batch whose select requests relations (or whose key is a GSI /
|
|
4472
|
+
* partial key) falls back to a per-key single-item read (`executeAdHocQuery`) run
|
|
4473
|
+
* in parallel, since `BatchGetItem` cannot assemble relations. Both branches
|
|
4474
|
+
* return the identical ordered `(Item | null)[]` — the routing is a physical-read
|
|
4475
|
+
* choice only (no data / order / projection difference). A key array on a `list`
|
|
4476
|
+
* route is rejected (a partition read has no multi-key meaning).
|
|
4477
|
+
*/
|
|
4478
|
+
readonly key: Record<string, unknown> | readonly Record<string, unknown>[];
|
|
6303
4479
|
readonly select: Record<string, unknown>;
|
|
6304
4480
|
readonly options?: ReadRouteOptions;
|
|
6305
4481
|
}
|
|
6306
4482
|
/** The read envelope: alias → {@link ReadRouteDescriptor}. */
|
|
6307
4483
|
type ReadEnvelope = Record<string, ReadRouteDescriptor>;
|
|
6308
|
-
/** The result of a read route — a `query` item (or `null`) or a `list` connection. */
|
|
6309
|
-
type ReadRouteResult = Record<string, unknown> | null | {
|
|
6310
|
-
items: Record<string, unknown>[];
|
|
6311
|
-
cursor: string | null;
|
|
6312
|
-
};
|
|
6313
4484
|
/** The write execution mode: atomic transaction (default) or non-atomic parallel. */
|
|
6314
4485
|
type MutateMode = 'transaction' | 'parallel';
|
|
6315
4486
|
/** A read-back projection for a write op (the EXISTING query select shape). */
|
|
@@ -6325,6 +4496,12 @@ interface InProcessWriteDescriptor {
|
|
|
6325
4496
|
readonly create?: ModelRef;
|
|
6326
4497
|
readonly update?: ModelRef;
|
|
6327
4498
|
readonly remove?: ModelRef;
|
|
4499
|
+
/**
|
|
4500
|
+
* `upsert` — a `PutItem` with **no** `attribute_not_exists(PK)` guard (an
|
|
4501
|
+
* unconditional overwrite; the semantics of the removed `definePut`). Unlike
|
|
4502
|
+
* `create`, an `upsert` may carry a `condition` (honored verbatim).
|
|
4503
|
+
*/
|
|
4504
|
+
readonly upsert?: ModelRef;
|
|
6328
4505
|
/**
|
|
6329
4506
|
* The target's primary-key binding. A single key object applies the op once; an
|
|
6330
4507
|
* **array** of key objects (#101 `key: K | K[]`) applies the SAME op to every key
|
|
@@ -6339,25 +4516,6 @@ interface InProcessWriteDescriptor {
|
|
|
6339
4516
|
}
|
|
6340
4517
|
/** The write envelope: alias → {@link InProcessWriteDescriptor}. */
|
|
6341
4518
|
type WriteEnvelope = Record<string, InProcessWriteDescriptor>;
|
|
6342
|
-
/** Options for {@link executeWriteEnvelope}. */
|
|
6343
|
-
interface MutateOptions {
|
|
6344
|
-
readonly mode?: MutateMode;
|
|
6345
|
-
/**
|
|
6346
|
-
* Per-call throttle / transient-error retry override (issue #111), applied to
|
|
6347
|
-
* every write this envelope composes (the atomic `TransactWriteItems` in
|
|
6348
|
-
* `transaction` mode, or each op's send in `parallel` mode). A {@link RetryPolicy}
|
|
6349
|
-
* overrides the global policy; `false` disables retry. Omit ⇒ the configured /
|
|
6350
|
-
* built-in default policy.
|
|
6351
|
-
*/
|
|
6352
|
-
readonly retry?: RetryOverride;
|
|
6353
|
-
/**
|
|
6354
|
-
* Host-injected per-call write context (issue #50 / #139) — exposed to every
|
|
6355
|
-
* write hook (W1–W5) as `ctx.context`. In `transaction` mode every composed op
|
|
6356
|
-
* shares it and the persist hooks fire once for the atomic batch; in `parallel`
|
|
6357
|
-
* mode each independent op sees it. NEVER serialized. Absent ⇒ `{}`.
|
|
6358
|
-
*/
|
|
6359
|
-
readonly context?: RequestContext;
|
|
6360
|
-
}
|
|
6361
4519
|
/** A per-alias result in `parallel` mode: success carries the read-back, else error. */
|
|
6362
4520
|
type ParallelOpResult = {
|
|
6363
4521
|
readonly ok: true;
|
|
@@ -6366,7 +4524,7 @@ type ParallelOpResult = {
|
|
|
6366
4524
|
readonly ok: false;
|
|
6367
4525
|
readonly error: Error;
|
|
6368
4526
|
};
|
|
6369
|
-
declare const INTENT_KEYS: readonly ["create", "update", "remove"];
|
|
4527
|
+
declare const INTENT_KEYS: readonly ["create", "update", "remove", "upsert"];
|
|
6370
4528
|
type Intent = (typeof INTENT_KEYS)[number];
|
|
6371
4529
|
|
|
6372
4530
|
/**
|
|
@@ -6378,11 +4536,17 @@ type Intent = (typeof INTENT_KEYS)[number];
|
|
|
6378
4536
|
|
|
6379
4537
|
/** The entity type a {@link ModelStatic} value (or model class) carries. */
|
|
6380
4538
|
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;
|
|
6381
|
-
/**
|
|
4539
|
+
/**
|
|
4540
|
+
* The result type of one read route, derived from `query` / `list` + `select`.
|
|
4541
|
+
* A `query` route whose `key` is an ARRAY (#283 batch read, the `DDBModel.batchGet`
|
|
4542
|
+
* fold) resolves to a per-key ARRAY of `Item | null`, aligned to the input keys.
|
|
4543
|
+
*/
|
|
6382
4544
|
type ReadRouteResultOf<R> = R extends {
|
|
6383
4545
|
readonly query: infer M;
|
|
6384
4546
|
readonly select: infer S;
|
|
6385
|
-
} ?
|
|
4547
|
+
} ? R extends {
|
|
4548
|
+
readonly key: readonly unknown[];
|
|
4549
|
+
} ? readonly (QueryResultFor<EntityOf<M>, S> | null)[] : QueryResultFor<EntityOf<M>, S> | null : R extends {
|
|
6386
4550
|
readonly list: infer M;
|
|
6387
4551
|
readonly select: infer S;
|
|
6388
4552
|
} ? {
|
|
@@ -6538,7 +4702,7 @@ interface CompiledReadRoute {
|
|
|
6538
4702
|
* ## Why "prepare once, execute many"
|
|
6539
4703
|
*
|
|
6540
4704
|
* `DDBModel.mutate` / `Model.query` recompile the plan on every call (`#203`'s
|
|
6541
|
-
* "軽量だが per-call 再コンパイル" cell). `
|
|
4705
|
+
* "軽量だが per-call 再コンパイル" cell). `publishCommand` / `publishQuery`
|
|
6542
4706
|
* precompile but weld it to a named CQRS contract. `prepare` is the missing
|
|
6543
4707
|
* middle: precompiled **without** the contract ceremony.
|
|
6544
4708
|
*
|
|
@@ -6553,11 +4717,11 @@ interface CompiledReadRoute {
|
|
|
6553
4717
|
* computed once. `execute` binds the concrete key VALUES + per-call pagination /
|
|
6554
4718
|
* consistency and runs the unified read-plan executor
|
|
6555
4719
|
* (`executeCompiledReadRoute`, issue #249) — the identical core `Model.query` /
|
|
6556
|
-
* `Model.list` and `
|
|
4720
|
+
* `Model.list` and `publishQuery` use. (The
|
|
6557
4721
|
* `ExecutionPlan` bakes concrete key values into its `keyCondition`, so a single
|
|
6558
4722
|
* compiled operation object is NOT reusable across param sets; the precompiled
|
|
6559
4723
|
* product is the resolved model + normalized projection — exactly what the public
|
|
6560
|
-
* `
|
|
4724
|
+
* `publishQuery` op reuses — and the residual per-op `plan()` is identical to
|
|
6561
4725
|
* that public path.)
|
|
6562
4726
|
*
|
|
6563
4727
|
* ## No runtime capture (the design's key constraint)
|
|
@@ -6602,17 +4766,25 @@ interface PreparedParamRef {
|
|
|
6602
4766
|
/** The param name, e.g. `userId`. */
|
|
6603
4767
|
readonly name: string;
|
|
6604
4768
|
}
|
|
6605
|
-
/** Runtime guard: is `value` a {@link PreparedParamRef} (a `$.<name>` read)? */
|
|
6606
|
-
declare function isPreparedParamRef(value: unknown): value is PreparedParamRef;
|
|
6607
4769
|
/** The placeholder proxy handed to a `prepare` body as `$`. Every `$.<name>` read
|
|
6608
4770
|
* mints a {@link PreparedParamRef}; any further access / coercion throws so the
|
|
6609
|
-
* body stays a pure, params-independent template (no runtime capture).
|
|
4771
|
+
* body stays a pure, params-independent template (no runtime capture).
|
|
4772
|
+
*
|
|
4773
|
+
* **Input Port 統一 (issue #264)**: this proxy is one of the three authoring
|
|
4774
|
+
* spellings of the SAME Input Port reference model — see the unification note
|
|
4775
|
+
* on {@link import('../define/transaction.js').ParamProxy} (`defineTransaction`'s
|
|
4776
|
+
* `p.*` proxy is the model; a `$.<name>` read here is the same "declared
|
|
4777
|
+
* input field, bound at execution" reference that serializes as `{<name>}`
|
|
4778
|
+
* templates / `{ref:["input","<name>"]}` expression refs elsewhere). Behavior
|
|
4779
|
+
* here is unchanged in 0.7.x (interop kept). */
|
|
6610
4780
|
type PreparedInputProxy = Record<string, PreparedParamRef>;
|
|
6611
4781
|
/** A `prepare` write route: `{ create|update|remove: () => Model, key, input?, condition?, result? }`. */
|
|
6612
4782
|
interface PreparedWriteRoute {
|
|
6613
4783
|
readonly create?: ModelRef;
|
|
6614
4784
|
readonly update?: ModelRef;
|
|
6615
4785
|
readonly remove?: ModelRef;
|
|
4786
|
+
/** `upsert` — a `PutItem` with no `attribute_not_exists(PK)` guard (unconditional overwrite; the `definePut` semantics). */
|
|
4787
|
+
readonly upsert?: ModelRef;
|
|
6616
4788
|
readonly key: Record<string, unknown>;
|
|
6617
4789
|
readonly input?: Record<string, unknown>;
|
|
6618
4790
|
readonly condition?: Record<string, unknown>;
|
|
@@ -6688,22 +4860,6 @@ declare class PreparedReadStatement {
|
|
|
6688
4860
|
private runRoute;
|
|
6689
4861
|
}
|
|
6690
4862
|
type PreparedStatement = PreparedReadStatement | PreparedWriteStatement;
|
|
6691
|
-
/**
|
|
6692
|
-
* The max number of distinct compiled prepared statements the fallback cache
|
|
6693
|
-
* retains. Bounded so an unbounded variety of inline `prepare` shapes cannot grow
|
|
6694
|
-
* the cache without limit (issue #205 AC). LRU eviction: least-recently-used first.
|
|
6695
|
-
*/
|
|
6696
|
-
declare const PREPARE_CACHE_MAX = 256;
|
|
6697
|
-
/**
|
|
6698
|
-
* Compile a declarative read/write route body into a reusable prepared statement
|
|
6699
|
-
* (issue #205). Backs {@link DDBModel.prepare}.
|
|
6700
|
-
*
|
|
6701
|
-
* The body is evaluated ONCE with a recording `$` proxy; the resulting routes are
|
|
6702
|
-
* classified (all-read or all-write), a structure key is computed, and the compiled
|
|
6703
|
-
* handle is memoized in the bounded fallback cache so an inline `prepare(fn)` of the
|
|
6704
|
-
* same shape recompiles at most once.
|
|
6705
|
-
*/
|
|
6706
|
-
declare function prepare(body: PreparedBody): PreparedStatement;
|
|
6707
4863
|
|
|
6708
4864
|
/**
|
|
6709
4865
|
* `subscribe` — the declarative, batch CDC consumer builder (issue #153).
|
|
@@ -6778,66 +4934,6 @@ type CdcSubscribeHandlers = keyof CdcModelRegistry extends never ? Record<string
|
|
|
6778
4934
|
declare abstract class DDBModel {
|
|
6779
4935
|
/** @internal Type brand — prevents primitives from structurally matching DDBModel. */
|
|
6780
4936
|
private readonly __brand;
|
|
6781
|
-
/**
|
|
6782
|
-
* Register the `DynamoDBClient` GraphDDB sends through.
|
|
6783
|
-
*
|
|
6784
|
-
* IMPORTANT (issue #111): GraphDDB now owns single-op throttle / transient-error
|
|
6785
|
-
* retry (see {@link setRetryPolicy}), so a client left at the AWS SDK default
|
|
6786
|
-
* `maxAttempts: 3` retries MULTIPLICATIVELY underneath the library. Construct the
|
|
6787
|
-
* client with `maxAttempts: 1` so the library is the single source of truth for
|
|
6788
|
-
* retry — e.g. `new DynamoDBClient({ maxAttempts: 1 })`. GraphDDB does NOT mutate
|
|
6789
|
-
* a client you pass in.
|
|
6790
|
-
*/
|
|
6791
|
-
static setClient(client: DynamoDBClient): void;
|
|
6792
|
-
/**
|
|
6793
|
-
* Read back the registered `DynamoDBClient` — the resolved client last passed to
|
|
6794
|
-
* {@link setClient}. Symmetric with {@link setClient} and backed by the same
|
|
6795
|
-
* {@link ClientManager} store, so it always reflects the client GraphDDB sends
|
|
6796
|
-
* through.
|
|
6797
|
-
*
|
|
6798
|
-
* Throws a clear error if no client has been registered yet (call
|
|
6799
|
-
* {@link setClient} first) — matching {@link ClientManager.getClient}'s "loud,
|
|
6800
|
-
* never silently `undefined`" convention shared with the rest of the client API.
|
|
6801
|
-
*/
|
|
6802
|
-
static getClient(): DynamoDBClient;
|
|
6803
|
-
static setTableMapping(mapping: Record<string, string>): void;
|
|
6804
|
-
/**
|
|
6805
|
-
* Configure the global single-op retry policy (issue #111) — exponential backoff
|
|
6806
|
-
* + jitter with a bounded attempt cap, applied to every GetItem / Query / Put /
|
|
6807
|
-
* Update / Delete / relation-fan-out send and to `TransactWriteItems`. Mirrors
|
|
6808
|
-
* {@link setClient} / {@link setTableMapping}. A sensible default is ALWAYS on; call
|
|
6809
|
-
* this only to tune it (or pass `null` to restore the default). A per-call
|
|
6810
|
-
* `retry?: RetryPolicy | false` on an operation's options overrides this globally
|
|
6811
|
-
* for that call (`false` disables retry).
|
|
6812
|
-
*/
|
|
6813
|
-
static setRetryPolicy(policy: RetryPolicy | null): void;
|
|
6814
|
-
/**
|
|
6815
|
-
* Register a {@link Middleware} (issue #50; read path #138, write path #139).
|
|
6816
|
-
* One registered object may carry read and/or write hooks.
|
|
6817
|
-
*
|
|
6818
|
-
* - **Read** R1–R5 — request entry (`read.before`), each physical op before send
|
|
6819
|
-
* / after raw fetch (`read.op.before` / `read.op.afterFetch`, INCLUDING every
|
|
6820
|
-
* relation fan-out fetch), the final hydrated result (`read.afterFetch`), and on
|
|
6821
|
-
* error (`read.op.onError` / `read.onError`).
|
|
6822
|
-
* - **Write** W1–W5 — the logical write before effect derivation (`write.before`,
|
|
6823
|
-
* may mutate `ctx.input` / `ctx.kind` incl. a delete→update rewrite), after commit
|
|
6824
|
-
* (`write.after`), the physical persist on the fully composed batch
|
|
6825
|
-
* (`write.persist.before` / `write.persist.after`, fires ONCE per atomic
|
|
6826
|
-
* transaction), and on error (`write.persist.onError` / `write.onError`).
|
|
6827
|
-
*
|
|
6828
|
-
* Hooks may observe, mutate `ctx`, `throw` to cancel, or (on `onError`) recover by
|
|
6829
|
-
* returning a value; the library imposes no semantics. Ordering: `before*` runs
|
|
6830
|
-
* first-registered-first (FIFO); `after*` / `onError` run last-registered-first
|
|
6831
|
-
* (LIFO). Mirrors {@link setRetryPolicy} — a host-runtime global stored on
|
|
6832
|
-
* {@link ClientManager}; never serialized (the bridge #48 is unaffected). Per-call
|
|
6833
|
-
* host context flows in via the read's / write's `{ context }` option.
|
|
6834
|
-
*/
|
|
6835
|
-
static use(mw: Middleware): void;
|
|
6836
|
-
/** Remove every registered middleware (read AND write; teardown / tests). See {@link use}. */
|
|
6837
|
-
static clearMiddleware(): void;
|
|
6838
|
-
static transaction(fn: (tx: TransactionContext) => void | Promise<void>, options?: {
|
|
6839
|
-
context?: RequestContext;
|
|
6840
|
-
}): Promise<void>;
|
|
6841
4937
|
/**
|
|
6842
4938
|
* In-process multi-route READ (issue #101 — the unified envelope). Each alias
|
|
6843
4939
|
* route (`{ query | list: Model, key, select, options? }`) runs **independently
|
|
@@ -6868,8 +4964,8 @@ declare abstract class DDBModel {
|
|
|
6868
4964
|
/**
|
|
6869
4965
|
* **Prepared statement** (issue #205 / design #203) — the read/write-unified
|
|
6870
4966
|
* middle tier between the ad-hoc `DDBModel.mutate` / `DDBModel.query` (per-call
|
|
6871
|
-
* recompiled) and the public CQRS contract (`
|
|
6872
|
-
* `
|
|
4967
|
+
* recompiled) and the public CQRS contract (`publishCommand` /
|
|
4968
|
+
* `publishQuery`, precompiled but contract-welded).
|
|
6873
4969
|
*
|
|
6874
4970
|
* `prepare($ => ({...}))` **compiles the declarative route body once** and returns
|
|
6875
4971
|
* a handle whose `.execute(params)` binds the per-call values and runs through the
|
|
@@ -6939,20 +5035,6 @@ declare abstract class DDBModel {
|
|
|
6939
5035
|
* registered `@cdcProjected` model throws at build time.
|
|
6940
5036
|
*/
|
|
6941
5037
|
static subscribe(handlers: CdcSubscribeHandlers): ChangeHandler;
|
|
6942
|
-
/**
|
|
6943
|
-
* Multi-key, multi-model batch read. Each {@link BatchGetRequest} contributes its
|
|
6944
|
-
* keys to a chunked `BatchGetItem` per physical table; results are re-grouped by
|
|
6945
|
-
* model on the returned {@link BatchGetResult}.
|
|
6946
|
-
*
|
|
6947
|
-
* Fires the read hooks (issue #142): request-level R1/R4/R5 with kind
|
|
6948
|
-
* `'batchGet'`, and op-level R2/R3/R5 for each underlying physical `BatchGetItem`
|
|
6949
|
-
* op. The optional `{ context }` is exposed to every hook as `ctx.context` and
|
|
6950
|
-
* threaded UNCHANGED to every op (absent ⇒ `{}`), so a global hook (e.g. a
|
|
6951
|
-
* tenant-scope R2 FilterExpression) applies across the whole batch. With no
|
|
6952
|
-
* middleware registered the read path is unchanged (zero-overhead fast path).
|
|
6953
|
-
*/
|
|
6954
|
-
static batchGet(requests: BatchGetRequest[], options?: BatchGetOptions): Promise<BatchGetResult>;
|
|
6955
|
-
static batchWrite(requests: BatchWriteRequest[]): Promise<void>;
|
|
6956
5038
|
static asModel<C extends new (...args: never[]) => DDBModel>(this: C): ModelStatic<InstanceType<C>, C>;
|
|
6957
5039
|
}
|
|
6958
5040
|
|
|
@@ -6981,8 +5063,6 @@ interface Column<V = unknown, M = unknown> {
|
|
|
6981
5063
|
/** @internal phantom — owning-model brand. */
|
|
6982
5064
|
readonly __model?: M;
|
|
6983
5065
|
}
|
|
6984
|
-
/** Runtime guard: is this value a {@link Column} reference? */
|
|
6985
|
-
declare function isColumn(value: unknown): value is Column;
|
|
6986
5066
|
/**
|
|
6987
5067
|
* The `Model.col` map: one {@link Column} per scalar field of the entity, each
|
|
6988
5068
|
* typed with the field's declared value type and branded to the model.
|
|
@@ -7015,8 +5095,6 @@ interface KeySegment {
|
|
|
7015
5095
|
/** The interpolated column references, in order. */
|
|
7016
5096
|
readonly columns: readonly Column[];
|
|
7017
5097
|
}
|
|
7018
|
-
/** Runtime guard: is this value a {@link KeySegment}? */
|
|
7019
|
-
declare function isKeySegment(value: unknown): value is KeySegment;
|
|
7020
5098
|
/**
|
|
7021
5099
|
* The interpolation slot accepted by {@link k}: a typed column reference
|
|
7022
5100
|
* (`c.field`). Bare strings are NOT accepted — a key field must be named via a
|
|
@@ -7082,519 +5160,4 @@ declare function key<T extends Record<string, unknown>>(builder: (c: {
|
|
|
7082
5160
|
readonly [K in keyof T]-?: Column<T[K], T>;
|
|
7083
5161
|
}) => KeyStructure): KeyDefinitionMarker<T>;
|
|
7084
5162
|
|
|
7085
|
-
type
|
|
7086
|
-
interface FieldOptions {
|
|
7087
|
-
format?: 'datetime' | 'date';
|
|
7088
|
-
readonly?: boolean;
|
|
7089
|
-
serialize?: (value: unknown) => unknown;
|
|
7090
|
-
deserialize?: (value: unknown) => unknown;
|
|
7091
|
-
/**
|
|
7092
|
-
* Optional human-readable description of the field (issue #154). Pure
|
|
7093
|
-
* documentation metadata — it does NOT affect storage, hydration, key handling,
|
|
7094
|
-
* or any runtime behaviour. When present it is propagated to the field entry in
|
|
7095
|
-
* `manifest.json` ({@link ManifestField.description}) and surfaces as a field
|
|
7096
|
-
* comment in the generated Python `types.py`. Absent → byte-for-byte unchanged
|
|
7097
|
-
* output (backward compatible).
|
|
7098
|
-
*/
|
|
7099
|
-
description?: string;
|
|
7100
|
-
}
|
|
7101
|
-
interface FieldMetadata {
|
|
7102
|
-
propertyName: string;
|
|
7103
|
-
dynamoType: DynamoType;
|
|
7104
|
-
options?: FieldOptions;
|
|
7105
|
-
/**
|
|
7106
|
-
* For a `@literal(...)` field — a `string`-stored field whose value is one of a
|
|
7107
|
-
* known, finite set — the allowed literal values, in declaration order (issue
|
|
7108
|
-
* #75). Recorded purely as model metadata so a Contract param bound into the
|
|
7109
|
-
* field can recover the precise `literal` param-kind from its bind position
|
|
7110
|
-
* (`src/spec/contracts.ts`), serializing as `{ type: 'literal', literals: [...] }`
|
|
7111
|
-
* instead of the imprecise `'string'`. JSON-safe by construction (a non-empty
|
|
7112
|
-
* array of strings). `undefined` for every other field decorator (`@string`,
|
|
7113
|
-
* `@number`, …), so plain fields are byte-for-byte unchanged (backward compat).
|
|
7114
|
-
*/
|
|
7115
|
-
literals?: readonly string[];
|
|
7116
|
-
/**
|
|
7117
|
-
* For an `@ttl` field (issue #172, Epic #167 — CFn generator C4) — marks this
|
|
7118
|
-
* field as the model's **DynamoDB Time-To-Live** attribute. `@ttl` is a
|
|
7119
|
-
* standalone semantic decorator pinned to `number` (it records `dynamoType: 'N'`
|
|
7120
|
-
* itself, so `@ttl expiresAt!: string` is a compile error), because DynamoDB TTL
|
|
7121
|
-
* requires a Number attribute holding **epoch seconds**. It is a **physical
|
|
7122
|
-
* schema** fact (unlike the maintenance signals kept off the manifest): the flag
|
|
7123
|
-
* flows through {@link EntityMetadata} into `manifest.json`
|
|
7124
|
-
* ({@link ManifestEntity.ttlAttribute}), which the CloudFormation emitter consumes
|
|
7125
|
-
* to render `TimeToLiveSpecification`. DynamoDB allows exactly one TTL attribute
|
|
7126
|
-
* per table, enforced loudly at model registration (`@model`). `undefined` for
|
|
7127
|
-
* every other field decorator, so non-TTL fields are byte-for-byte unchanged.
|
|
7128
|
-
*/
|
|
7129
|
-
ttl?: boolean;
|
|
7130
|
-
}
|
|
7131
|
-
|
|
7132
|
-
/**
|
|
7133
|
-
* The maintenance kind of a model (issue #152). `'entity'` (the default) is a plain
|
|
7134
|
-
* stored entity. `'materializedView'` / `'sparseView'` mark a model as a maintained
|
|
7135
|
-
* **view** row whose `@maintainedFrom` declarations feed the maintenance graph (the
|
|
7136
|
-
* adapter that replaces the old `defineView` / `ViewRegistry` path). A `sparseView`
|
|
7137
|
-
* requires every `@maintainedFrom` to carry a `when` membership predicate.
|
|
7138
|
-
*/
|
|
7139
|
-
type ModelKind = 'entity' | 'materializedView' | 'sparseView';
|
|
7140
|
-
/**
|
|
7141
|
-
* One lowered `@maintainedFrom(source, (self, source) => options)` declaration on a
|
|
7142
|
-
* view model (issue #152). It is the decorator-declarative replacement for one
|
|
7143
|
-
* `defineView` source slice: the symbolic `(self, source) =>` callback has already
|
|
7144
|
-
* been evaluated into the SAME payload-rooted IR (`keyBind` → `$.entity.*` key
|
|
7145
|
-
* binding, `project` → {@link ProjectionMap}), so the adapter
|
|
7146
|
-
* (`src/relation/maintenance-view-adapter.ts`) reconstructs a byte-identical
|
|
7147
|
-
* `ViewSourceSlice` from it.
|
|
7148
|
-
*
|
|
7149
|
-
* `collection` and `when` are mutually exclusive (a row either holds a maintained
|
|
7150
|
-
* list or appears/disappears as a whole — the same rule the old builder enforced).
|
|
7151
|
-
*/
|
|
7152
|
-
interface MaintainedFromMetadata {
|
|
7153
|
-
/** Resolves the source entity whose lifecycle events maintain the view row. */
|
|
7154
|
-
sourceFactory: () => new (...args: unknown[]) => unknown;
|
|
7155
|
-
/** The lifecycle events that maintain the view row (`created`/`updated`/`removed`). */
|
|
7156
|
-
on: readonly MaintainEvent[];
|
|
7157
|
-
/** The view-row key binding (view-row key field → payload-rooted source value). */
|
|
7158
|
-
keyBind: Readonly<Record<string, EffectPath>>;
|
|
7159
|
-
/** The projection of the source payload into the view row (target attr → transform). */
|
|
7160
|
-
project: ProjectionMap;
|
|
7161
|
-
/** Bounded/ordered collection options when this declaration maintains a list. */
|
|
7162
|
-
collection?: {
|
|
7163
|
-
/** The view-row attribute that holds the maintained list (a `self.<field>`). */
|
|
7164
|
-
field: string;
|
|
7165
|
-
maxItems?: number;
|
|
7166
|
-
order?: 'ASC' | 'DESC';
|
|
7167
|
-
/** The source value the collection orders by / trims on (`$.entity.<field>`). */
|
|
7168
|
-
orderBy?: EffectPath;
|
|
7169
|
-
};
|
|
7170
|
-
/** Sparse-view membership predicate (mutually exclusive with `collection`). */
|
|
7171
|
-
when?: MembershipPredicate;
|
|
7172
|
-
consistency?: MaintainConsistency;
|
|
7173
|
-
updateMode?: MaintainUpdateMode;
|
|
7174
|
-
/**
|
|
7175
|
-
* Optional human-readable description of this maintained-from source slice (issue
|
|
7176
|
-
* #166, follow-up of #154), supplied via
|
|
7177
|
-
* `@maintainedFrom(() => Source, (self, source) => ({ description, ... }))`. Pure
|
|
7178
|
-
* documentation metadata — it does NOT affect the maintenance IR (`keyBind` /
|
|
7179
|
-
* `project` / `on` / `collection` / `when`) or any runtime behaviour.
|
|
7180
|
-
*
|
|
7181
|
-
* NOTE (representation): the maintenance IR is deliberately kept OFF the
|
|
7182
|
-
* serializable `manifest.json` / `operations.json` — the {@link Manifest} carries
|
|
7183
|
-
* only the physical shape (table / keys / GSIs / relation navigation), NOT the
|
|
7184
|
-
* maintenance graph (see `detectStreamMaintenance` in `src/codegen/cloudformation.ts`).
|
|
7185
|
-
* There is therefore no manifest / operations / generated-Python site for a
|
|
7186
|
-
* maintained-from source; this description is captured at the metadata layer only
|
|
7187
|
-
* (mirroring where #154 captured its descriptions before propagation), available to
|
|
7188
|
-
* any future consumer that DOES surface `@maintainedFrom` (e.g. the CDC-projection
|
|
7189
|
-
* typed-consumer path, #153). Absent → byte-for-byte unchanged metadata.
|
|
7190
|
-
*/
|
|
7191
|
-
description?: string;
|
|
7192
|
-
}
|
|
7193
|
-
interface KeyDefinition {
|
|
7194
|
-
/** The canonical structured key (PK/SK segment lists). */
|
|
7195
|
-
segmented: SegmentedKey;
|
|
7196
|
-
inputFieldNames: string[];
|
|
7197
|
-
}
|
|
7198
|
-
interface GsiDefinition {
|
|
7199
|
-
indexName: string;
|
|
7200
|
-
/** The canonical structured key (PK/SK segment lists). */
|
|
7201
|
-
segmented: SegmentedKey;
|
|
7202
|
-
inputFieldNames: string[];
|
|
7203
|
-
unique: boolean;
|
|
7204
|
-
/**
|
|
7205
|
-
* Optional human-readable description of the GSI (issue #166, follow-up of #154),
|
|
7206
|
-
* supplied via `gsi(name, key, { description })`. Pure documentation metadata — it
|
|
7207
|
-
* does NOT affect the index key, projection, or any runtime behaviour. When present
|
|
7208
|
-
* it is propagated to the index entry in `manifest.json`
|
|
7209
|
-
* ({@link ManifestGsi.description}) and surfaces as the docstring of any generated
|
|
7210
|
-
* Python query method that reads through this index (a query carrying the GSI's
|
|
7211
|
-
* `indexName`, when the query itself declares no description). Absent →
|
|
7212
|
-
* byte-for-byte unchanged output (backward compatible).
|
|
7213
|
-
*/
|
|
7214
|
-
description?: string;
|
|
7215
|
-
}
|
|
7216
|
-
interface RelationLimitOptions {
|
|
7217
|
-
default: number;
|
|
7218
|
-
max: number;
|
|
7219
|
-
}
|
|
7220
|
-
/**
|
|
7221
|
-
* The named maintenance pattern a relation participates in (Epic #118 §5.1).
|
|
7222
|
-
*
|
|
7223
|
-
* `pattern` is the *preset* selector: omitting it leaves the relation a plain
|
|
7224
|
-
* read-only navigation (the historical `hasMany`/`belongsTo`/`hasOne`
|
|
7225
|
-
* behaviour), exactly backward compatible. When present it names the AWS
|
|
7226
|
-
* design pattern the relation lowers to — the lowering itself (maintenance
|
|
7227
|
-
* graph / compile injection) is out of scope for issue #121 and lands in
|
|
7228
|
-
* #124/#125; here the value is recorded purely as declaration metadata.
|
|
7229
|
-
*
|
|
7230
|
-
* Typed as a `string` union of the known presets but kept open-ended via the
|
|
7231
|
-
* `(string & {})` tail so a future preset can be authored before this union is
|
|
7232
|
-
* widened, without a breaking change to callers.
|
|
7233
|
-
*
|
|
7234
|
-
* The union lists only the presets that are actually *reachable* as a recorded
|
|
7235
|
-
* `RelationOptions.pattern` / `AggregateOptions.pattern` value: `samePartition`,
|
|
7236
|
-
* `counter`, `embeddedSnapshot`, and the two versioned discriminators
|
|
7237
|
-
* `versionedLatest` / `versionedHistory` (issue #152). `materializedView` /
|
|
7238
|
-
* `sparseView` migrated to the model-level `@model({ kind })` selector (they are
|
|
7239
|
-
* a {@link ModelKind}, not a relation preset); the placeholder `edge` /
|
|
7240
|
-
* `dualEdge` / `versioned` / `externalProjection` names are not authored anywhere
|
|
7241
|
-
* and were removed. A future preset can still be written against the `(string & {})`
|
|
7242
|
-
* tail before it is added to this union.
|
|
7243
|
-
*/
|
|
7244
|
-
type RelationPattern = 'samePartition' | 'counter' | 'embeddedSnapshot' | 'versionedLatest' | 'versionedHistory' | (string & {});
|
|
7245
|
-
/**
|
|
7246
|
-
* Consistency boundary a maintained write is declared against (Epic #118 §4.A.7):
|
|
7247
|
-
* `transactional` = same `TransactWriteItems`; `eventual` = asynchronous catch-up.
|
|
7248
|
-
*/
|
|
7249
|
-
type RelationConsistency = 'transactional' | 'eventual';
|
|
7250
|
-
/**
|
|
7251
|
-
* How a maintained write is applied (Epic #118 §4.A.7):
|
|
7252
|
-
* `mutation` = composed into the same mutation; `stream` = driven asynchronously
|
|
7253
|
-
* off a stream / outbox.
|
|
7254
|
-
*/
|
|
7255
|
-
type RelationUpdateMode = 'mutation' | 'stream';
|
|
7256
|
-
/**
|
|
7257
|
-
* Read-strategy hints for a maintained relation (Epic #118 §5.1 `read`).
|
|
7258
|
-
*
|
|
7259
|
-
* All fields optional — present only when a `pattern` declares a read shape
|
|
7260
|
-
* (e.g. an `embeddedSnapshot` keeping the latest N items). A bare relation
|
|
7261
|
-
* carries no `read` block.
|
|
7262
|
-
*/
|
|
7263
|
-
interface RelationReadOptions {
|
|
7264
|
-
/** Cap on materialised items kept inline (e.g. "latest 3 posts"). */
|
|
7265
|
-
maxItems?: number;
|
|
7266
|
-
/** Ordering of the maintained collection. */
|
|
7267
|
-
order?: 'ASC' | 'DESC';
|
|
7268
|
-
}
|
|
7269
|
-
/**
|
|
7270
|
-
* Write-maintenance declaration for a relation (Epic #118 §5.1 `write`).
|
|
7271
|
-
*
|
|
7272
|
-
* `maintainedOn` is the cross-entity trigger list (`"<Entity>.<event>"`, e.g.
|
|
7273
|
-
* `"Post.created"`) that the maintenance graph (#124/#125) will key off. Recorded
|
|
7274
|
-
* here as declaration metadata only.
|
|
7275
|
-
*/
|
|
7276
|
-
interface RelationWriteOptions {
|
|
7277
|
-
/**
|
|
7278
|
-
* Cross-entity triggers, e.g. `['Post.created', 'Post.removed']`. The event
|
|
7279
|
-
* segment is the confirmed shared lifecycle vocabulary (epic #118 A/B
|
|
7280
|
-
* reconciliation): `created` / `updated` / `removed`.
|
|
7281
|
-
*/
|
|
7282
|
-
maintainedOn?: string[];
|
|
7283
|
-
consistency?: RelationConsistency;
|
|
7284
|
-
updateMode?: RelationUpdateMode;
|
|
7285
|
-
}
|
|
7286
|
-
/**
|
|
7287
|
-
* Snapshot / projection capture map for a maintained relation
|
|
7288
|
-
* (Epic #118 §5.1 / §5.2 `projection`).
|
|
7289
|
-
*
|
|
7290
|
-
* Maps each captured field name on the maintaining side to **how** the source
|
|
7291
|
-
* value is projected. The confirmed shared vocabulary (epic #118 A/B
|
|
7292
|
-
* reconciliation) is the **function-form IR** — the same {@link
|
|
7293
|
-
* ProjectionTransform} node A (#120) defines on `src/define/entity-writes.ts`,
|
|
7294
|
-
* re-used here (NOT re-defined) so the two surfaces never re-diverge. Each value
|
|
7295
|
-
* is either:
|
|
7296
|
-
*
|
|
7297
|
-
* - a {@link ProjectionTransform} (built with the standalone authoring helpers,
|
|
7298
|
-
* e.g. `preview('body', 120)` — RFC §5.2 `textPreview: preview('body', 120)`),
|
|
7299
|
-
* or
|
|
7300
|
-
* - a bare `string` — the **identity shorthand**: project that source path
|
|
7301
|
-
* through unchanged (e.g. `{ postId: 'postId' }`).
|
|
7302
|
-
*
|
|
7303
|
-
* `Readonly` so the recorded declaration metadata is not mutated downstream.
|
|
7304
|
-
* Issue #121 scopes this to the declaration shape; the maintenance graph /
|
|
7305
|
-
* compile injection that consumes the IR is #124/#125.
|
|
7306
|
-
*
|
|
7307
|
-
* @example `{ postId: 'postId', textPreview: preview('body', 120) }`
|
|
7308
|
-
*/
|
|
7309
|
-
type RelationProjection = Readonly<Record<string, ProjectionTransform | string>>;
|
|
7310
|
-
interface RelationOptions {
|
|
7311
|
-
limit?: RelationLimitOptions;
|
|
7312
|
-
order?: 'ASC' | 'DESC';
|
|
7313
|
-
/**
|
|
7314
|
-
* Optional human-readable description of the relation (issue #166, follow-up of
|
|
7315
|
-
* #154), supplied via `@hasMany/@belongsTo/@hasOne(() => T, keyBind, { description })`.
|
|
7316
|
-
* Pure documentation metadata — it does NOT affect navigation, key binding, or any
|
|
7317
|
-
* runtime behaviour. When present it is propagated to the relation entry in
|
|
7318
|
-
* `manifest.json` ({@link ManifestRelation.description}) and surfaces as a trailing
|
|
7319
|
-
* `# …` comment on the relation's field in the generated Python result TypedDict /
|
|
7320
|
-
* dataclass. Absent → byte-for-byte unchanged output (backward compatible).
|
|
7321
|
-
*/
|
|
7322
|
-
description?: string;
|
|
7323
|
-
/**
|
|
7324
|
-
* Named maintenance preset (Epic #118). Omitted = plain read-only navigation,
|
|
7325
|
-
* fully backward compatible with the historical relation decorators.
|
|
7326
|
-
*/
|
|
7327
|
-
pattern?: RelationPattern;
|
|
7328
|
-
read?: RelationReadOptions;
|
|
7329
|
-
write?: RelationWriteOptions;
|
|
7330
|
-
projection?: RelationProjection;
|
|
7331
|
-
/**
|
|
7332
|
-
* Lowered versioned-pattern declaration (issue #152, section B). Present only when
|
|
7333
|
-
* the relation declares `pattern: 'versionedLatest' | 'versionedHistory'` via the
|
|
7334
|
-
* `@hasOne/@hasMany(() => Target, (self, source) => ({...}))` callback form. The
|
|
7335
|
-
* declaring model is the SOURCE (a revision); the relation target is the
|
|
7336
|
-
* DESTINATION view row. The adapter
|
|
7337
|
-
* (`src/relation/maintenance-view-adapter.ts`) lowers this to the SAME `defineView`
|
|
7338
|
-
* snapshot IR the old `defineVersioned` produced (latest = overwrite, history =
|
|
7339
|
-
* append). `on` are the lifecycle events; `project` is the lowered projection map.
|
|
7340
|
-
*/
|
|
7341
|
-
versioned?: {
|
|
7342
|
-
readonly mode: 'latest' | 'history';
|
|
7343
|
-
readonly on: readonly MaintainEvent[];
|
|
7344
|
-
readonly project: ProjectionMap;
|
|
7345
|
-
readonly consistency?: MaintainConsistency;
|
|
7346
|
-
readonly updateMode?: MaintainUpdateMode;
|
|
7347
|
-
};
|
|
7348
|
-
}
|
|
7349
|
-
/**
|
|
7350
|
-
* Descriptor for a `refs` relation (issue #197, Pattern 2 Embedded Refs read
|
|
7351
|
-
* side). A `refs` relation resolves an **inline id-list** held on the parent row
|
|
7352
|
-
* — e.g. `PPost.tagRefs: { tagId: string }[]` — into the referenced child
|
|
7353
|
-
* bodies, in ONE deduped `BatchGetItem` (never per-ref `GetItem`).
|
|
7354
|
-
*
|
|
7355
|
-
* Unlike `hasMany`/`belongsTo`/`hasOne`, whose key is bound from a parent
|
|
7356
|
-
* **scalar** field ({@link RelationMetadata.keyBinding} — `target key ← parent
|
|
7357
|
-
* scalar`), a `refs` key is bound from EACH ELEMENT of a parent LIST attribute:
|
|
7358
|
-
* the traversal reads `parent[from]` (a list), pulls `.key` off every element,
|
|
7359
|
-
* and fans those out into a single BatchGet against the target. This descriptor
|
|
7360
|
-
* captures exactly that list-valued source shape, which the scalar `keyBinding`
|
|
7361
|
-
* `Record<string,string>` cannot express.
|
|
7362
|
-
*
|
|
7363
|
-
* `keyBinding` on a `refs` relation still records the **target child key field ←
|
|
7364
|
-
* element key name** mapping (e.g. `{ tagId: 'tagId' }`), so key building /
|
|
7365
|
-
* projection reuse the existing scalar machinery per resolved element; `refs`
|
|
7366
|
-
* only adds *where the scalar values come from* (the parent list), not a new key
|
|
7367
|
-
* grammar. That keeps `refs` byte-compatible with every scalar-keyBinding
|
|
7368
|
-
* consumer (manifest, planner, dedupe) — an unknown-to-them relation kind is
|
|
7369
|
-
* simply carried through with a valid scalar `keyBinding`.
|
|
7370
|
-
*/
|
|
7371
|
-
interface RefsBinding {
|
|
7372
|
-
/** The parent LIST attribute holding the inline reference elements (e.g. `'tagRefs'`). */
|
|
7373
|
-
from: string;
|
|
7374
|
-
/** The key field to read off EACH list element (e.g. `'tagId'`). */
|
|
7375
|
-
key: string;
|
|
7376
|
-
}
|
|
7377
|
-
interface RelationMetadata {
|
|
7378
|
-
type: 'hasMany' | 'hasOne' | 'belongsTo' | 'refs';
|
|
7379
|
-
propertyName: string;
|
|
7380
|
-
targetFactory: () => new (...args: unknown[]) => unknown;
|
|
7381
|
-
keyBinding: Record<string, string>;
|
|
7382
|
-
/**
|
|
7383
|
-
* List-valued source descriptor for a `refs` relation (issue #197). Present
|
|
7384
|
-
* IFF `type === 'refs'`; the traversal reads the parent list `refs.from`, pulls
|
|
7385
|
-
* `refs.key` off each element, and resolves the referenced child bodies in one
|
|
7386
|
-
* deduped BatchGet. Absent for every scalar-keyed relation (byte-compatible).
|
|
7387
|
-
*/
|
|
7388
|
-
refs?: RefsBinding;
|
|
7389
|
-
options?: RelationOptions;
|
|
7390
|
-
}
|
|
7391
|
-
/**
|
|
7392
|
-
* The scalar-aggregate value an `@aggregate` field derives from its source entity
|
|
7393
|
-
* (Epic #118 §5.2 counter / latest preset; issue #122). It is the function-form
|
|
7394
|
-
* IR built by the {@link count} / {@link max} value helpers — the aggregation an
|
|
7395
|
-
* `@aggregate(() => Source, keyBinding, { pattern, value })` field maintains:
|
|
7396
|
-
*
|
|
7397
|
-
* - `count` — the cardinality of source rows matched by the key binding (no
|
|
7398
|
-
* source field; e.g. `postCount!: number` ← `count()`).
|
|
7399
|
-
* - `max` — the maximum value of a named source `field` (e.g. `lastPostAt!:
|
|
7400
|
-
* string` ← `max('createdAt')`).
|
|
7401
|
-
*
|
|
7402
|
-
* Recorded purely as declaration metadata (issue #122 scope = declaration
|
|
7403
|
-
* surface): the maintenance graph (#124) / compile injection (#125) consume it;
|
|
7404
|
-
* the effect itself is out of scope here. Kept open-ended via the discriminated
|
|
7405
|
-
* `op` so a future aggregation (`min`, `sum`, …) extends the union without a
|
|
7406
|
-
* breaking change.
|
|
7407
|
-
*
|
|
7408
|
-
* @example `count()` → `{ op: 'count' }`
|
|
7409
|
-
* @example `max('createdAt')` → `{ op: 'max', field: 'createdAt' }`
|
|
7410
|
-
*/
|
|
7411
|
-
type AggregateValue = {
|
|
7412
|
-
readonly op: 'count';
|
|
7413
|
-
} | {
|
|
7414
|
-
readonly op: 'max';
|
|
7415
|
-
readonly field: string;
|
|
7416
|
-
};
|
|
7417
|
-
/**
|
|
7418
|
-
* Options for an `@aggregate` field (Epic #118 §5.2 counter / latest preset;
|
|
7419
|
-
* issue #122). Mirrors the maintenance vocabulary a relation declares so the two
|
|
7420
|
-
* authoring surfaces stay in lockstep:
|
|
7421
|
-
*
|
|
7422
|
-
* - `pattern` reuses {@link RelationPattern} (e.g. `'counter'`) — the named
|
|
7423
|
-
* maintenance preset, NOT re-defined here.
|
|
7424
|
-
* - `value` is the {@link AggregateValue} the field maintains (`count()` /
|
|
7425
|
-
* `max('createdAt')`).
|
|
7426
|
-
* - `write` reuses {@link RelationWriteOptions} verbatim — `maintainedOn`
|
|
7427
|
-
* triggers (`Post.created` / `Post.removed`, the confirmed shared
|
|
7428
|
-
* `created|updated|removed` event vocabulary) plus `consistency`
|
|
7429
|
-
* (`transactional|eventual`) / `updateMode` (`mutation|stream`). These are NOT
|
|
7430
|
-
* re-defined; they are the same shared types B (#121) added.
|
|
7431
|
-
*/
|
|
7432
|
-
interface AggregateOptions {
|
|
7433
|
-
/** Named maintenance preset (typically `'counter'`); reuses {@link RelationPattern}. */
|
|
7434
|
-
pattern?: RelationPattern;
|
|
7435
|
-
/** The scalar aggregation this field maintains (`count()` / `max('createdAt')`). */
|
|
7436
|
-
value: AggregateValue;
|
|
7437
|
-
/** Cross-entity maintenance triggers / consistency / update-mode (reused from B). */
|
|
7438
|
-
write?: RelationWriteOptions;
|
|
7439
|
-
}
|
|
7440
|
-
/**
|
|
7441
|
-
* Declaration metadata recorded by an `@aggregate` field decorator (issue #122).
|
|
7442
|
-
*
|
|
7443
|
-
* An `@aggregate` is a **scalar** field (`postCount!: number`,
|
|
7444
|
-
* `lastPostAt!: string`) whose value is derived — by aggregating rows of a source
|
|
7445
|
-
* entity resolved by {@link targetFactory} + {@link keyBinding} — rather than
|
|
7446
|
-
* stored directly. It is therefore recorded in its **own** metadata array
|
|
7447
|
-
* (`EntityMetadata.aggregates`), distinct from both {@link FieldMetadata} (a
|
|
7448
|
-
* plain stored attribute) and {@link RelationMetadata} (a navigation yielding
|
|
7449
|
-
* model instances): an aggregate carries a source binding like a relation, but
|
|
7450
|
-
* surfaces a scalar value like a field. The collector path mirrors relations
|
|
7451
|
-
* (decorator pushes, `@model` drains).
|
|
7452
|
-
*
|
|
7453
|
-
* Issue #122 scope is the declaration surface + metadata pass-through only; the
|
|
7454
|
-
* aggregation effect's compile / runtime (#124/#125/#127) is out of scope.
|
|
7455
|
-
*/
|
|
7456
|
-
interface AggregateMetadata {
|
|
7457
|
-
/** The scalar field the aggregate value is written to. */
|
|
7458
|
-
propertyName: string;
|
|
7459
|
-
/** Resolves the source entity whose rows are aggregated. */
|
|
7460
|
-
targetFactory: () => new (...args: unknown[]) => unknown;
|
|
7461
|
-
/** The key binding selecting the source rows (target key field → source field). */
|
|
7462
|
-
keyBinding: Record<string, string>;
|
|
7463
|
-
/** The aggregate preset / value / maintenance-write declaration. */
|
|
7464
|
-
options: AggregateOptions;
|
|
7465
|
-
}
|
|
7466
|
-
interface EmbeddedMetadata {
|
|
7467
|
-
propertyName: string;
|
|
7468
|
-
modelFactory: () => new (...args: unknown[]) => unknown;
|
|
7469
|
-
}
|
|
7470
|
-
interface EntityMetadata {
|
|
7471
|
-
tableName: string;
|
|
7472
|
-
prefix: string;
|
|
7473
|
-
fields: FieldMetadata[];
|
|
7474
|
-
primaryKey: KeyDefinition | null;
|
|
7475
|
-
gsiDefinitions: GsiDefinition[];
|
|
7476
|
-
relations: RelationMetadata[];
|
|
7477
|
-
/** Scalar `@aggregate` fields (Epic #118 §5.2 counter / latest; issue #122). */
|
|
7478
|
-
aggregates: AggregateMetadata[];
|
|
7479
|
-
embeddedFields: EmbeddedMetadata[];
|
|
7480
|
-
/**
|
|
7481
|
-
* The model's maintenance kind (issue #152). `'entity'` (default; also assumed
|
|
7482
|
-
* when absent, for hand-built metadata in tests) is a plain stored entity;
|
|
7483
|
-
* `'materializedView'` / `'sparseView'` mark it a maintained view row fed by
|
|
7484
|
-
* {@link maintainedFrom}.
|
|
7485
|
-
*/
|
|
7486
|
-
kind?: ModelKind;
|
|
7487
|
-
/**
|
|
7488
|
-
* Optional human-readable description of the entity (issue #154), supplied via
|
|
7489
|
-
* `@model({ description })`. Pure documentation metadata — it does NOT affect
|
|
7490
|
-
* storage, keys, or any runtime behaviour. When present it is propagated to the
|
|
7491
|
-
* entity entry in `manifest.json` ({@link ManifestEntity.description}) and
|
|
7492
|
-
* surfaces as the class docstring in the generated Python `types.py`. Absent →
|
|
7493
|
-
* byte-for-byte unchanged output (backward compatible).
|
|
7494
|
-
*/
|
|
7495
|
-
description?: string;
|
|
7496
|
-
/**
|
|
7497
|
-
* Class-level `@maintainedFrom(...)` declarations (issue #152). Non-empty only on
|
|
7498
|
-
* a `materializedView` / `sparseView` model; each is one source slice the view
|
|
7499
|
-
* row is maintained from. The adapter lowers these to the maintenance graph's
|
|
7500
|
-
* `ViewDefinition` IR (replacing the old `defineView` / `ViewRegistry` path).
|
|
7501
|
-
* Absent / empty for a plain entity.
|
|
7502
|
-
*/
|
|
7503
|
-
maintainedFrom?: MaintainedFromMetadata[];
|
|
7504
|
-
/**
|
|
7505
|
-
* Whether the model is flagged **CDC-parseable** by `@cdcProjected()` (issue
|
|
7506
|
-
* #153). It carries NO runtime side effect — it is purely a compile-/registration-
|
|
7507
|
-
* time capability marker: `Model.fromChange(event)` (and, transitively,
|
|
7508
|
-
* `DDBModel.subscribe` routing to this model) is gated on it, and it is the SSoT
|
|
7509
|
-
* the codegen reads to emit the `CdcModelRegistry` module augmentation that types
|
|
7510
|
-
* `subscribe`'s handlers by model name. `true` only on a model carrying the
|
|
7511
|
-
* decorator; absent (⇒ falsy) for every plain entity, so a non-`@cdcProjected`
|
|
7512
|
-
* model is byte-for-byte unchanged (backward compatible). Deliberately kept OFF
|
|
7513
|
-
* the manifest / bridge (it is a host-runtime + TS-typing concern, like the
|
|
7514
|
-
* maintenance signals — not a physical-schema fact like {@link ttlAttribute}).
|
|
7515
|
-
*/
|
|
7516
|
-
cdcProjected?: boolean;
|
|
7517
|
-
/**
|
|
7518
|
-
* The physical attribute name of the model's `@ttl` field, when one is declared
|
|
7519
|
-
* (issue #172, Epic #167 — CFn generator C4). DynamoDB TTL requires a single
|
|
7520
|
-
* Number attribute holding epoch seconds; `@model` derives this from the (unique)
|
|
7521
|
-
* field carrying {@link FieldMetadata.ttl} and throws if more than one field on the
|
|
7522
|
-
* SAME model is `@ttl` (the cross-entity, same-physical-table single-TTL rule is
|
|
7523
|
-
* enforced downstream, at manifest build, where all entities on a table are known).
|
|
7524
|
-
* A non-key field's physical attribute name is its property name, so this is the
|
|
7525
|
-
* `@ttl` field's property name. Propagated to the manifest
|
|
7526
|
-
* ({@link ManifestEntity.ttlAttribute}) — the SSoT the CloudFormation emitter reads
|
|
7527
|
-
* to render `TimeToLiveSpecification`. Absent when the model declares no `@ttl`.
|
|
7528
|
-
*/
|
|
7529
|
-
ttlAttribute?: string;
|
|
7530
|
-
}
|
|
7531
|
-
|
|
7532
|
-
/**
|
|
7533
|
-
* Decorator-metadata → maintenance-graph `ViewDefinition` adapter (issue #152).
|
|
7534
|
-
*
|
|
7535
|
-
* ## What this replaces
|
|
7536
|
-
*
|
|
7537
|
-
* The old `defineView` / `defineVersioned` builders (deleted) registered
|
|
7538
|
-
* `ViewDefinition`s into a side `ViewRegistry`, which the maintenance graph read.
|
|
7539
|
-
* That builder carried a registration side-effect + a `generation` counter (a
|
|
7540
|
-
* silent-drop guard). This adapter removes that whole path: it derives the SAME
|
|
7541
|
-
* `ViewDefinition` IR purely from the declarative `MetadataRegistry` — the
|
|
7542
|
-
* Contract-layer SSoT, whose own `generation` counter is the single
|
|
7543
|
-
* invalidation source — so a view declared via `@model({ kind })` + `@maintainedFrom`
|
|
7544
|
-
* (and a versioned relation declared via `@hasOne/@hasMany` `pattern`) is discovered
|
|
7545
|
-
* with no separate registry.
|
|
7546
|
-
*
|
|
7547
|
-
* ## IR invariance
|
|
7548
|
-
*
|
|
7549
|
-
* The `ViewDefinition` / `ViewSourceSlice` shapes are UNCHANGED. The maintenance
|
|
7550
|
-
* graph, the K (#130) stream/drain, and the L (#131) rebuild consume the adapter's
|
|
7551
|
-
* output exactly as they consumed `ViewRegistry.getAll()`. Only the SOURCE of the
|
|
7552
|
-
* definitions moved from a builder side-effect to declarative metadata.
|
|
7553
|
-
*
|
|
7554
|
-
* ## Order independence (issue #152 hardening)
|
|
7555
|
-
*
|
|
7556
|
-
* Stacked `@maintainedFrom` declarations carry no order meaning. The adapter rejects
|
|
7557
|
-
* — symmetrically, so the error is identical whichever order the decorators are
|
|
7558
|
-
* stacked — any two declarations on the same view that:
|
|
7559
|
-
* 1. write the SAME projection target attribute (ambiguous duplicate projection);
|
|
7560
|
-
* 2. maintain the SAME `collection.field`;
|
|
7561
|
-
* 3. bind the SAME view-key field to a DIFFERENT source path (the view-row identity
|
|
7562
|
-
* would be inconsistent across sources).
|
|
7563
|
-
* A legitimate multi-source view (different sources filling DIFFERENT fields, all
|
|
7564
|
-
* binding the SAME key the SAME way) is fine.
|
|
7565
|
-
*/
|
|
7566
|
-
|
|
7567
|
-
type AnyModelClass = abstract new (...args: any[]) => any;
|
|
7568
|
-
/** The view source slice IR the maintenance graph consumes (one per source × view). */
|
|
7569
|
-
interface ViewSourceSlice {
|
|
7570
|
-
readonly sourceClass: AnyModelClass;
|
|
7571
|
-
readonly maintainedOn: readonly MaintainTrigger[];
|
|
7572
|
-
readonly keys: Readonly<Record<string, EffectPath>>;
|
|
7573
|
-
readonly project: ProjectionMap;
|
|
7574
|
-
readonly collection?: {
|
|
7575
|
-
readonly field: string;
|
|
7576
|
-
readonly maxItems?: number;
|
|
7577
|
-
readonly orderBy?: EffectPath;
|
|
7578
|
-
readonly orderDir?: 'ASC' | 'DESC';
|
|
7579
|
-
};
|
|
7580
|
-
readonly predicate?: MembershipPredicate;
|
|
7581
|
-
}
|
|
7582
|
-
/** The view definition IR — an independent view row maintained from one or more sources. */
|
|
7583
|
-
interface ViewDefinition {
|
|
7584
|
-
readonly name: string;
|
|
7585
|
-
readonly viewClass: AnyModelClass;
|
|
7586
|
-
readonly pattern: 'materializedView' | 'sparseView';
|
|
7587
|
-
readonly slices: readonly ViewSourceSlice[];
|
|
7588
|
-
readonly updateMode?: 'mutation' | 'stream';
|
|
7589
|
-
readonly consistency?: 'transactional' | 'eventual';
|
|
7590
|
-
}
|
|
7591
|
-
/**
|
|
7592
|
-
* Build every {@link ViewDefinition} for a registry — from `@model({ kind })` +
|
|
7593
|
-
* `@maintainedFrom` view models AND from versioned-pattern relations. This is the
|
|
7594
|
-
* adapter the maintenance graph calls in place of the deleted `ViewRegistry.getAll()`.
|
|
7595
|
-
*
|
|
7596
|
-
* @param registry The models to scan. Defaults to `MetadataRegistry.getAll()`.
|
|
7597
|
-
*/
|
|
7598
|
-
declare function collectViewDefinitions(registry?: ReadonlyMap<Function, EntityMetadata>): ViewDefinition[];
|
|
7599
|
-
|
|
7600
|
-
export { type AnyOperationDefinition as $, type ContractSpec as A, type BatchResult as B, type CdcEmulatorOptions as C, type DynamoDBOperation as D, type EventLog as E, type FaultSpec as F, type QuerySpec as G, type CommandSpec as H, type TransactionSpec as I, type ContextSpec as J, SPEC_VERSION as K, type ParamSpec as L, type ModelStatic as M, type ParamDescriptor as N, type EntityRef as O, type PutInput as P, type QueryModelContract as Q, type ReplayOptions as R, type ShardId as S, type TransactWriteExecItem as T, type Unsubscribe as U, type ViewDefinition as V, type WriteExecOptions as W, type ConditionInput as X, type Param as Y, type DefinitionMap as Z, type OperationsDocument as _, type CdcMode as a, type ModelKind as a$, type Manifest as a0, type BridgeBundle as a1, type ConditionSpec as a2, type PreparedBody as a3, type CommandContractMethodSpec as a4, type CommandResolutionTarget as a5, type CompiledFragment as a6, type CompiledMutationPlan as a7, type ComposeSpec as a8, type CompositionPlanSpec as a9, type QueryContractMethodSpec as aA, type RangeConditionSpec as aB, type ReadOperationType as aC, type TransactionItemSpec as aD, type TransactionItemType as aE, type WhenSpec as aF, type WriteOperationType as aG, assertNoCrossFragmentMaintainCollision as aH, compileFragment as aI, compileMutationPlan as aJ, compileSingleFragmentPlan as aK, resetMaintenanceGraphCache as aL, resolveLifecycle as aM, resolveMaintainers as aN, type SelectableOf as aO, type PrimaryKeyOf as aP, type RequestContext as aQ, type Middleware as aR, type ReadRequestKind as aS, type CtxModel as aT, type ReadParams as aU, type ReadRequestCtx as aV, type Item as aW, type RetryPolicy as aX, type RetryOverride as aY, type KeyDefinition as aZ, type GsiDefinition as a_, type ContractCardinality as aa, type ContractCommandResult as ab, type ContractInputArity as ac, type ContractKeySpec as ad, type ContractKind as ae, type ContractResolution as af, type DerivedConditionCheck as ag, type DerivedEdgeWrite as ah, type DerivedIdempotencyGuard as ai, type DerivedMaintainOutbox as aj, type DerivedMaintainWrite as ak, type DerivedOutboxEvent as al, type DerivedUniqueGuard as am, type DerivedUpdate as an, type EntityRefResolver as ao, type ExecutionPlanSpec as ap, type FilterSpec as aq, MAX_TRANSACT_COMPOSE_ITEMS as ar, type ManifestEntity as as, type ManifestField as at, type ManifestFieldType as au, type ManifestGsi as av, type ManifestKey as aw, type ManifestRelation as ax, type ManifestTable as ay, type OperationSpec as az, type ChangeBatch as b, type ContractQueryParams as b$, type FieldOptions as b0, type DynamoType as b1, type ProjectionTransform as b2, type MaintainEvent as b3, type MembershipPredicate as b4, type MaintainConsistency as b5, type MaintainUpdateMode as b6, type MembershipPredicateOp as b7, type RelationOptions as b8, type AggregateOptions as b9, type BatchWriteRequest as bA, CONTRACT_RANGE_FANOUT_CONCURRENCY as bB, type CdcModelRegistry as bC, type CdcSubscribeHandlers as bD, type Change as bE, type CollectionEffect as bF, type CollectionOptions as bG, type Column as bH, type ColumnMap as bI, type CommandInputShape as bJ, type CommandMethod as bK, type CommandPlan as bL, type CommandResultKind as bM, type CommandSelectShape as bN, type CondSlot as bO, type ConditionCheckInput as bP, type Connection as bQ, type ContractCallSignature as bR, type ContractCommandParams as bS, type ContractComposeNode as bT, type ContractFromRef as bU, type ContractItem as bV, type ContractKeyFieldRef as bW, type ContractKeyInput as bX, type ContractKeyRef as bY, type ContractMethodOp as bZ, type ContractParamRef as b_, type AggregateValue as ba, type SelectBuilderSpec as bb, type RawCondition as bc, type ExecutionPlan as bd, type FieldMetadata as be, type ResolvedKey as bf, type Slot as bg, type PreparedWriteExecOptions as bh, type CommandReturn as bi, type ParallelOpResult as bj, type PreparedStatement as bk, type RelationMetadata as bl, type MaintainEffect as bm, type OperationDefinition as bn, type WriteDefinitionOptions as bo, type PartialQueryKeyOf as bp, type StrictSelectSpec as bq, type ReadDefinitionOptions as br, type EntityInput as bs, type UniqueQueryKeyOf as bt, type AggregateMetadata as bu, type BatchDeleteRequest as bv, type BatchGetOptions as bw, type BatchGetRequest as bx, BatchGetResult as by, type BatchPutRequest as bz, type ChangeEvent as c, PreparedReadStatement as c$, type CounterAggregate as c0, type CounterEffect as c1, type CtxBase as c2, DEFAULT_MAX_ATTEMPTS as c3, DEFAULT_RETRY_POLICY as c4, type DeleteOptions as c5, type DeriveEffect as c6, type DescriptorBinding as c7, ENTITY_WRITES_MARKER as c8, type EdgeEffect as c9, type MaintainItem as cA, type MaintainTrigger as cB, type MaintenanceGraph as cC, type MembershipEffect as cD, type ModelRef as cE, type MutateMode as cF, type MutateOptions as cG, type MutateParallelResult as cH, type MutateTransactionResult as cI, type MutationBody as cJ, type MutationDescriptorMap as cK, type MutationFragment as cL, type MutationInputProxy as cM, type MutationInputRef as cN, type MutationIntent as cO, type NumberParam as cP, type OperationKind as cQ, PREPARE_CACHE_MAX as cR, type ParamKind as cS, type ParamStructure as cT, type PersistCtx as cU, type PersistOrigin as cV, type PlannedCommandMethod as cW, type PreparedInputProxy as cX, type PreparedParamRef as cY, type PreparedReadExecOptions as cZ, type PreparedReadRoute as c_, type EffectPath as ca, type EmbeddedMetadata as cb, type EmitEffect as cc, type EntityWritesDefinition as cd, type EntityWritesShape as ce, type ExecutableCommandContract as cf, type ExecutableQueryContract as cg, type FilterInput as ch, type FragmentCondition as ci, type FragmentConditionOperatorObject as cj, type FragmentInput as ck, type GsiDefinitionMarker as cl, type GsiOptions as cm, type IdempotencyEffect as cn, type InProcessWriteDescriptor as co, type InlineSnapshotSpec as cp, type InputArity as cq, type KeyDefinitionMarker as cr, type KeySegment as cs, type KeySlot as ct, type KeyStructure as cu, type KeyedResult as cv, LIFECYCLE_CONTRACT_MARKER as cw, type LifecycleContract as cx, type LifecycleEffects as cy, type LiteralParam as cz, type ChangeEventName as d, executeCommandMethod as d$, type PreparedWriteRoute as d0, PreparedWriteStatement as d1, type ProjectionMap as d2, type ProjectionTransformOp as d3, type PutOptions as d4, type QueryEnvelopeResult as d5, type QueryKeyOf as d6, type QueryMethod as d7, type QueryResult as d8, type ReadEnvelope as d9, type StringParam as dA, TransactionContext as dB, type UniqueEffect as dC, type Updatable as dD, type UpdateOptions as dE, type ViewSourceSlice as dF, type WriteCtx as dG, type WriteDescriptor as dH, type WriteEnvelope as dI, type WriteInput as dJ, type WriteKind as dK, type WriteLifecyclePhase as dL, type WriteMiddleware as dM, type WriteRecorder as dN, type WriteResultProjection as dO, attachModelClass as dP, buildDeleteInput as dQ, buildMaintenanceGraph as dR, buildPutInput as dS, buildUpdateInput as dT, collectViewDefinitions as dU, cond as dV, contractOfMethodSpec as dW, definePlan as dX, entityWrites as dY, executeBatchGet as dZ, executeBatchWrite as d_, type ReadOpCtx as da, type ReadOpKind as db, type ReadRouteDescriptor as dc, type ReadRouteOptions as dd, type ReadRouteResult as de, type RecordedCompose as df, type RelationBuilder as dg, type RelationConsistency as dh, type RelationLimitOptions as di, type RelationPattern as dj, type RelationProjection as dk, type RelationReadOptions as dl, type RelationSelect as dm, type RelationSpec as dn, type RelationUpdateMode as dp, type RelationWriteOptions as dq, type RequiresEffect as dr, type Resolution as ds, type RetryInfo as dt, type RetryOperationKind as du, type SegmentSpec as dv, type SegmentedKey as dw, type SelectBuilder as dx, type SelectOf as dy, type SnapshotEffect as dz, type ChangeHandler as e, executeDelete as e0, executeKeyedBatchGet as e1, executePut as e2, executeQueryMethod as e3, executeRangeFanout as e4, executeTransaction as e5, executeUpdate as e6, from as e7, getEntityWrites as e8, gsi as e9, mintContractParamRef as eA, mutation as eB, param as eC, prepare as eD, preview as eE, publicCommandModel as eF, publicQueryModel as eG, query as eH, wholeKeysSentinel as eI, identity as ea, isColumn as eb, isCommandModelContract as ec, isCommandPlan as ed, isContractComposeNode as ee, isContractFromRef as ef, isContractKeyFieldRef as eg, isContractKeyRef as eh, isContractParamRef as ei, isEntityWritesDefinition as ej, isKeySegment as ek, isLifecycleContract as el, isMaintainTrigger as em, isMutationFragment as en, isMutationInputRef as eo, isParam as ep, isPlannedCommandMethod as eq, isPreparedParamRef as er, isQueryModelContract as es, isRetryableError as et, isRetryableTransactionCancellation as eu, k as ev, key as ew, lifecyclePhaseForIntent as ex, maintainTrigger as ey, mintContractKeyFieldRef as ez, type ClockMode as f, type ConcurrentRecomputeRef as g, type StartingPosition as h, type StreamViewType as i, type SubscribeHandler as j, type SubscribeHandlers as k, buildSubscribeHandler as l, type EntityMetadata as m, type Executor as n, type ReadExecOptions as o, type ExecutorResult as p, type BatchGetExecInput as q, type WriteResult as r, type UpdateInput as s, type DeleteInput as t, type BatchWriteExecItem as u, type BatchExecOptions as v, DDBModel as w, type QueryMethodSpec as x, type CommandModelContract as y, type CommandMethodSpec as z };
|
|
5163
|
+
export { type AnyOperationDefinition as $, type EventLog as A, type BatchGetExecInput as B, type CommandReturn as C, DDBModel as D, type EffectPath as E, type FaultSpec as F, type ReplayOptions as G, type ShardId as H, type MaintainTrigger as I, type BatchResult as J, type CdcMode as K, type ChangeBatch as L, type ModelStatic as M, type ChangeEventName as N, type ClockMode as O, type PreparedWriteExecOptions as P, type QueryModelContract as Q, type ReadExecOptions as R, type Slot as S, type TransactWriteExecItem as T, type UpdateInput as U, type StartingPosition as V, type WriteExecOptions as W, type StreamViewType as X, type SubscribeHandler as Y, type SubscribeHandlers as Z, buildSubscribeHandler as _, type ParallelOpResult as a, type SelectBuilder as a$, type TransactionDefinition as a0, type CompiledFragment as a1, type CompiledMutationPlan as a2, type DerivedConditionCheck as a3, type DerivedEdgeWrite as a4, type DerivedIdempotencyGuard as a5, type DerivedMaintainOutbox as a6, type DerivedMaintainWrite as a7, type DerivedOutboxEvent as a8, type DerivedUniqueGuard as a9, type GsiDefinitionMarker as aA, type GsiOptions as aB, type InlineSnapshotSpec as aC, type Item as aD, type KeyDefinitionMarker as aE, type KeySegment as aF, type KeySlot as aG, type KeyStructure as aH, type MutateAuthoringOptions as aI, type ParamDescriptor as aJ, type ParamStructure as aK, type PartialQueryKeyOf as aL, type PersistCtx as aM, type PersistOrigin as aN, type QueryKeyOf as aO, type QueryResult as aP, type RawCondition as aQ, type ReadOpCtx as aR, type ReadOpKind as aS, type ReadParams as aT, type ReadRequestCtx as aU, type ReadRequestKind as aV, type RelationBuilder as aW, type RelationSelect as aX, type RelationSpec as aY, type RequestContext as aZ, type SegmentSpec as a_, type DerivedUpdate as aa, type EntityRefResolver as ab, MAX_TRANSACT_COMPOSE_ITEMS as ac, assertNoCrossFragmentMaintainCollision as ad, compileFragment as ae, compileMutationPlan as af, compileSingleFragmentPlan as ag, resetMaintenanceGraphCache as ah, resolveLifecycle as ai, resolveMaintainers as aj, type SelectableOf as ak, type PrimaryKeyOf as al, type MembershipPredicateOp as am, publishQuery as an, publishCommand as ao, type RetryPolicy as ap, type Middleware as aq, type CdcModelRegistry as ar, type CdcSubscribeHandlers as as, type Change as at, type Column as au, type ColumnMap as av, type CondSlot as aw, type CtxBase as ax, type CtxModel as ay, type FilterInput as az, type PreparedBody as b, type SelectOf as b0, type UniqueQueryKeyOf as b1, type Updatable as b2, type WriteCtx as b3, type WriteInput as b4, type WriteKind as b5, type WriteMiddleware as b6, cond as b7, entityWrites as b8, getEntityWrites as b9, gsi as ba, identity as bb, k as bc, key as bd, mutate as be, preview as bf, when as bg, type PreparedStatement as c, type SegmentedKey as d, type ProjectionTransform as e, type MaintainEvent as f, type ProjectionMap as g, type MaintainConsistency as h, type MaintainUpdateMode as i, type MembershipPredicate as j, type QueryMethodSpec as k, type CommandModelContract as l, type CommandMethodSpec as m, type Executor as n, type DynamoDBOperation as o, type ExecutorResult as p, type PutInput as q, type WriteResult as r, type DeleteInput as s, type BatchWriteExecItem as t, type BatchExecOptions as u, type ChangeEvent as v, type CdcEmulatorOptions as w, type ChangeHandler as x, type Unsubscribe as y, type ConcurrentRecomputeRef as z };
|