graphddb 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -9
- package/dist/{chunk-6LEHSX45.js → chunk-F27INYI2.js} +2839 -1081
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +59 -3318
- package/dist/index.js +22 -1822
- package/dist/testing/index.d.ts +2 -2
- package/dist/types-D6qLpw2M.d.ts +4514 -0
- package/package.json +1 -1
- package/dist/types-CDrWiPxp.d.ts +0 -1203
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as SelectableOf, f as PrimaryKeyOf, E as Executor, g as
|
|
2
|
-
export {
|
|
1
|
+
import { S as SelectableOf, f as PrimaryKeyOf, E as Executor, g as ExecutionPlanSpec, h as SegmentedKey, i as SelectBuilderSpec, j as TransactionSpec, k as Manifest, l as ExecutionPlan, R as ResolvedKey, D as DynamoDBOperation, a as ExecutorResult, B as BatchGetExecInput, P as PutInput, W as WriteExecOptions, b as WriteResult, U as UpdateInput, c as DeleteInput, d as BatchWriteExecItem, T as TransactWriteExecItem, m as CdcEmulatorOptions, n as ChangeHandler, o as Unsubscribe, F as FaultSpec, p as ConcurrentRecomputeRef, C as ChangeEvent, q as EventLog, r as ReplayOptions, s as ShardId, t as TransactionItemSpec, u as Param, v as ParamDescriptor, w as DefinitionMap, O as OperationDefinition, e as DDBModel, M as ModelStatic, x as WriteDefinitionOptions, y as PartialQueryKeyOf, z as StrictSelectSpec, A as EntityInput, G as UniqueQueryKeyOf, H as EntityRef, I as ConditionInput, Q as QueryModelContract, J as QueryMethodSpec, K as CommandModelContract, L as CommandMethodSpec, N as ContractSpec, V as QuerySpec, X as CommandSpec, Y as ContextSpec, Z as OperationsDocument, _ as AnyOperationDefinition, $ as BridgeBundle, a0 as ConditionSpec } from './types-D6qLpw2M.js';
|
|
2
|
+
export { a1 as BatchDeleteRequest, a2 as BatchGetRequest, a3 as BatchGetResult, a4 as BatchPutRequest, a5 as BatchResult, a6 as BatchWriteRequest, a7 as CONTRACT_RANGE_FANOUT_CONCURRENCY, a8 as CdcMode, a9 as ChangeBatch, aa as ChangeEventName, ab as ClockMode, ac as Column, ad as ColumnMap, ae as CommandContractMethodSpec, af as CommandInputShape, ag as CommandMethod, ah as CommandPlan, ai as CommandResolutionTarget, aj as CommandResultKind, ak as CommandSelectShape, al as CompiledFragment, am as ComposeSpec, an as CondSlot, ao as ConditionCheckInput, ap as Connection, aq as ContractCallSignature, ar as ContractCardinality, as as ContractCommandParams, at as ContractCommandResult, au as ContractComposeNode, av as ContractFromRef, aw as ContractInputArity, ax as ContractItem, ay as ContractKeyFieldRef, az as ContractKeyInput, aA as ContractKeyRef, aB as ContractKeySpec, aC as ContractKind, aD as ContractMethodOp, aE as ContractParamRef, aF as ContractQueryParams, aG as ContractResolution, aH as DeleteOptions, aI as DeriveEffect, aJ as DerivedEdgeWrite, aK as DerivedUpdate, aL as DescriptorBinding, aM as ENTITY_WRITES_MARKER, aN as EdgeEffect, aO as EffectPath, aP as EmitEffect, aQ as EntityWritesDefinition, aR as EntityWritesShape, aS as ExecutableCommandContract, aT as ExecutableQueryContract, aU as FilterInput, aV as FilterSpec, aW as FragmentInput, aX as GsiDefinitionMarker, aY as GsiOptions, aZ as IdempotencyEffect, a_ as InProcessWriteDescriptor, a$ as InputArity, b0 as KeyDefinitionMarker, b1 as KeySegment, b2 as KeySlot, b3 as KeyStructure, b4 as KeyedResult, b5 as LIFECYCLE_CONTRACT_MARKER, b6 as LifecycleContract, b7 as LifecycleEffects, b8 as LiteralParam, b9 as ManifestEntity, ba as ManifestField, bb as ManifestFieldType, bc as ManifestGsi, bd as ManifestKey, be as ManifestRelation, bf as ManifestTable, bg as ModelRef, bh as MutateMode, bi as MutateOptions, bj as MutateParallelResult, bk as MutateTransactionResult, bl as MutationBody, bm as MutationDescriptorMap, bn as MutationFragment, bo as MutationInputProxy, bp as MutationInputRef, bq as MutationIntent, br as NumberParam, bs as OperationKind, bt as OperationSpec, bu as ParallelOpResult, bv as ParamKind, bw as ParamSpec, bx as ParamStructure, by as PlannedCommandMethod, bz as PutOptions, bA as QueryContractMethodSpec, bB as QueryEnvelopeResult, bC as QueryKeyOf, bD as QueryMethod, bE as QueryResult, bF as RangeConditionSpec, bG as RawCondition, bH as ReadEnvelope, bI as ReadOperationType, bJ as ReadRouteDescriptor, bK as ReadRouteOptions, bL as ReadRouteResult, bM as RecordedCompose, bN as RelationBuilder, bO as RelationSelect, bP as RelationSpec, bQ as RequiresEffect, bR as Resolution, bS as SPEC_VERSION, bT as SegmentSpec, bU as SelectBuilder, bV as SelectOf, bW as StartingPosition, bX as StreamViewType, bY as StringParam, bZ as TransactionContext, b_ as TransactionItemType, b$ as UniqueEffect, c0 as Updatable, c1 as UpdateOptions, c2 as WhenSpec, c3 as WriteDescriptor, c4 as WriteEnvelope, c5 as WriteLifecyclePhase, c6 as WriteOperationType, c7 as WriteRecorder, c8 as WriteResultProjection, c9 as attachModelClass, ca as buildDeleteInput, cb as buildPutInput, cc as buildUpdateInput, cd as compileFragment, ce as compileMutationPlan, cf as compileSingleFragmentPlan, cg as cond, ch as contractOfMethodSpec, ci as definePlan, cj as entityWrites, ck as executeBatchGet, cl as executeBatchWrite, cm as executeCommandMethod, cn as executeDelete, co as executeKeyedBatchGet, cp as executePut, cq as executeQueryMethod, cr as executeRangeFanout, cs as executeTransaction, ct as executeUpdate, cu as from, cv as getEntityWrites, cw as gsi, cx as isColumn, cy as isCommandModelContract, cz as isCommandPlan, cA as isContractComposeNode, cB as isContractFromRef, cC as isContractKeyFieldRef, cD as isContractKeyRef, cE as isContractParamRef, cF as isEntityWritesDefinition, cG as isKeySegment, cH as isLifecycleContract, cI as isMutationFragment, cJ as isMutationInputRef, cK as isParam, cL as isPlannedCommandMethod, cM as isQueryModelContract, cN as k, cO as key, cP as lifecyclePhaseForIntent, cQ as mintContractKeyFieldRef, cR as mintContractParamRef, cS as mutation, cT as param, cU as publicCommandModel, cV as publicQueryModel, cW as query, cX as resolveLifecycle, cY as wholeKeysSentinel } from './types-D6qLpw2M.js';
|
|
3
3
|
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
4
4
|
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
5
5
|
|
|
@@ -122,6 +122,60 @@ declare const BATCH_GET_MAX_KEYS = 100;
|
|
|
122
122
|
*/
|
|
123
123
|
declare const BATCH_WRITE_MAX_ITEMS = 25;
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The single source of truth for **relation execution staging** (issue #70),
|
|
127
|
+
* shared by the static SSoT generator (`src/spec/operations.ts`,
|
|
128
|
+
* {@link buildQuerySpec}) and the TS host runtime
|
|
129
|
+
* (`src/relation/traversal.ts`, {@link resolveRelations}). It is the TS analogue
|
|
130
|
+
* of the Python runtime's `_relation_stages` / `_plan_concurrency`
|
|
131
|
+
* (`python/graphddb_runtime/runtime.py`).
|
|
132
|
+
*
|
|
133
|
+
* ## Why one shared module (anti-drift)
|
|
134
|
+
*
|
|
135
|
+
* #70a records an {@link ExecutionPlanSpec} (`groups` + `concurrency`) in the
|
|
136
|
+
* SSoT; #70b made the Python runtime **honor** it; #70c makes the TS host runtime
|
|
137
|
+
* honor it too. For the two runtimes to make **identical** plan-declared
|
|
138
|
+
* decisions, TS must consume *the same plan facts* the generator serialized — not
|
|
139
|
+
* re-derive parallelism by an independent "parallelize every sibling" walk. The
|
|
140
|
+
* host runtime has no generated JSON to load (it works off live decorator
|
|
141
|
+
* metadata + a `select` tree), so it derives the plan from that live tree using
|
|
142
|
+
* **this exact function** — the same one the generator calls to produce the
|
|
143
|
+
* serialized `executionPlan`. Generator and runtime therefore cannot drift: they
|
|
144
|
+
* are one codepath. {@link deriveExecutionPlan} is the seam.
|
|
145
|
+
*
|
|
146
|
+
* ## How the runtime obtains the plan
|
|
147
|
+
*
|
|
148
|
+
* The generator produces `operations[]` (root + relation ops, each with a
|
|
149
|
+
* `resultPath`) and runs {@link deriveExecutionPlan} over their result paths. The
|
|
150
|
+
* host runtime walks the live `select` tree with {@link relationResultPaths},
|
|
151
|
+
* which emits the **identical** ordered `resultPath` list
|
|
152
|
+
* (`buildRelationOperations` is refactored to use it), and runs the **same**
|
|
153
|
+
* {@link deriveExecutionPlan} over `['$', ...those]`. The resulting
|
|
154
|
+
* {@link ExecutionPlanSpec} is threaded through the recursive relation traversal:
|
|
155
|
+
* at each parent level the traversal honors the plan's grouping (which child ops
|
|
156
|
+
* are an independent, concurrency-eligible stage) and the declared `concurrency`
|
|
157
|
+
* bound — reading them from the plan, never re-deciding them.
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A **resolved** relation execution plan as the host runtime consumes it: the
|
|
162
|
+
* serialized {@link ExecutionPlanSpec} (`groups` over operation indices +
|
|
163
|
+
* `concurrency`) together with the `resultPath` each operation index produces, so
|
|
164
|
+
* the recursive traversal can locate "the operations at this parent level" and
|
|
165
|
+
* read their declared stage from the plan.
|
|
166
|
+
*
|
|
167
|
+
* Built once at a read's entry point ({@link buildRelationExecutionPlan}) and
|
|
168
|
+
* threaded, unchanged, through every recursion level. A read whose `select` has
|
|
169
|
+
* **no** relations (or where the plan would be a single op) carries no plan; the
|
|
170
|
+
* traversal then has nothing to stage.
|
|
171
|
+
*/
|
|
172
|
+
interface RelationExecutionPlan {
|
|
173
|
+
/** The serialized staging facts (issue #70a) the traversal honors. */
|
|
174
|
+
readonly plan: ExecutionPlanSpec;
|
|
175
|
+
/** Operation index → the `resultPath` it writes (index 0 = root `$`). */
|
|
176
|
+
readonly resultPaths: readonly string[];
|
|
177
|
+
}
|
|
178
|
+
|
|
125
179
|
type DynamoType = 'S' | 'N' | 'BOOL' | 'B' | 'SS' | 'NS' | 'L' | 'M';
|
|
126
180
|
interface FieldOptions {
|
|
127
181
|
format?: 'datetime' | 'date';
|
|
@@ -412,732 +466,6 @@ declare function compileFilterExpression(filter: Record<string, unknown> | undef
|
|
|
412
466
|
*/
|
|
413
467
|
declare function evaluateFilter(item: Record<string, unknown>, filter: Record<string, unknown>): boolean;
|
|
414
468
|
|
|
415
|
-
/**
|
|
416
|
-
* Parameter placeholders for the parameterized query / command definition DSL
|
|
417
|
-
* (issue #41, Python-bridge Phase 0a).
|
|
418
|
-
*
|
|
419
|
-
* A `Param<T>` is a **branded placeholder** standing in for a value that is not
|
|
420
|
-
* known at definition time but is supplied later (at execution, e.g. from
|
|
421
|
-
* Python). It carries:
|
|
422
|
-
*
|
|
423
|
-
* - the **TypeScript value type** `T` it represents (`string`, `number`, or a
|
|
424
|
-
* string-literal union), preserved through the IR and the inferred return
|
|
425
|
-
* types of `defineQueries` / `defineCommands`; and
|
|
426
|
-
* - a **runtime descriptor** (`kind` + optional `literals`) that the static
|
|
427
|
-
* planner (#42) and the code generator read to emit `operations.json`.
|
|
428
|
-
*
|
|
429
|
-
* Placeholders are created with `param.string()`, `param.number()`, and
|
|
430
|
-
* `param.literal(...)`. They are *only* legal at value positions inside a
|
|
431
|
-
* parameterized key / changes structure passed to the `define*` entry points —
|
|
432
|
-
* never to the live `Model.query` / `Model.put` runtime API, whose types remain
|
|
433
|
-
* uncontaminated by params.
|
|
434
|
-
*/
|
|
435
|
-
/** Brand tag preventing a plain object from structurally matching a {@link Param}. */
|
|
436
|
-
declare const PARAM_BRAND: unique symbol;
|
|
437
|
-
/** Runtime discriminant for a parameter placeholder. */
|
|
438
|
-
type ParamKind = 'string' | 'number' | 'literal' | 'array';
|
|
439
|
-
/**
|
|
440
|
-
* A branded parameter placeholder representing a value of TypeScript type `T`
|
|
441
|
-
* that is bound at execution time rather than definition time.
|
|
442
|
-
*
|
|
443
|
-
* The brand (`[PARAM_BRAND]: T`) keeps `Param<string>` and `Param<number>`
|
|
444
|
-
* distinct and prevents a bare value from being mistaken for a placeholder.
|
|
445
|
-
*
|
|
446
|
-
* @typeParam T - The value type this placeholder stands in for.
|
|
447
|
-
*/
|
|
448
|
-
interface Param<out T> {
|
|
449
|
-
/** @internal Type brand carrying the represented value type. */
|
|
450
|
-
readonly [PARAM_BRAND]: T;
|
|
451
|
-
/** Runtime discriminant: `'string'` | `'number'` | `'literal'` | `'array'`. */
|
|
452
|
-
readonly kind: ParamKind;
|
|
453
|
-
/**
|
|
454
|
-
* For `param.literal(...)`, the allowed literal values (preserved at runtime
|
|
455
|
-
* for the generator). `undefined` for `string` / `number`.
|
|
456
|
-
*/
|
|
457
|
-
readonly literals?: readonly T[];
|
|
458
|
-
/**
|
|
459
|
-
* For `param.array({...})`, the descriptor of each element's fields. Lets the
|
|
460
|
-
* transaction planner (#46) type `forEach` element references and emit the
|
|
461
|
-
* `{item.<field>}` templates. `undefined` for scalar params.
|
|
462
|
-
*/
|
|
463
|
-
readonly element?: Readonly<Record<string, Param<unknown>>>;
|
|
464
|
-
}
|
|
465
|
-
/** A `Param` whose represented value type is `string`. */
|
|
466
|
-
type StringParam = Param<string>;
|
|
467
|
-
/** A `Param` whose represented value type is `number`. */
|
|
468
|
-
type NumberParam = Param<number>;
|
|
469
|
-
/** A `Param` whose represented value type is the literal union `L`. */
|
|
470
|
-
type LiteralParam<L extends string | number> = Param<L>;
|
|
471
|
-
/** The element-field descriptor shape accepted by {@link param.array}. */
|
|
472
|
-
type ArrayElementShape = Record<string, Param<unknown>>;
|
|
473
|
-
/**
|
|
474
|
-
* The TypeScript element type implied by an {@link ArrayElementShape} `E`: each
|
|
475
|
-
* field carries the value type its placeholder represents.
|
|
476
|
-
*/
|
|
477
|
-
type ElementOf<E extends ArrayElementShape> = {
|
|
478
|
-
[K in keyof E]: E[K] extends Param<infer V> ? V : never;
|
|
479
|
-
};
|
|
480
|
-
/** A `Param` standing in for an **array** whose elements have shape `E`. */
|
|
481
|
-
type ArrayParam<E extends ArrayElementShape> = Param<ElementOf<E>[]> & {
|
|
482
|
-
readonly element: E;
|
|
483
|
-
};
|
|
484
|
-
/** Allowed scalar value types a placeholder may stand in for. */
|
|
485
|
-
type ParamValue = string | number;
|
|
486
|
-
/**
|
|
487
|
-
* Placeholder factory. Each call returns a branded {@link Param} carrying both
|
|
488
|
-
* the TypeScript value type and a runtime descriptor.
|
|
489
|
-
*/
|
|
490
|
-
declare const param: {
|
|
491
|
-
/** A placeholder for a `string` value. */
|
|
492
|
-
readonly string: () => StringParam;
|
|
493
|
-
/** A placeholder for a `number` value. */
|
|
494
|
-
readonly number: () => NumberParam;
|
|
495
|
-
/**
|
|
496
|
-
* A placeholder constrained to one of the given literal values. The value
|
|
497
|
-
* type of the resulting {@link Param} is the **union of the literals**, so it
|
|
498
|
-
* is preserved precisely in the IR and the inferred definition types.
|
|
499
|
-
*
|
|
500
|
-
* @example
|
|
501
|
-
* ```ts
|
|
502
|
-
* param.literal('active', 'disabled'); // Param<'active' | 'disabled'>
|
|
503
|
-
* ```
|
|
504
|
-
*/
|
|
505
|
-
readonly literal: <const L extends readonly [ParamValue, ...ParamValue[]]>(...values: L) => LiteralParam<L[number]>;
|
|
506
|
-
/**
|
|
507
|
-
* A placeholder for an **array** parameter whose elements have the given field
|
|
508
|
-
* shape. Used by `defineTransaction`'s `tx.forEach(p.<arrayParam>, …)` to bind
|
|
509
|
-
* each element's fields to `{item.<field>}` templates.
|
|
510
|
-
*
|
|
511
|
-
* @example
|
|
512
|
-
* ```ts
|
|
513
|
-
* param.array({ userId: param.string(), role: param.string() });
|
|
514
|
-
* // Param<{ userId: string; role: string }[]>
|
|
515
|
-
* ```
|
|
516
|
-
*/
|
|
517
|
-
readonly array: <const E extends ArrayElementShape>(element: E) => ArrayParam<E>;
|
|
518
|
-
};
|
|
519
|
-
/** Runtime type guard: is `value` a parameter placeholder? */
|
|
520
|
-
declare function isParam(value: unknown): value is Param<unknown>;
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Serializable static operation specs and manifest (issue #42, Python-bridge
|
|
524
|
-
* Phase 0b).
|
|
525
|
-
*
|
|
526
|
-
* Everything in this module is **JSON-serializable** by construction (only
|
|
527
|
-
* strings / numbers / booleans / arrays / plain objects — no functions, classes,
|
|
528
|
-
* `Date`, `Set`, or `undefined`-valued fields are emitted). These types mirror
|
|
529
|
-
* the `graphddb_manifest.json` / `graphddb_operations.json` shapes sketched in
|
|
530
|
-
* `docs/python-bridge.md`. The generator (#43) writes them to disk; the
|
|
531
|
-
* Python runtime (#44+) interprets them.
|
|
532
|
-
*
|
|
533
|
-
* ## Templates and placeholders
|
|
534
|
-
*
|
|
535
|
-
* Key-condition / range / item / change *values* are **template strings** with
|
|
536
|
-
* two placeholder forms:
|
|
537
|
-
*
|
|
538
|
-
* - `{paramName}` — bound from the caller-supplied params at execution time.
|
|
539
|
-
* - `{result.sourceField}` — bound from a field of the **prior** operation's
|
|
540
|
-
* result item(s), used to chain relation operations.
|
|
541
|
-
*
|
|
542
|
-
* A value with no `{...}` is a literal (e.g. a fixed sort-key discriminator
|
|
543
|
-
* `PROFILE`).
|
|
544
|
-
*/
|
|
545
|
-
|
|
546
|
-
/** The schema version emitted in every manifest / operations document. */
|
|
547
|
-
declare const SPEC_VERSION: "1.0";
|
|
548
|
-
/** Field type as carried in the manifest (derived from the DynamoDB type). */
|
|
549
|
-
type ManifestFieldType = 'string' | 'number' | 'boolean' | 'binary' | 'stringSet' | 'numberSet' | 'list' | 'map';
|
|
550
|
-
interface ManifestField {
|
|
551
|
-
readonly type: ManifestFieldType;
|
|
552
|
-
/**
|
|
553
|
-
* Semantic format for `string` fields that carry a serialized temporal value.
|
|
554
|
-
* `datetime` → ISO 8601 instant, `date` → `YYYY-MM-DD`. Absent for plain
|
|
555
|
-
* fields. The Python runtime uses this to restore `datetime` on hydration,
|
|
556
|
-
* mirroring the TS hydrator's `format`-driven `Date` reconstruction.
|
|
557
|
-
*/
|
|
558
|
-
readonly format?: 'datetime' | 'date';
|
|
559
|
-
}
|
|
560
|
-
/** A key (PK or GSI) template descriptor in the manifest. */
|
|
561
|
-
interface ManifestKey {
|
|
562
|
-
/** Input field names the key is composed from (in declaration order). */
|
|
563
|
-
readonly inputFields: readonly string[];
|
|
564
|
-
/** Template for the partition-key value, e.g. `EMAIL#{email}`. */
|
|
565
|
-
readonly pkTemplate: string;
|
|
566
|
-
/** Template for the sort-key value, or `null` when the key has no sort key. */
|
|
567
|
-
readonly skTemplate: string | null;
|
|
568
|
-
}
|
|
569
|
-
interface ManifestGsi extends ManifestKey {
|
|
570
|
-
readonly indexName: string;
|
|
571
|
-
readonly unique: boolean;
|
|
572
|
-
}
|
|
573
|
-
interface ManifestRelation {
|
|
574
|
-
readonly type: 'hasMany' | 'hasOne' | 'belongsTo';
|
|
575
|
-
/** Target entity (model class) name. */
|
|
576
|
-
readonly target: string;
|
|
577
|
-
/** target field → source field (on this entity). */
|
|
578
|
-
readonly keyBinding: Readonly<Record<string, string>>;
|
|
579
|
-
}
|
|
580
|
-
interface ManifestEntity {
|
|
581
|
-
/** Declared (logical) table name. */
|
|
582
|
-
readonly table: string;
|
|
583
|
-
/** Physical table name (after `TableMapping` resolution). */
|
|
584
|
-
readonly physicalName: string;
|
|
585
|
-
/** PK prefix, e.g. `USER#`. */
|
|
586
|
-
readonly prefix: string;
|
|
587
|
-
readonly fields: Readonly<Record<string, ManifestField>>;
|
|
588
|
-
/** Primary key, or `null` when the entity has none. */
|
|
589
|
-
readonly key: ManifestKey | null;
|
|
590
|
-
readonly gsis: readonly ManifestGsi[];
|
|
591
|
-
readonly relations: Readonly<Record<string, ManifestRelation>>;
|
|
592
|
-
}
|
|
593
|
-
interface ManifestTable {
|
|
594
|
-
readonly physicalName: string;
|
|
595
|
-
}
|
|
596
|
-
interface Manifest {
|
|
597
|
-
readonly version: typeof SPEC_VERSION;
|
|
598
|
-
readonly tables: Readonly<Record<string, ManifestTable>>;
|
|
599
|
-
readonly entities: Readonly<Record<string, ManifestEntity>>;
|
|
600
|
-
}
|
|
601
|
-
interface ParamSpec {
|
|
602
|
-
readonly type: ParamKind;
|
|
603
|
-
readonly required: boolean;
|
|
604
|
-
/** Allowed literal values for `literal` params; omitted otherwise. */
|
|
605
|
-
readonly literals?: readonly (string | number)[];
|
|
606
|
-
/**
|
|
607
|
-
* For an `array` param (transaction `forEach` source), the per-element field
|
|
608
|
-
* descriptors (field name → element param spec). Omitted for scalar params.
|
|
609
|
-
*/
|
|
610
|
-
readonly element?: Readonly<Record<string, ParamSpec>>;
|
|
611
|
-
}
|
|
612
|
-
/** A `begins_with` range condition on a sort key (templated value). */
|
|
613
|
-
interface RangeConditionSpec {
|
|
614
|
-
readonly operator: 'begins_with';
|
|
615
|
-
readonly key: string;
|
|
616
|
-
/** Template value, e.g. `GROUP#` or `GROUP#{result.groupId}`. */
|
|
617
|
-
readonly value: string;
|
|
618
|
-
}
|
|
619
|
-
/** Server-side declarative filter, carried verbatim for the runtime to compile. */
|
|
620
|
-
interface FilterSpec {
|
|
621
|
-
/** The declarative {@link FilterInput} tree (JSON-safe). */
|
|
622
|
-
readonly declarative: unknown;
|
|
623
|
-
}
|
|
624
|
-
type ReadOperationType = 'Query' | 'GetItem' | 'BatchGetItem';
|
|
625
|
-
/**
|
|
626
|
-
* A single static read operation. Relation chains are expressed as multiple
|
|
627
|
-
* operations whose `resultPath` / templated key conditions wire them together.
|
|
628
|
-
*/
|
|
629
|
-
interface OperationSpec {
|
|
630
|
-
readonly type: ReadOperationType;
|
|
631
|
-
readonly tableName: string;
|
|
632
|
-
readonly indexName?: string;
|
|
633
|
-
/**
|
|
634
|
-
* Key condition: attribute name → template value. For a `BatchGetItem` this is
|
|
635
|
-
* the per-key shape (one key per resolved source item at runtime).
|
|
636
|
-
*/
|
|
637
|
-
readonly keyCondition: Readonly<Record<string, string>>;
|
|
638
|
-
readonly rangeCondition?: RangeConditionSpec;
|
|
639
|
-
/** Projected fields (entity field names). */
|
|
640
|
-
readonly projection: readonly string[];
|
|
641
|
-
readonly limit?: number;
|
|
642
|
-
readonly filter?: FilterSpec;
|
|
643
|
-
/**
|
|
644
|
-
* Where the result of this operation is placed in the assembled result.
|
|
645
|
-
* `$` is the root; `$.groups.items` a relation connection's items, etc.
|
|
646
|
-
*/
|
|
647
|
-
readonly resultPath: string;
|
|
648
|
-
/**
|
|
649
|
-
* For relation operations: the source field on the prior result whose value(s)
|
|
650
|
-
* drive this operation's `{result.*}` placeholders. Absent on the root op.
|
|
651
|
-
*/
|
|
652
|
-
readonly sourceField?: string;
|
|
653
|
-
}
|
|
654
|
-
/**
|
|
655
|
-
* The **execution plan** for a multi-operation read (issue #70a). It is the
|
|
656
|
-
* Single Source of Truth for *how* a query's {@link OperationSpec}[] is staged:
|
|
657
|
-
* which operations are independent (may run concurrently) and which must wait for
|
|
658
|
-
* a prior operation's result. Every runtime that honors it produces identical
|
|
659
|
-
* effects with identical concurrency discipline — the staging is **serialized,
|
|
660
|
-
* not re-derived** (cf. the contract decided-facts discipline).
|
|
661
|
-
*
|
|
662
|
-
* - `groups` is an ordered list of **stages**; each stage is a list of indices
|
|
663
|
-
* into the query's `operations[]`. Operations **within** a stage are mutually
|
|
664
|
-
* independent and may be issued concurrently; stages run **in order** because a
|
|
665
|
-
* later stage reads a `{result.*}` value produced by an earlier one. Stage 0 is
|
|
666
|
-
* always `[0]` (the root). Every operation index appears in exactly one stage,
|
|
667
|
-
* and the stages are a topological layering of the result-dependency graph.
|
|
668
|
-
* - `concurrency` is the declared in-flight bound a runtime applies **within**
|
|
669
|
-
* each stage (the shared {@link RELATION_TRAVERSAL_CONCURRENCY} = 16): at most
|
|
670
|
-
* this many operations of a stage are issued at once.
|
|
671
|
-
*
|
|
672
|
-
* ## Derivation (see `buildReadOperations` / `deriveExecutionPlan`)
|
|
673
|
-
*
|
|
674
|
-
* The planner emits `operations[]` in a deterministic pre-order: the root at
|
|
675
|
-
* index 0, then each relation child immediately followed by its own descendants.
|
|
676
|
-
* A child's key templates reference `{result.<sourceField>}` of the **operation
|
|
677
|
-
* whose `resultPath` is the child's parent path** — that producer is the child's
|
|
678
|
-
* sole dependency. The stage of an operation is therefore `1 + stage(parent)`:
|
|
679
|
-
* the root is stage 0; sibling relations that share a parent path land in the
|
|
680
|
-
* **same** stage (no inter-`{result.*}` dependency between siblings, so they are
|
|
681
|
-
* independent and concurrency-eligible); a grandchild that reads its parent's
|
|
682
|
-
* `{result.*}` lands one stage later. This mirrors exactly the level-by-level
|
|
683
|
-
* fan-out the TS relation runtime performs at runtime (`resolveRelations` issues
|
|
684
|
-
* sibling relations together under the same bound) — #70a only *records* it.
|
|
685
|
-
*
|
|
686
|
-
* Absent on a single-operation spec and on specs produced before #70a; a consumer
|
|
687
|
-
* without a plan falls back to one-operation-per-stage **sequential** execution
|
|
688
|
-
* (the pre-#70 behavior), so an absent plan never changes results.
|
|
689
|
-
*/
|
|
690
|
-
interface ExecutionPlanSpec {
|
|
691
|
-
/** Ordered stages; each stage is indices into `operations[]` (stage 0 = `[0]`). */
|
|
692
|
-
readonly groups: readonly (readonly number[])[];
|
|
693
|
-
/** The declared in-flight bound applied within each stage (16). */
|
|
694
|
-
readonly concurrency: number;
|
|
695
|
-
}
|
|
696
|
-
/** A query (read) definition's full execution spec. */
|
|
697
|
-
interface QuerySpec {
|
|
698
|
-
readonly params: Readonly<Record<string, ParamSpec>>;
|
|
699
|
-
readonly operations: readonly OperationSpec[];
|
|
700
|
-
/**
|
|
701
|
-
* Result cardinality of the **root** (`$`) operation, derived from the
|
|
702
|
-
* definition kind: `'one'` for `defineQuery` (a single entity object, with any
|
|
703
|
-
* relations attached directly), `'many'` for `defineList` (a `{ items, cursor }`
|
|
704
|
-
* connection). The relation runtime (#45) uses this to shape the root result:
|
|
705
|
-
* a `'one'` Query root takes the first matched item rather than returning a
|
|
706
|
-
* connection. Absent in specs produced before #45; consumers should treat an
|
|
707
|
-
* absent value as `'many'` for a `Query`/`BatchGetItem` root and `'one'` for a
|
|
708
|
-
* `GetItem` root (the pre-#45 behavior).
|
|
709
|
-
*/
|
|
710
|
-
readonly cardinality?: 'one' | 'many';
|
|
711
|
-
/**
|
|
712
|
-
* The staged execution plan (issue #70a): which {@link operations} are
|
|
713
|
-
* independent (concurrency-eligible) vs. result-dependent. Present only when the
|
|
714
|
-
* query has more than one operation (a relation chain); a single-operation read
|
|
715
|
-
* needs no plan. See {@link ExecutionPlanSpec} for the derivation and the
|
|
716
|
-
* backward-compatible (plan-absent → sequential) fallback.
|
|
717
|
-
*/
|
|
718
|
-
readonly executionPlan?: ExecutionPlanSpec;
|
|
719
|
-
}
|
|
720
|
-
type WriteOperationType = 'PutItem' | 'UpdateItem' | 'DeleteItem';
|
|
721
|
-
/**
|
|
722
|
-
* A condition expression on a write, as a JSON-serializable spec (issues #46,
|
|
723
|
-
* #81). The supported subset:
|
|
724
|
-
*
|
|
725
|
-
* - `{ kind: 'notExists' }` → `attribute_not_exists(PK)` (legacy whole-row guard).
|
|
726
|
-
* - `{ kind: 'attributeExists'; field }` → `attribute_exists(<field>)` — the named
|
|
727
|
-
* attribute (any field, incl. PK/SK) must be present. The foundation for
|
|
728
|
-
* referential-integrity derivation.
|
|
729
|
-
* - `{ kind: 'attributeNotExists'; field }` → `attribute_not_exists(<field>)` — the
|
|
730
|
-
* named attribute must be absent (field-level uniqueness / first-write guard).
|
|
731
|
-
* - `{ kind: 'equals'; fields }` → `#f = :v AND …` field equality (each value a
|
|
732
|
-
* `{param}` / literal template).
|
|
733
|
-
*/
|
|
734
|
-
type ConditionSpec = {
|
|
735
|
-
readonly kind: 'notExists';
|
|
736
|
-
} | {
|
|
737
|
-
readonly kind: 'attributeExists';
|
|
738
|
-
readonly field: string;
|
|
739
|
-
} | {
|
|
740
|
-
readonly kind: 'attributeNotExists';
|
|
741
|
-
readonly field: string;
|
|
742
|
-
} | {
|
|
743
|
-
readonly kind: 'equals';
|
|
744
|
-
readonly fields: Readonly<Record<string, string>>;
|
|
745
|
-
};
|
|
746
|
-
interface CommandSpec {
|
|
747
|
-
readonly type: WriteOperationType;
|
|
748
|
-
readonly tableName: string;
|
|
749
|
-
readonly entity: string;
|
|
750
|
-
readonly params: Readonly<Record<string, ParamSpec>>;
|
|
751
|
-
/**
|
|
752
|
-
* Key condition for `UpdateItem` / `DeleteItem` (attribute name → template).
|
|
753
|
-
* Absent for `PutItem` (the whole item is built from `item`).
|
|
754
|
-
*/
|
|
755
|
-
readonly keyCondition?: Readonly<Record<string, string>>;
|
|
756
|
-
/** The full item template for `PutItem` (field name → template / literal). */
|
|
757
|
-
readonly item?: Readonly<Record<string, string>>;
|
|
758
|
-
/** Field-level changes for `UpdateItem` (field name → template / literal). */
|
|
759
|
-
readonly changes?: Readonly<Record<string, string>>;
|
|
760
|
-
/** Optional write condition (subset). */
|
|
761
|
-
readonly condition?: ConditionSpec;
|
|
762
|
-
}
|
|
763
|
-
/**
|
|
764
|
-
* A declarative `when` guard on a transaction item (issue #46). Compares a
|
|
765
|
-
* templated left-hand value against a templated right-hand value; the item is
|
|
766
|
-
* skipped when the comparison does not hold. Both values are template strings
|
|
767
|
-
* (`{param}` / `{item.<field>}` / literal), resolved by the runtime before the
|
|
768
|
-
* comparison. The runtime compares the resolved **string** values.
|
|
769
|
-
*/
|
|
770
|
-
interface WhenSpec {
|
|
771
|
-
readonly op: 'eq' | 'ne';
|
|
772
|
-
/** Templated left-hand value (typically an `{item.<field>}` or `{param}`). */
|
|
773
|
-
readonly left: string;
|
|
774
|
-
/** Templated right-hand value (literal or another reference). */
|
|
775
|
-
readonly right: string;
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* The kind of a transaction item. The three write kinds (`Put` / `Update` /
|
|
779
|
-
* `Delete`) mutate an item; `ConditionCheck` (issue #81) is a **read-only
|
|
780
|
-
* assertion** on another item — it asserts a {@link ConditionSpec} holds for the
|
|
781
|
-
* keyed row without modifying it, and its failure cancels the **whole**
|
|
782
|
-
* `TransactWriteItems` atomically. It is the foundation for referential-integrity
|
|
783
|
-
* derivation (proposal: `requires <Entity> exists` → a `ConditionCheck` with
|
|
784
|
-
* `attribute_exists`).
|
|
785
|
-
*/
|
|
786
|
-
type TransactionItemType = 'Put' | 'Update' | 'Delete' | 'ConditionCheck';
|
|
787
|
-
/**
|
|
788
|
-
* A single templated item in a transaction. When `forEach` is present, the item
|
|
789
|
-
* is expanded **once per element** of the named array param, with each element's
|
|
790
|
-
* fields bound to the item's `{item.<field>}` placeholders.
|
|
791
|
-
*
|
|
792
|
-
* Field presence by `type`:
|
|
793
|
-
*
|
|
794
|
-
* | type | item | keyCondition | changes | add | condition |
|
|
795
|
-
* |----------------|------|--------------|---------|-----|--------------------------|
|
|
796
|
-
* | `Put` | ✓ | — | — | — | optional |
|
|
797
|
-
* | `Update` | — | ✓ | ✓ / — | ✓ / — | optional |
|
|
798
|
-
* | `Delete` | — | ✓ | — | — | optional |
|
|
799
|
-
* | `ConditionCheck` | — | ✓ | — | — | **required** (the assert) |
|
|
800
|
-
*
|
|
801
|
-
* An `Update` carries `changes` (a `SET` of named fields) and/or `add` (an atomic
|
|
802
|
-
* `ADD` of named numeric fields, issue #85) — at least one of the two. The two are
|
|
803
|
-
* distinct DynamoDB update actions: `SET #f = :v` **overwrites**, while
|
|
804
|
-
* `ADD #f :delta` **atomically increments** without a read (concurrency-safe), so
|
|
805
|
-
* a derived counter (`User.postCount += 1`) MUST be an `add`, never a `changes`
|
|
806
|
-
* `SET` (which would clobber a concurrent increment).
|
|
807
|
-
*/
|
|
808
|
-
interface TransactionItemSpec {
|
|
809
|
-
readonly type: TransactionItemType;
|
|
810
|
-
readonly tableName: string;
|
|
811
|
-
readonly entity: string;
|
|
812
|
-
/** Put: the full item template (field → template / literal). */
|
|
813
|
-
readonly item?: Readonly<Record<string, string>>;
|
|
814
|
-
/** Update / Delete / ConditionCheck: the key template (attribute → template). */
|
|
815
|
-
readonly keyCondition?: Readonly<Record<string, string>>;
|
|
816
|
-
/** Update: field-level `SET` change templates (overwrite). */
|
|
817
|
-
readonly changes?: Readonly<Record<string, string>>;
|
|
818
|
-
/**
|
|
819
|
-
* Update: field-level atomic `ADD` deltas (issue #85, derived counters). Each
|
|
820
|
-
* value is a numeric template (`{param}` / a literal number string); the runtime
|
|
821
|
-
* applies `ADD #field :delta`, an atomic increment that needs no prior read and
|
|
822
|
-
* is safe under concurrency. A negative delta decrements (e.g. `-1` on a remove).
|
|
823
|
-
*/
|
|
824
|
-
readonly add?: Readonly<Record<string, string>>;
|
|
825
|
-
/**
|
|
826
|
-
* The write / assertion condition (subset). Optional on `Put` / `Update` /
|
|
827
|
-
* `Delete`; **required** on a `ConditionCheck` (the assertion it makes).
|
|
828
|
-
*/
|
|
829
|
-
readonly condition?: ConditionSpec;
|
|
830
|
-
/** Optional declarative guard; the item is skipped when it does not hold. */
|
|
831
|
-
readonly when?: WhenSpec;
|
|
832
|
-
/** When present, expand this item once per element of the named array param. */
|
|
833
|
-
readonly forEach?: {
|
|
834
|
-
readonly source: string;
|
|
835
|
-
};
|
|
836
|
-
/**
|
|
837
|
-
* Marks a **raw marker-row** write (issue #86 uniqueness guard): the row is NOT a
|
|
838
|
-
* modeled entity, so its primary key is carried **literally** rather than derived
|
|
839
|
-
* from manifest metadata. When `true`:
|
|
840
|
-
*
|
|
841
|
-
* - a `Put` writes its `item` record **verbatim** as the stored item — the `item`
|
|
842
|
-
* already carries the synthetic `PK` / `SK` templates (`UNIQUE#…`), so the
|
|
843
|
-
* runtimes do NOT prepend a model prefix or compute GSI attributes;
|
|
844
|
-
* - a `Delete` uses its `keyCondition` (the `PK` / `SK` templates) as the row Key
|
|
845
|
-
* verbatim.
|
|
846
|
-
*
|
|
847
|
-
* The companion {@link entity} is the {@link MARKER_ROW_ENTITY} sentinel (there is
|
|
848
|
-
* no manifest entity to resolve). Absent / `false` on every modeled write (a base
|
|
849
|
-
* entity / edge / counter / ConditionCheck item), so those paths are unchanged.
|
|
850
|
-
*/
|
|
851
|
-
readonly literalKey?: boolean;
|
|
852
|
-
}
|
|
853
|
-
/** A declarative transaction definition's full execution spec. */
|
|
854
|
-
interface TransactionSpec {
|
|
855
|
-
readonly params: Readonly<Record<string, ParamSpec>>;
|
|
856
|
-
readonly items: readonly TransactionItemSpec[];
|
|
857
|
-
/**
|
|
858
|
-
* A static upper bound on the expanded item count when computable (no `forEach`
|
|
859
|
-
* present → the exact count; with `forEach` it is left absent because the
|
|
860
|
-
* element count is only known at execution — the runtime then enforces the
|
|
861
|
-
* DynamoDB ≤25 limit after expansion).
|
|
862
|
-
*/
|
|
863
|
-
readonly maxItems?: number;
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* Whether a contract is a public **read** (`'query'`) or **write** (`'command'`)
|
|
867
|
-
* interface. Mirrors {@link QueryModelContract} / {@link CommandModelContract}'s
|
|
868
|
-
* `kind` discriminant from the Contract IR (#58).
|
|
869
|
-
*/
|
|
870
|
-
type ContractKind = 'query' | 'command';
|
|
871
|
-
/**
|
|
872
|
-
* The resolution kind of a contract query method, **derived** by the planner from
|
|
873
|
-
* the internal op (never hand-written; see proposal "N+1 Safety"):
|
|
874
|
-
*
|
|
875
|
-
* - `'point'` — target keys are known (unique-key `query` / `GetItem`); a key
|
|
876
|
-
* array coalesces to one `BatchGetItem`.
|
|
877
|
-
* - `'range'` — target key set is unknown (partition `list` / `Query`); one
|
|
878
|
-
* request per partition key, so it is only ever safe for a single key.
|
|
879
|
-
*/
|
|
880
|
-
type ContractResolution = 'point' | 'range';
|
|
881
|
-
/**
|
|
882
|
-
* What input arity a contract method accepts, **derived** from its resolution:
|
|
883
|
-
*
|
|
884
|
-
* - `'either'` — a single key **or** an array (a `point` read / a known-key
|
|
885
|
-
* write; the array form is one `BatchGetItem` / batched write).
|
|
886
|
-
* - `'single'` — a single key only (a `range` read; an array would be an N+1
|
|
887
|
-
* fan-out and is rejected by construction).
|
|
888
|
-
* - `'array'` — an array only (reserved; not produced by the current resolvers).
|
|
889
|
-
*/
|
|
890
|
-
type ContractInputArity = 'single' | 'array' | 'either';
|
|
891
|
-
/**
|
|
892
|
-
* The per-key result cardinality of a contract query method, **derived** from the
|
|
893
|
-
* internal op kind (proposal "Cardinality matrix"):
|
|
894
|
-
*
|
|
895
|
-
* - `'one'` — at most one item per key (a `point` read).
|
|
896
|
-
* - `'many'` — a connection (`{ items, cursor }`) per key (a `range` read).
|
|
897
|
-
*
|
|
898
|
-
* This is the per-key shape; the input arity (single vs. array) then decides
|
|
899
|
-
* whether the overall result is bare or keyed.
|
|
900
|
-
*/
|
|
901
|
-
type ContractCardinality = 'one' | 'many';
|
|
902
|
-
/**
|
|
903
|
-
* The category of a contract command method's declared result, part of the
|
|
904
|
-
* contract (it surfaces in OpenAPI and every binding; proposal "Return values"):
|
|
905
|
-
*
|
|
906
|
-
* - `'void'` — fire-and-forget write (no body).
|
|
907
|
-
* - `'result'` — an outcome / status object (e.g. `{ ok, version }`).
|
|
908
|
-
* - `'entity'` — the updated entity (the post-write projection).
|
|
909
|
-
*/
|
|
910
|
-
type ContractCommandResult = 'void' | 'result' | 'entity';
|
|
911
|
-
/** The Key of a contract: the field names that compose its access / join key. */
|
|
912
|
-
interface ContractKeySpec {
|
|
913
|
-
/** The key field names, in declaration order (e.g. `["articleId"]`). */
|
|
914
|
-
readonly fields: readonly string[];
|
|
915
|
-
}
|
|
916
|
-
/**
|
|
917
|
-
* One External Query (Query Composition) binding on a contract query method
|
|
918
|
-
* (proposal "External Query"). It is a **build-time, in-process** read dependency
|
|
919
|
-
* on another contract referenced by name in the same SSoT — there is no protocol
|
|
920
|
-
* or transport. The runtime collects every parent key produced at this level and
|
|
921
|
-
* resolves the referenced contract **once, batched**, for all of them.
|
|
922
|
-
*
|
|
923
|
-
* A composed child MUST be `point` (proposal "N+1 Safety"): the parent step may
|
|
924
|
-
* yield N records, so a `range` child would be an N+1 fan-out.
|
|
925
|
-
*/
|
|
926
|
-
interface ComposeSpec {
|
|
927
|
-
/** The result property the composed value is attached to (e.g. `billing`). */
|
|
928
|
-
readonly as: string;
|
|
929
|
-
/** The referenced contract name (a symbol in the same SSoT). */
|
|
930
|
-
readonly contract: string;
|
|
931
|
-
/** The referenced method on that contract (e.g. `get`). */
|
|
932
|
-
readonly method: string;
|
|
933
|
-
/**
|
|
934
|
-
* An optional **logical** owner label (the bounded context that owns the
|
|
935
|
-
* referenced contract) — not a network address. Omitted when unlabeled.
|
|
936
|
-
*/
|
|
937
|
-
readonly context?: string;
|
|
938
|
-
/**
|
|
939
|
-
* The child-key binding: child key field → a `from` source path on the parent
|
|
940
|
-
* result, e.g. `{ accountId: "$.billingAccountId" }`.
|
|
941
|
-
*/
|
|
942
|
-
readonly bind: Readonly<Record<string, string>>;
|
|
943
|
-
/**
|
|
944
|
-
* The composed child's resolution. It MUST be `'point'` in a valid contract — a
|
|
945
|
-
* composed child coalesces to one `BatchGetItem` across all parent keys, so a
|
|
946
|
-
* `range` child (a partition `Query` that cannot be batched across the parent's
|
|
947
|
-
* keys) is an N+1 fan-out. The N+1 static checker (#60,
|
|
948
|
-
* {@link assertContractN1Safe}) rejects a `'range'` value on the build path; the
|
|
949
|
-
* field is widened to {@link ContractResolution} so the offending node can be
|
|
950
|
-
* carried into the assembled {@link ContractSpec} for the checker to inspect and
|
|
951
|
-
* reject with a clear message, rather than being silently unrepresentable.
|
|
952
|
-
*/
|
|
953
|
-
readonly resolution: ContractResolution;
|
|
954
|
-
/** The composed child's per-key cardinality (`'one'` for a valid `point`). */
|
|
955
|
-
readonly cardinality: ContractCardinality;
|
|
956
|
-
}
|
|
957
|
-
/**
|
|
958
|
-
* The **composition execution plan** for one query contract method (issue #70a),
|
|
959
|
-
* the contract-layer mirror of {@link ExecutionPlanSpec}. It records how a method's
|
|
960
|
-
* External Query compositions (`compose[]`) are staged relative to the parent read
|
|
961
|
-
* and to one another, and the batching discipline each composed child obeys:
|
|
962
|
-
*
|
|
963
|
-
* - `stages` is an ordered list whose entries are indices into the method's
|
|
964
|
-
* `compose[]`. The parent read (the referenced `operation`) is the implicit
|
|
965
|
-
* stage before all of these. Every compose child binds its child key purely from
|
|
966
|
-
* the **parent** result (`from('$.…')` paths), so sibling compositions are
|
|
967
|
-
* mutually independent: they form a single stage `[0, 1, …]` and may be resolved
|
|
968
|
-
* concurrently. (A composed child cannot, today, bind from another composed
|
|
969
|
-
* child — the #63 DSL only exposes parent `from` paths — so there is never a
|
|
970
|
-
* cross-composition dependency; the field is a list of stages so the shape can
|
|
971
|
-
* carry one if a future primitive introduces it.)
|
|
972
|
-
* - `concurrency` is the declared in-flight bound applied **within** a stage (the
|
|
973
|
-
* shared {@link RELATION_TRAVERSAL_CONCURRENCY} = 16).
|
|
974
|
-
* - `batchChunkSize` is the per-request key cap for a composed child read (100,
|
|
975
|
-
* the DynamoDB `BatchGetItem` limit): a composition resolves **one batched
|
|
976
|
-
* call** across all parent records (chunked at this size), never an N+1 fan-out
|
|
977
|
-
* (the composed child is `point`, enforced by the #60 N+1 checker).
|
|
978
|
-
*
|
|
979
|
-
* Emitted only when the method declares at least one composition. A method without
|
|
980
|
-
* compositions carries no plan, and a runtime without one resolves any
|
|
981
|
-
* compositions sequentially (the pre-#70 behavior) — so an absent plan never
|
|
982
|
-
* changes results.
|
|
983
|
-
*/
|
|
984
|
-
interface CompositionPlanSpec {
|
|
985
|
-
/** Ordered stages of indices into `compose[]` (independent siblings share a stage). */
|
|
986
|
-
readonly stages: readonly (readonly number[])[];
|
|
987
|
-
/** The declared in-flight bound applied within each composition stage (16). */
|
|
988
|
-
readonly concurrency: number;
|
|
989
|
-
/** Per-request key cap for a composed child's batched read (100, BatchGetItem limit). */
|
|
990
|
-
readonly batchChunkSize: number;
|
|
991
|
-
}
|
|
992
|
-
/**
|
|
993
|
-
* The serialized spec of one **query** contract method. Carries the decided facts
|
|
994
|
-
* (`resolution` / `inputArity` / `cardinality`) the planner derived in #58, a
|
|
995
|
-
* reference into `queries` by name (the internal op), and any External Query
|
|
996
|
-
* compositions. The decided facts are **serialized, not re-derived** — every
|
|
997
|
-
* runtime honors them.
|
|
998
|
-
*/
|
|
999
|
-
interface QueryContractMethodSpec {
|
|
1000
|
-
/** Derived: `'point'` (unique-key query) or `'range'` (partition list). */
|
|
1001
|
-
readonly resolution: ContractResolution;
|
|
1002
|
-
/** Derived: `'either'` for `point`, `'single'` for `range`. */
|
|
1003
|
-
readonly inputArity: ContractInputArity;
|
|
1004
|
-
/** Derived per-key result shape: `'one'` (point) or `'many'` (range). */
|
|
1005
|
-
readonly cardinality: ContractCardinality;
|
|
1006
|
-
/** The referenced read op name in `queries` (e.g. `ArticleById__get`). */
|
|
1007
|
-
readonly operation: string;
|
|
1008
|
-
/** External Query compositions on this method; omitted when there are none. */
|
|
1009
|
-
readonly compose?: readonly ComposeSpec[];
|
|
1010
|
-
/**
|
|
1011
|
-
* The composition staging + batch plan (issue #70a); present only when `compose`
|
|
1012
|
-
* is. See {@link CompositionPlanSpec}: independent compositions form one
|
|
1013
|
-
* concurrency-eligible stage after the parent read, each resolved as one batched
|
|
1014
|
-
* call (chunk ≤100). Absent → resolve compositions sequentially (pre-#70).
|
|
1015
|
-
*/
|
|
1016
|
-
readonly compositionPlan?: CompositionPlanSpec;
|
|
1017
|
-
}
|
|
1018
|
-
/**
|
|
1019
|
-
* How a contract command method resolves a single key vs. an array of keys to the
|
|
1020
|
-
* underlying write surface (proposal "Consistency with the existing write
|
|
1021
|
-
* surface"). A single key → one write op (or one transaction); an array → a
|
|
1022
|
-
* batched write — a `TransactWriteItems` when atomicity / per-item conditions are
|
|
1023
|
-
* required, or a `BatchWriteItem` when neither is.
|
|
1024
|
-
*/
|
|
1025
|
-
type CommandResolutionTarget =
|
|
1026
|
-
/** One write op, referenced by name in `commands`. */
|
|
1027
|
-
{
|
|
1028
|
-
readonly mode: 'op';
|
|
1029
|
-
readonly operation: string;
|
|
1030
|
-
}
|
|
1031
|
-
/** One transaction, referenced by name in `transactions`. */
|
|
1032
|
-
| {
|
|
1033
|
-
readonly mode: 'transaction';
|
|
1034
|
-
readonly transaction: string;
|
|
1035
|
-
}
|
|
1036
|
-
/**
|
|
1037
|
-
* A `BatchWriteItem` over the per-key write op (no conditions). References the
|
|
1038
|
-
* single write op in `commands` that is expanded per key.
|
|
1039
|
-
*/
|
|
1040
|
-
| {
|
|
1041
|
-
readonly mode: 'batchWrite';
|
|
1042
|
-
readonly operation: string;
|
|
1043
|
-
};
|
|
1044
|
-
/**
|
|
1045
|
-
* The serialized spec of one **command** contract method. Symmetric to
|
|
1046
|
-
* {@link QueryContractMethodSpec}: it carries the declared `result` type and the
|
|
1047
|
-
* `single` / `batch` resolution targets into the existing write specs.
|
|
1048
|
-
*/
|
|
1049
|
-
interface CommandContractMethodSpec {
|
|
1050
|
-
/** Derived input arity: writes accept `'either'` (single key or key array). */
|
|
1051
|
-
readonly inputArity: ContractInputArity;
|
|
1052
|
-
/** The declared result type (`void` | a `Result` | the updated entity). */
|
|
1053
|
-
readonly result: ContractCommandResult;
|
|
1054
|
-
/** How a single key resolves (one op / one transaction). */
|
|
1055
|
-
readonly single: CommandResolutionTarget;
|
|
1056
|
-
/**
|
|
1057
|
-
* How an array of keys resolves (a transaction / `BatchWriteItem`); omitted
|
|
1058
|
-
* when the contract does not declare a batched write form.
|
|
1059
|
-
*/
|
|
1060
|
-
readonly batch?: CommandResolutionTarget;
|
|
1061
|
-
/**
|
|
1062
|
-
* The **return projection** of a `mutation`-derived command method (issue #83;
|
|
1063
|
-
* proposal §3, "return selection as a read projection"). A JSON-safe boolean
|
|
1064
|
-
* field map: after the write commits, the runtime issues a **`GetItem` with
|
|
1065
|
-
* `ConsistentRead`** of the written entity's primary key and applies this
|
|
1066
|
-
* projection via the existing read-projection machinery, returning the projected
|
|
1067
|
-
* item. This is the **uniform** return mechanism for both a single-op command
|
|
1068
|
-
* and a future transaction (a `TransactWriteItems` cannot return item images),
|
|
1069
|
-
* so TS and Python return an **identical** projected item.
|
|
1070
|
-
*
|
|
1071
|
-
* The consistent read-back is issued against the read op named
|
|
1072
|
-
* `<Contract>__<method>__readback` (a synthesized {@link QuerySpec}: a `GetItem`
|
|
1073
|
-
* on the written entity's primary key projecting these fields). Present only on
|
|
1074
|
-
* a `.plan(mutation)` method; a hand-written #64 command omits it (its
|
|
1075
|
-
* {@link result} stays `'void'` / `'entity'` and it returns no projected item).
|
|
1076
|
-
*/
|
|
1077
|
-
readonly returnSelection?: Readonly<Record<string, boolean>>;
|
|
1078
|
-
}
|
|
1079
|
-
/**
|
|
1080
|
-
* A serialized contract — a keyed public read (`'query'`) or write (`'command'`)
|
|
1081
|
-
* interface holding one or more named methods. Each method references an existing
|
|
1082
|
-
* operation spec by name and adds the decided contract facts. The existing
|
|
1083
|
-
* operation-spec shapes are unchanged, so the current runtime keeps working.
|
|
1084
|
-
*/
|
|
1085
|
-
interface ContractSpec {
|
|
1086
|
-
/** Whether this is a read (`'query'`) or write (`'command'`) contract. */
|
|
1087
|
-
readonly kind: ContractKind;
|
|
1088
|
-
/** The contract Key (the access pattern / join / batch key). */
|
|
1089
|
-
readonly key: ContractKeySpec;
|
|
1090
|
-
/**
|
|
1091
|
-
* The named methods (use cases over the same Key). A query contract's methods
|
|
1092
|
-
* are {@link QueryContractMethodSpec}; a command contract's are
|
|
1093
|
-
* {@link CommandContractMethodSpec}. The map is keyed by contract `kind`.
|
|
1094
|
-
*/
|
|
1095
|
-
readonly methods: Readonly<Record<string, QueryContractMethodSpec>> | Readonly<Record<string, CommandContractMethodSpec>>;
|
|
1096
|
-
}
|
|
1097
|
-
/**
|
|
1098
|
-
* One bounded context's membership (issue #59, "context ownership"). Lists the
|
|
1099
|
-
* Models and Contracts that belong to the same context, so the future boundary
|
|
1100
|
-
* lint (#61) can tell **own** from **foreign**: a contract may resolve its own
|
|
1101
|
-
* context's Models directly, but may only reach another context through that
|
|
1102
|
-
* context's published Contract (direct use of a foreign Model is a build error).
|
|
1103
|
-
*
|
|
1104
|
-
* This issue only **serializes / emits** this declaration; it does not enforce
|
|
1105
|
-
* it. Both lists are entity / contract *names* (matching `manifest.entities` keys
|
|
1106
|
-
* and `contracts` keys respectively).
|
|
1107
|
-
*/
|
|
1108
|
-
interface ContextSpec {
|
|
1109
|
-
/** Model (entity) names owned by this context (sorted; manifest entity keys). */
|
|
1110
|
-
readonly models: readonly string[];
|
|
1111
|
-
/** Contract names owned by this context (sorted; `contracts` map keys). */
|
|
1112
|
-
readonly contracts: readonly string[];
|
|
1113
|
-
}
|
|
1114
|
-
interface OperationsDocument {
|
|
1115
|
-
readonly version: typeof SPEC_VERSION;
|
|
1116
|
-
readonly queries: Readonly<Record<string, QuerySpec>>;
|
|
1117
|
-
readonly commands: Readonly<Record<string, CommandSpec>>;
|
|
1118
|
-
/** Declarative transactions (issue #46). Absent in pre-#46 specs. */
|
|
1119
|
-
readonly transactions?: Readonly<Record<string, TransactionSpec>>;
|
|
1120
|
-
/**
|
|
1121
|
-
* The CQRS Contract layer (issue #59): contract name → {@link ContractSpec}.
|
|
1122
|
-
* Layered **on top of** the existing `queries` / `commands` / `transactions`
|
|
1123
|
-
* (each method references one of them by name). **Absent** when the input
|
|
1124
|
-
* defines no contracts — so a contract-free input produces a byte-identical
|
|
1125
|
-
* pre-#59 operations document (backward compatibility).
|
|
1126
|
-
*/
|
|
1127
|
-
readonly contracts?: Readonly<Record<string, ContractSpec>>;
|
|
1128
|
-
/**
|
|
1129
|
-
* Context-ownership declarations (issue #59): context name →
|
|
1130
|
-
* {@link ContextSpec}. Used by the boundary lint (#61) to tell own from
|
|
1131
|
-
* foreign. **Absent** when no contexts are declared (backward compatibility).
|
|
1132
|
-
*/
|
|
1133
|
-
readonly contexts?: Readonly<Record<string, ContextSpec>>;
|
|
1134
|
-
}
|
|
1135
|
-
/** The full `{ manifest, operations }` bundle produced by the static planner. */
|
|
1136
|
-
interface BridgeBundle {
|
|
1137
|
-
readonly manifest: Manifest;
|
|
1138
|
-
readonly operations: OperationsDocument;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
469
|
/**
|
|
1142
470
|
* Declarative transaction executor (issue #46, Python-bridge Phase 4).
|
|
1143
471
|
*
|
|
@@ -1221,8 +549,6 @@ interface QueryOptions {
|
|
|
1221
549
|
declare function executeQuery(modelClass: Function, key: Record<string, unknown>, selectSpec: Record<string, unknown>, options?: QueryOptions): Promise<Record<string, unknown> | null>;
|
|
1222
550
|
|
|
1223
551
|
interface ListInput {
|
|
1224
|
-
/** A plain projection or a `project(...)` builder. */
|
|
1225
|
-
select: Record<string, unknown>;
|
|
1226
552
|
limit?: number;
|
|
1227
553
|
after?: string;
|
|
1228
554
|
order?: 'ASC' | 'DESC';
|
|
@@ -1250,7 +576,7 @@ interface ListResult {
|
|
|
1250
576
|
* only `{ items, cursor }`, so internal-only fields such as `rawItems` never
|
|
1251
577
|
* cross the public API boundary (no reliance on `as` type narrowing).
|
|
1252
578
|
*/
|
|
1253
|
-
declare function executeList(modelClass: Function, key: Record<string, unknown>,
|
|
579
|
+
declare function executeList(modelClass: Function, key: Record<string, unknown>, selectSpec: Record<string, unknown>, options?: ListInput): Promise<ListResult>;
|
|
1254
580
|
|
|
1255
581
|
interface ExplainInput {
|
|
1256
582
|
select?: Record<string, unknown>;
|
|
@@ -1497,2591 +823,6 @@ declare class CdcEmulator {
|
|
|
1497
823
|
/** Create a CDC emulator (spec §10). */
|
|
1498
824
|
declare function createCdcEmulator(opts?: CdcEmulatorOptions): CdcEmulator;
|
|
1499
825
|
|
|
1500
|
-
/**
|
|
1501
|
-
* The single source of truth for **relation execution staging** (issue #70),
|
|
1502
|
-
* shared by the static SSoT generator (`src/spec/operations.ts`,
|
|
1503
|
-
* {@link buildQuerySpec}) and the TS host runtime
|
|
1504
|
-
* (`src/relation/traversal.ts`, {@link resolveRelations}). It is the TS analogue
|
|
1505
|
-
* of the Python runtime's `_relation_stages` / `_plan_concurrency`
|
|
1506
|
-
* (`python/graphddb_runtime/runtime.py`).
|
|
1507
|
-
*
|
|
1508
|
-
* ## Why one shared module (anti-drift)
|
|
1509
|
-
*
|
|
1510
|
-
* #70a records an {@link ExecutionPlanSpec} (`groups` + `concurrency`) in the
|
|
1511
|
-
* SSoT; #70b made the Python runtime **honor** it; #70c makes the TS host runtime
|
|
1512
|
-
* honor it too. For the two runtimes to make **identical** plan-declared
|
|
1513
|
-
* decisions, TS must consume *the same plan facts* the generator serialized — not
|
|
1514
|
-
* re-derive parallelism by an independent "parallelize every sibling" walk. The
|
|
1515
|
-
* host runtime has no generated JSON to load (it works off live decorator
|
|
1516
|
-
* metadata + a `select` tree), so it derives the plan from that live tree using
|
|
1517
|
-
* **this exact function** — the same one the generator calls to produce the
|
|
1518
|
-
* serialized `executionPlan`. Generator and runtime therefore cannot drift: they
|
|
1519
|
-
* are one codepath. {@link deriveExecutionPlan} is the seam.
|
|
1520
|
-
*
|
|
1521
|
-
* ## How the runtime obtains the plan
|
|
1522
|
-
*
|
|
1523
|
-
* The generator produces `operations[]` (root + relation ops, each with a
|
|
1524
|
-
* `resultPath`) and runs {@link deriveExecutionPlan} over their result paths. The
|
|
1525
|
-
* host runtime walks the live `select` tree with {@link relationResultPaths},
|
|
1526
|
-
* which emits the **identical** ordered `resultPath` list
|
|
1527
|
-
* (`buildRelationOperations` is refactored to use it), and runs the **same**
|
|
1528
|
-
* {@link deriveExecutionPlan} over `['$', ...those]`. The resulting
|
|
1529
|
-
* {@link ExecutionPlanSpec} is threaded through the recursive relation traversal:
|
|
1530
|
-
* at each parent level the traversal honors the plan's grouping (which child ops
|
|
1531
|
-
* are an independent, concurrency-eligible stage) and the declared `concurrency`
|
|
1532
|
-
* bound — reading them from the plan, never re-deciding them.
|
|
1533
|
-
*/
|
|
1534
|
-
|
|
1535
|
-
/**
|
|
1536
|
-
* A **resolved** relation execution plan as the host runtime consumes it: the
|
|
1537
|
-
* serialized {@link ExecutionPlanSpec} (`groups` over operation indices +
|
|
1538
|
-
* `concurrency`) together with the `resultPath` each operation index produces, so
|
|
1539
|
-
* the recursive traversal can locate "the operations at this parent level" and
|
|
1540
|
-
* read their declared stage from the plan.
|
|
1541
|
-
*
|
|
1542
|
-
* Built once at a read's entry point ({@link buildRelationExecutionPlan}) and
|
|
1543
|
-
* threaded, unchanged, through every recursion level. A read whose `select` has
|
|
1544
|
-
* **no** relations (or where the plan would be a single op) carries no plan; the
|
|
1545
|
-
* traversal then has nothing to stage.
|
|
1546
|
-
*/
|
|
1547
|
-
interface RelationExecutionPlan {
|
|
1548
|
-
/** The serialized staging facts (issue #70a) the traversal honors. */
|
|
1549
|
-
readonly plan: ExecutionPlanSpec;
|
|
1550
|
-
/** Operation index → the `resultPath` it writes (index 0 = root `$`). */
|
|
1551
|
-
readonly resultPaths: readonly string[];
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
/**
|
|
1555
|
-
* Intermediate representation (IR) produced by the parameterized definition DSL
|
|
1556
|
-
* (issue #41). This is the **in-memory, typed** representation consumed by the
|
|
1557
|
-
* static planner (#42); JSON serialization is #42's responsibility, so nothing
|
|
1558
|
-
* here is serialized — the IR keeps full type information (param value types,
|
|
1559
|
-
* select projection) so downstream type derivation stays sound.
|
|
1560
|
-
*
|
|
1561
|
-
* Each `define*` entry point captures, for one definition:
|
|
1562
|
-
*
|
|
1563
|
-
* - `entity` — the model name + (runtime) class the operation targets;
|
|
1564
|
-
* - `operation`— the operation kind (`query` | `list` | `put` | `update`
|
|
1565
|
-
* | `delete`);
|
|
1566
|
-
* - the operation **structure** with {@link Param} placeholders left in place
|
|
1567
|
-
* (`key` / `item` / `changes`, plus `select` for reads); and
|
|
1568
|
-
* - `params` — a `name → descriptor` map collected from every placeholder in
|
|
1569
|
-
* the structure, with the descriptor preserving the param's value type.
|
|
1570
|
-
*/
|
|
1571
|
-
|
|
1572
|
-
/** The set of operation kinds a definition may describe. */
|
|
1573
|
-
type OperationKind = 'query' | 'list' | 'put' | 'update' | 'delete';
|
|
1574
|
-
/**
|
|
1575
|
-
* A declarative write condition for a parameterized command definition (issues
|
|
1576
|
-
* #46, #81). Mirrors the runtime `condition` subset
|
|
1577
|
-
* (`expression/condition-expression.ts`):
|
|
1578
|
-
*
|
|
1579
|
-
* - `{ notExists: true }` → `attribute_not_exists(PK)` (the row must not exist).
|
|
1580
|
-
* This legacy whole-row form is kept verbatim for backward compatibility.
|
|
1581
|
-
* - `{ attributeExists: '<field>' }` → `attribute_exists(<field>)` — the named
|
|
1582
|
-
* attribute (any field, including PK / SK) must be present. The foundation for
|
|
1583
|
-
* referential-integrity derivation (proposal "capability gaps": `requires X
|
|
1584
|
-
* exists`).
|
|
1585
|
-
* - `{ attributeNotExists: '<field>' }` → `attribute_not_exists(<field>)` — the
|
|
1586
|
-
* named attribute must be absent (a field-level uniqueness / first-write guard).
|
|
1587
|
-
* - `{ field: value, … }` → field equality (`#f = :v AND …`). Each value may be
|
|
1588
|
-
* a concrete literal **or** a {@link Param} placeholder (bound at execution).
|
|
1589
|
-
*
|
|
1590
|
-
* The forms are mutually exclusive: `notExists` / `attributeExists` /
|
|
1591
|
-
* `attributeNotExists` are single-primitive conditions and ignore any sibling
|
|
1592
|
-
* fields. (When more than one would be present the most specific is taken in the
|
|
1593
|
-
* order `notExists` → `attributeExists` → `attributeNotExists`.)
|
|
1594
|
-
*/
|
|
1595
|
-
type ConditionInput = {
|
|
1596
|
-
readonly notExists: true;
|
|
1597
|
-
} | {
|
|
1598
|
-
readonly attributeExists: string;
|
|
1599
|
-
} | {
|
|
1600
|
-
readonly attributeNotExists: string;
|
|
1601
|
-
} | Readonly<Record<string, Param<unknown> | string | number | boolean>>;
|
|
1602
|
-
/** Options accepted by the write `define*` entry points (issue #46). */
|
|
1603
|
-
interface WriteDefinitionOptions {
|
|
1604
|
-
/** Optional declarative write condition (subset). */
|
|
1605
|
-
readonly condition?: ConditionInput;
|
|
1606
|
-
}
|
|
1607
|
-
/**
|
|
1608
|
-
* Identifies the entity an operation targets. `name` is the model class name
|
|
1609
|
-
* (entity name); `modelClass` is retained so the planner can resolve metadata
|
|
1610
|
-
* (keys / GSIs / fields) from the `MetadataRegistry`.
|
|
1611
|
-
*/
|
|
1612
|
-
interface EntityRef {
|
|
1613
|
-
readonly name: string;
|
|
1614
|
-
readonly modelClass: Function;
|
|
1615
|
-
}
|
|
1616
|
-
/**
|
|
1617
|
-
* A single parameter descriptor in an operation's `params` map. Carries the
|
|
1618
|
-
* runtime `kind` (and, for `literal`, the allowed values) and — purely at the
|
|
1619
|
-
* type level — the represented value type `T`.
|
|
1620
|
-
*
|
|
1621
|
-
* @typeParam T - The value type the parameter stands in for.
|
|
1622
|
-
*/
|
|
1623
|
-
interface ParamDescriptor<T = unknown> {
|
|
1624
|
-
readonly kind: ParamKind;
|
|
1625
|
-
readonly literals?: readonly T[];
|
|
1626
|
-
/**
|
|
1627
|
-
* For an `array` param (transaction `forEach` source), the per-element field
|
|
1628
|
-
* descriptors. `undefined` for scalar params.
|
|
1629
|
-
*/
|
|
1630
|
-
readonly element?: Readonly<Record<string, ParamDescriptor>>;
|
|
1631
|
-
/** Parameters are always required (no optional params in this phase). */
|
|
1632
|
-
readonly required: true;
|
|
1633
|
-
}
|
|
1634
|
-
/**
|
|
1635
|
-
* Structure with {@link Param} placeholders left intact (key / item / changes).
|
|
1636
|
-
* A recursive record whose leaves are either concrete scalar literals (e.g. a
|
|
1637
|
-
* fixed sort-key discriminator) or `Param<…>` placeholders.
|
|
1638
|
-
*/
|
|
1639
|
-
type ParamStructure = {
|
|
1640
|
-
readonly [key: string]: ParamStructure | Param<unknown> | string | number | boolean;
|
|
1641
|
-
};
|
|
1642
|
-
/**
|
|
1643
|
-
* The typed IR for one definition. Generic over the entity, the parameterized
|
|
1644
|
-
* structure(s), the (optional) select projection, and the collected params map
|
|
1645
|
-
* so that all type information survives into the value returned by
|
|
1646
|
-
* `defineQueries` / `defineCommands`.
|
|
1647
|
-
*
|
|
1648
|
-
* Field presence by `operation`:
|
|
1649
|
-
*
|
|
1650
|
-
* | op | key | select | changes | params |
|
|
1651
|
-
* |----------|-----|--------|---------|--------|
|
|
1652
|
-
* | query | ✓ | ✓ | — | ✓ |
|
|
1653
|
-
* | list | ✓ | ✓ | — | ✓ |
|
|
1654
|
-
* | put | item (in `key`) | — | — | ✓ |
|
|
1655
|
-
* | update | ✓ | — | ✓ | ✓ |
|
|
1656
|
-
* | delete | ✓ | — | — | ✓ |
|
|
1657
|
-
*
|
|
1658
|
-
* @typeParam T - The entity type.
|
|
1659
|
-
* @typeParam Op - The operation kind.
|
|
1660
|
-
* @typeParam K - The parameterized key / item structure.
|
|
1661
|
-
* @typeParam S - The select projection (reads only; `undefined` for writes).
|
|
1662
|
-
* @typeParam Ch - The parameterized changes structure (`update` only).
|
|
1663
|
-
* @typeParam P - The collected `name → ParamDescriptor` map.
|
|
1664
|
-
*/
|
|
1665
|
-
interface OperationDefinition<T extends DDBModel, Op extends OperationKind, K, S, Ch, P extends Record<string, ParamDescriptor>> {
|
|
1666
|
-
/** @internal Marks this object as a definition IR node. */
|
|
1667
|
-
readonly __isOperationDefinition: true;
|
|
1668
|
-
readonly entity: EntityRef;
|
|
1669
|
-
readonly operation: Op;
|
|
1670
|
-
/**
|
|
1671
|
-
* The parameterized key for `query` / `list` / `update` / `delete`, or the
|
|
1672
|
-
* parameterized item for `put`. Placeholders are preserved in place.
|
|
1673
|
-
*/
|
|
1674
|
-
readonly key: K;
|
|
1675
|
-
/** The select projection for `query` / `list`; `undefined` otherwise. */
|
|
1676
|
-
readonly select: S;
|
|
1677
|
-
/** The parameterized changes for `update`; `undefined` otherwise. */
|
|
1678
|
-
readonly changes: Ch;
|
|
1679
|
-
/**
|
|
1680
|
-
* The declarative write condition for `put` / `update` / `delete`; `undefined`
|
|
1681
|
-
* when no condition is attached or for reads (issue #46).
|
|
1682
|
-
*/
|
|
1683
|
-
readonly condition?: ConditionInput;
|
|
1684
|
-
/** Collected parameters: name → descriptor (value type preserved in `P`). */
|
|
1685
|
-
readonly params: P;
|
|
1686
|
-
/** @internal Phantom carrier so the entity type `T` is retained on the node. */
|
|
1687
|
-
readonly __entityType?: (value: T) => void;
|
|
1688
|
-
}
|
|
1689
|
-
/** Any operation definition node, regardless of its type parameters. */
|
|
1690
|
-
type AnyOperationDefinition = OperationDefinition<any, OperationKind, unknown, unknown, unknown, Record<string, ParamDescriptor>>;
|
|
1691
|
-
/**
|
|
1692
|
-
* A map of definition name → {@link OperationDefinition}, as accepted by
|
|
1693
|
-
* `defineQueries` / `defineCommands` and returned (unchanged in type) from them.
|
|
1694
|
-
*/
|
|
1695
|
-
type DefinitionMap = Record<string, AnyOperationDefinition>;
|
|
1696
|
-
|
|
1697
|
-
/**
|
|
1698
|
-
* Model write-semantics — the reusable *save contract* `entityWrites` (issue #83,
|
|
1699
|
-
* Epic #80; spec `docs/mutation-command-derivation.md` §2).
|
|
1700
|
-
*
|
|
1701
|
-
* `Model.writes` declares the **write-side invariants / effects required for this
|
|
1702
|
-
* entity to be consistent on DynamoDB** — a *reusable save contract*, NOT a
|
|
1703
|
-
* mutation and NOT business logic. It is a per-lifecycle map
|
|
1704
|
-
* (`{ create?, update?, remove? }`) whose values are {@link LifecycleContract}s
|
|
1705
|
-
* built with `w.lifecycle({...})`; each carries the §2 effect arrays
|
|
1706
|
-
* (`requires` / `unique` / `edges` / `derive` / `emits` / `idempotency`).
|
|
1707
|
-
*
|
|
1708
|
-
* ## What #83 does — and deliberately does NOT — with the effects
|
|
1709
|
-
*
|
|
1710
|
-
* The mutation compiler (#83) resolves a fragment's lifecycle from this save
|
|
1711
|
-
* contract (the fragment's intent — `m.create` / `m.update` / `m.remove` — picks
|
|
1712
|
-
* `create` / `update` / `remove`) and emits the **base write op** for that
|
|
1713
|
-
* lifecycle (create → `PutItem` with `attribute_not_exists(PK)`, update →
|
|
1714
|
-
* `UpdateItem`, remove → `DeleteItem`). The §2 effect arrays are **stored
|
|
1715
|
-
* opaquely** here and consumed by NONE of #83: deriving `ConditionCheck`
|
|
1716
|
-
* (requires, #84), uniqueness guards (#86), adjacency edge `Put` / `Delete` from
|
|
1717
|
-
* `edges` (#85), derived counter `UpdateItem`s (#85), outbox events (#87), and
|
|
1718
|
-
* the idempotency guard (#87) are each their own follow-up issue. #83 only shapes
|
|
1719
|
-
* the declaration and exposes the {@link LifecycleContract.effects} a later issue
|
|
1720
|
-
* reads — the **per-fragment hook** the compiler leaves empty (see
|
|
1721
|
-
* `compileFragment` in `src/spec/mutation-command.ts`).
|
|
1722
|
-
*
|
|
1723
|
-
* ## Coexistence with edge writes (`edgeWrites`, #82)
|
|
1724
|
-
*
|
|
1725
|
-
* This is a **distinct** construct from {@link edgeWrites} (#82, the adjacency
|
|
1726
|
-
* edge-only `writes` member). `edgeWrites` declares *only* the edge write side of
|
|
1727
|
-
* an adjacency entity; `entityWrites` is the full per-lifecycle save contract a
|
|
1728
|
-
* mutation fragment adopts. Both are recognized by their own brand, so a model
|
|
1729
|
-
* may carry either form on its static `writes` member without collision; the
|
|
1730
|
-
* mutation compiler reads {@link getEntityWrites} (this marker), the edge
|
|
1731
|
-
* derivation reads its own.
|
|
1732
|
-
*
|
|
1733
|
-
* ## Trivial base op when a model declares no `writes`
|
|
1734
|
-
*
|
|
1735
|
-
* A target model that declares **no** `entityWrites` save contract has no
|
|
1736
|
-
* lifecycle to resolve; the fragment then compiles the **trivial base op** for
|
|
1737
|
-
* its intent (create → `Put` `attribute_not_exists(PK)`, update → `Update`,
|
|
1738
|
-
* remove → `Delete`) directly against the plain model. So
|
|
1739
|
-
* `m.create(() => PostModel, { input })` works on a bare model — see
|
|
1740
|
-
* {@link resolveLifecycle} in `src/spec/mutation-command.ts`.
|
|
1741
|
-
*/
|
|
1742
|
-
/**
|
|
1743
|
-
* The lifecycle phase a save contract entry declares. The mutation fragment's
|
|
1744
|
-
* intent (`m.create` / `m.update` / `m.remove`) selects the matching entry.
|
|
1745
|
-
*/
|
|
1746
|
-
type WriteLifecyclePhase = 'create' | 'update' | 'remove';
|
|
1747
|
-
/**
|
|
1748
|
-
* A path-rooted leaf value in a save-contract effect (proposal §2): every value
|
|
1749
|
-
* binds to an explicit path root so its source is unambiguous —
|
|
1750
|
-
* `$.input.*` (the mutation input) or `$.entity.*` (the written entity). #83
|
|
1751
|
-
* stores these verbatim (it consumes none); a follow-up issue (#84–#87) reads
|
|
1752
|
-
* them when deriving the effect's DynamoDB realization.
|
|
1753
|
-
*/
|
|
1754
|
-
type EffectPath = string;
|
|
1755
|
-
/**
|
|
1756
|
-
* A referential-integrity requirement (proposal §2 `requires`): the named target
|
|
1757
|
-
* entity must exist for the keys bound from `$.input.*` / `$.entity.*`. Derived
|
|
1758
|
-
* to a `ConditionCheck` (`attribute_exists`) by #84 — **not** by #83, which only
|
|
1759
|
-
* stores it.
|
|
1760
|
-
*/
|
|
1761
|
-
interface RequiresEffect {
|
|
1762
|
-
readonly kind: 'requires';
|
|
1763
|
-
/** Resolves the entity whose existence is asserted. */
|
|
1764
|
-
readonly targetFactory: () => new (...args: unknown[]) => unknown;
|
|
1765
|
-
/** The key binding: target key field → a path-rooted source value. */
|
|
1766
|
-
readonly keys: Readonly<Record<string, EffectPath>>;
|
|
1767
|
-
}
|
|
1768
|
-
/**
|
|
1769
|
-
* A uniqueness guard (proposal §2 `unique`): `{ name, scope, fields }` — a named
|
|
1770
|
-
* composite-scope uniqueness invariant. Derived to a `UNIQUE#…` guard `Put` with
|
|
1771
|
-
* `attribute_not_exists` by #86 — stored opaquely here.
|
|
1772
|
-
*/
|
|
1773
|
-
interface UniqueEffect {
|
|
1774
|
-
readonly kind: 'unique';
|
|
1775
|
-
/** The guard name (its key-shape discriminator on DynamoDB). */
|
|
1776
|
-
readonly name: string;
|
|
1777
|
-
/** The scope the uniqueness is partitioned by (path-rooted values). */
|
|
1778
|
-
readonly scope: readonly EffectPath[];
|
|
1779
|
-
/** The fields whose combined value must be unique within the scope. */
|
|
1780
|
-
readonly fields: readonly EffectPath[];
|
|
1781
|
-
}
|
|
1782
|
-
/**
|
|
1783
|
-
* An edge write effect (proposal §2 `edges`): create / delete the adjacency row
|
|
1784
|
-
* that materializes a target relation. Derived to an adjacency `Put` / `Delete`
|
|
1785
|
-
* by #82/#85 (the `src/relation/edge-write.ts` primitive) — stored opaquely here.
|
|
1786
|
-
*/
|
|
1787
|
-
interface EdgeEffect {
|
|
1788
|
-
readonly kind: 'putEdge' | 'deleteEdge';
|
|
1789
|
-
/** Resolves the target model whose relation this edge feeds. */
|
|
1790
|
-
readonly targetFactory: () => new (...args: unknown[]) => unknown;
|
|
1791
|
-
/** The relation property on the target that reads this edge. */
|
|
1792
|
-
readonly relationProperty: string;
|
|
1793
|
-
}
|
|
1794
|
-
/**
|
|
1795
|
-
* A derived (cascading) update (proposal §2 `derive`): e.g. `User.postCount +=
|
|
1796
|
-
* 1`. Derived to an `UpdateItem` `ADD` / `SET` in the transaction by #85 —
|
|
1797
|
-
* stored opaquely here.
|
|
1798
|
-
*/
|
|
1799
|
-
interface DeriveEffect {
|
|
1800
|
-
readonly kind: 'derive';
|
|
1801
|
-
/** Resolves the entity whose attribute is derived-updated. */
|
|
1802
|
-
readonly targetFactory: () => new (...args: unknown[]) => unknown;
|
|
1803
|
-
/** The key binding of the updated row (path-rooted values). */
|
|
1804
|
-
readonly keys: Readonly<Record<string, EffectPath>>;
|
|
1805
|
-
/** The attribute to increment / set. */
|
|
1806
|
-
readonly attribute: string;
|
|
1807
|
-
/** The amount to add (for an increment). */
|
|
1808
|
-
readonly amount: number;
|
|
1809
|
-
}
|
|
1810
|
-
/**
|
|
1811
|
-
* A domain event (proposal §2 `emits`): an outbox `Put` drained to Streams /
|
|
1812
|
-
* `src/cdc/`. Derived by #87 — stored opaquely here.
|
|
1813
|
-
*/
|
|
1814
|
-
interface EmitEffect {
|
|
1815
|
-
readonly kind: 'event';
|
|
1816
|
-
/** The event name (e.g. `PostCreated`). */
|
|
1817
|
-
readonly name: string;
|
|
1818
|
-
/** The event payload, each value path-rooted. */
|
|
1819
|
-
readonly payload: Readonly<Record<string, EffectPath>>;
|
|
1820
|
-
}
|
|
1821
|
-
/**
|
|
1822
|
-
* An idempotency guard (proposal §2 `idempotency`): a client-token guard item
|
|
1823
|
-
* (`attribute_not_exists`). Derived by #87 — stored opaquely here.
|
|
1824
|
-
*/
|
|
1825
|
-
interface IdempotencyEffect {
|
|
1826
|
-
readonly kind: 'idempotency';
|
|
1827
|
-
/** The path-rooted token the guard keys on (e.g. `$.input.requestId`). */
|
|
1828
|
-
readonly token: EffectPath;
|
|
1829
|
-
}
|
|
1830
|
-
/**
|
|
1831
|
-
* The full §2 effect set a {@link LifecycleContract} carries. Every field is
|
|
1832
|
-
* optional; an omitted field is the empty effect set. #83 stores this verbatim
|
|
1833
|
-
* and consumes none — the per-fragment hook a later issue (#84–#87) reads.
|
|
1834
|
-
*/
|
|
1835
|
-
interface LifecycleEffects {
|
|
1836
|
-
/** Referential-integrity requirements (→ `ConditionCheck`, #84). */
|
|
1837
|
-
readonly requires?: readonly RequiresEffect[];
|
|
1838
|
-
/** Uniqueness guards (→ `UNIQUE#…` guard `Put`, #86). */
|
|
1839
|
-
readonly unique?: readonly UniqueEffect[];
|
|
1840
|
-
/** Edge write effects (→ adjacency `Put` / `Delete`, #82/#85). */
|
|
1841
|
-
readonly edges?: readonly EdgeEffect[];
|
|
1842
|
-
/** Derived / cascading updates (→ `UpdateItem` `ADD`/`SET`, #85). */
|
|
1843
|
-
readonly derive?: readonly DeriveEffect[];
|
|
1844
|
-
/** Domain events (→ outbox `Put`, #87). */
|
|
1845
|
-
readonly emits?: readonly EmitEffect[];
|
|
1846
|
-
/** Idempotency guard (→ client-token guard `Put`, #87). */
|
|
1847
|
-
readonly idempotency?: IdempotencyEffect;
|
|
1848
|
-
}
|
|
1849
|
-
/**
|
|
1850
|
-
* Marker carried by a {@link LifecycleContract} so it is recognized regardless of
|
|
1851
|
-
* declaration shape. Built by {@link WriteRecorder.lifecycle}.
|
|
1852
|
-
*/
|
|
1853
|
-
declare const LIFECYCLE_CONTRACT_MARKER: unique symbol;
|
|
1854
|
-
/**
|
|
1855
|
-
* One lifecycle's save contract — the §2 effect set for a single phase
|
|
1856
|
-
* (`create` / `update` / `remove`). #83 emits the lifecycle's **base write op**
|
|
1857
|
-
* (selected from the fragment intent) and leaves the {@link effects} as the
|
|
1858
|
-
* untouched per-fragment hook a later issue derives.
|
|
1859
|
-
*/
|
|
1860
|
-
interface LifecycleContract {
|
|
1861
|
-
readonly [LIFECYCLE_CONTRACT_MARKER]: true;
|
|
1862
|
-
/** The §2 effect arrays, stored opaquely by #83. */
|
|
1863
|
-
readonly effects: LifecycleEffects;
|
|
1864
|
-
}
|
|
1865
|
-
/** Runtime guard: is `value` a {@link LifecycleContract} (a `w.lifecycle(...)`)? */
|
|
1866
|
-
declare function isLifecycleContract(value: unknown): value is LifecycleContract;
|
|
1867
|
-
/**
|
|
1868
|
-
* Marker carried by a model's static `writes` member when it is an
|
|
1869
|
-
* {@link EntityWritesDefinition} (an `entityWrites(...)` save contract), so it is
|
|
1870
|
-
* told apart from an {@link edgeWrites} member (#82) on the same conventional
|
|
1871
|
-
* property name.
|
|
1872
|
-
*/
|
|
1873
|
-
declare const ENTITY_WRITES_MARKER: unique symbol;
|
|
1874
|
-
/**
|
|
1875
|
-
* The per-lifecycle save contract produced by {@link entityWrites}: a map from
|
|
1876
|
-
* {@link WriteLifecyclePhase} to its {@link LifecycleContract}. Every phase is
|
|
1877
|
-
* optional — a model may declare only the lifecycles it needs (e.g. a `create` +
|
|
1878
|
-
* `remove` save contract with no `update`).
|
|
1879
|
-
*/
|
|
1880
|
-
interface EntityWritesDefinition {
|
|
1881
|
-
readonly [ENTITY_WRITES_MARKER]: true;
|
|
1882
|
-
/** The create-lifecycle save contract, if declared. */
|
|
1883
|
-
readonly create?: LifecycleContract;
|
|
1884
|
-
/** The update-lifecycle save contract, if declared. */
|
|
1885
|
-
readonly update?: LifecycleContract;
|
|
1886
|
-
/** The remove-lifecycle save contract, if declared. */
|
|
1887
|
-
readonly remove?: LifecycleContract;
|
|
1888
|
-
}
|
|
1889
|
-
/** Runtime guard: is `value` an {@link EntityWritesDefinition}? */
|
|
1890
|
-
declare function isEntityWritesDefinition(value: unknown): value is EntityWritesDefinition;
|
|
1891
|
-
/**
|
|
1892
|
-
* The recorder handed to the {@link entityWrites} builder. `w.lifecycle({...})`
|
|
1893
|
-
* accepts the §2 effect arrays and brands them into a {@link LifecycleContract}.
|
|
1894
|
-
* #83 stores the effects opaquely; #84–#87 read them.
|
|
1895
|
-
*/
|
|
1896
|
-
interface WriteRecorder {
|
|
1897
|
-
/**
|
|
1898
|
-
* Build one lifecycle's save contract from its §2 effect arrays. Every effect
|
|
1899
|
-
* field is optional; an omitted field is an empty effect set. The returned
|
|
1900
|
-
* contract is branded so the compiler can resolve it from the model's `writes`
|
|
1901
|
-
* map by lifecycle phase.
|
|
1902
|
-
*/
|
|
1903
|
-
lifecycle(effects?: LifecycleEffects): LifecycleContract;
|
|
1904
|
-
/** Declare a referential-integrity requirement (§2 `requires`; derived #84). */
|
|
1905
|
-
exists(target: () => new (...args: unknown[]) => unknown, keys: Readonly<Record<string, EffectPath>>): RequiresEffect;
|
|
1906
|
-
/** Declare a uniqueness guard (§2 `unique`; derived #86). */
|
|
1907
|
-
unique(spec: {
|
|
1908
|
-
readonly name: string;
|
|
1909
|
-
readonly scope: readonly EffectPath[];
|
|
1910
|
-
readonly fields: readonly EffectPath[];
|
|
1911
|
-
}): UniqueEffect;
|
|
1912
|
-
/** Declare an edge create effect (§2 `edges`; derived #82/#85). */
|
|
1913
|
-
putEdge(target: () => new (...args: unknown[]) => unknown, relationProperty: string): EdgeEffect;
|
|
1914
|
-
/** Declare an edge delete effect (§2 `edges`; derived #82/#85). */
|
|
1915
|
-
deleteEdge(target: () => new (...args: unknown[]) => unknown, relationProperty: string): EdgeEffect;
|
|
1916
|
-
/** Declare a derived / cascading update (§2 `derive`; derived #85). */
|
|
1917
|
-
increment(target: () => new (...args: unknown[]) => unknown, keys: Readonly<Record<string, EffectPath>>, attribute: string, amount: number): DeriveEffect;
|
|
1918
|
-
/** Declare a domain event (§2 `emits`; derived #87). */
|
|
1919
|
-
event(name: string, payload: Readonly<Record<string, EffectPath>>): EmitEffect;
|
|
1920
|
-
/** Declare an idempotency guard (§2 `idempotency`; derived #87). */
|
|
1921
|
-
idempotentBy(token: EffectPath): IdempotencyEffect;
|
|
1922
|
-
}
|
|
1923
|
-
/**
|
|
1924
|
-
* The lifecycle map a {@link entityWrites} builder returns: each phase is either
|
|
1925
|
-
* a built {@link LifecycleContract} (`w.lifecycle({...})`) or omitted.
|
|
1926
|
-
*/
|
|
1927
|
-
interface EntityWritesShape {
|
|
1928
|
-
readonly create?: LifecycleContract;
|
|
1929
|
-
readonly update?: LifecycleContract;
|
|
1930
|
-
readonly remove?: LifecycleContract;
|
|
1931
|
-
}
|
|
1932
|
-
/**
|
|
1933
|
-
* Declare a model's reusable **save contract** (issue #83, proposal §2). The
|
|
1934
|
-
* builder receives a {@link WriteRecorder} and returns the per-lifecycle map
|
|
1935
|
-
* (`{ create?, update?, remove? }`), each value a `w.lifecycle({...})` carrying
|
|
1936
|
-
* the §2 effect arrays. The result is stored on the model as
|
|
1937
|
-
* `static readonly writes = entityWrites<Model>((w) => ({ … }))`.
|
|
1938
|
-
*
|
|
1939
|
-
* #83 reads only **which** lifecycles are declared (to resolve a fragment's base
|
|
1940
|
-
* write op by intent) and stores the effect arrays opaquely; the effect
|
|
1941
|
-
* derivation is #84–#87.
|
|
1942
|
-
*
|
|
1943
|
-
* @typeParam M The model type the save contract belongs to (documentary; the
|
|
1944
|
-
* recorder is structural).
|
|
1945
|
-
* @example
|
|
1946
|
-
* ```ts
|
|
1947
|
-
* static readonly writes = entityWrites<PostModel>((w) => ({
|
|
1948
|
-
* create: w.lifecycle({
|
|
1949
|
-
* requires: [w.exists(() => UserModel, { userId: '$.input.userId' })],
|
|
1950
|
-
* edges: [w.putEdge(() => UserModel, 'posts')],
|
|
1951
|
-
* }),
|
|
1952
|
-
* remove: w.lifecycle({ edges: [w.deleteEdge(() => UserModel, 'posts')] }),
|
|
1953
|
-
* }));
|
|
1954
|
-
* ```
|
|
1955
|
-
*/
|
|
1956
|
-
declare function entityWrites<M = unknown>(builder: (w: WriteRecorder) => EntityWritesShape): EntityWritesDefinition;
|
|
1957
|
-
/**
|
|
1958
|
-
* Read the {@link EntityWritesDefinition} declared on a model class as its static
|
|
1959
|
-
* `writes` member, or `undefined` when the model declares no `entityWrites` save
|
|
1960
|
-
* contract. Scans the class's own static members for the {@link
|
|
1961
|
-
* ENTITY_WRITES_MARKER}, exactly as {@link getEdgeWrites} (#82) scans for the
|
|
1962
|
-
* edge marker — so the two `writes` forms coexist without a fixed property name
|
|
1963
|
-
* collision (a model carries one or the other; the marker disambiguates).
|
|
1964
|
-
*/
|
|
1965
|
-
declare function getEntityWrites(modelClass: new (...args: unknown[]) => unknown): EntityWritesDefinition | undefined;
|
|
1966
|
-
/** The lifecycle phase a mutation fragment intent selects. */
|
|
1967
|
-
declare function lifecyclePhaseForIntent(intent: 'create' | 'update' | 'remove'): WriteLifecyclePhase;
|
|
1968
|
-
|
|
1969
|
-
/**
|
|
1970
|
-
* Internal write-plan composition DSL — `mutation` / `definePlan` (issue #83,
|
|
1971
|
-
* Epic #80; spec `docs/mutation-command-derivation.md` §1, §3).
|
|
1972
|
-
*
|
|
1973
|
-
* A **mutation is INTERNAL** — it is *not* a public API. It is a *write-plan
|
|
1974
|
-
* composition language* used as the **implementation** of a Command: it declares
|
|
1975
|
-
* a set of write **fragments** that must execute atomically. The only externally
|
|
1976
|
-
* exposed surface is the fixed Command IF (`publicCommandModel(...).plan(...)`,
|
|
1977
|
-
* `src/define/contract.ts`); the mutation document is never exposed.
|
|
1978
|
-
*
|
|
1979
|
-
* ```ts
|
|
1980
|
-
* const CreatePost = mutation('CreatePost', (m, $) => [
|
|
1981
|
-
* m.create(() => PostModel, { input: { userId: $.userId, title: $.title } }),
|
|
1982
|
-
* ]);
|
|
1983
|
-
* ```
|
|
1984
|
-
*
|
|
1985
|
-
* ## Fragments are DECLARATIONS, not executed calls
|
|
1986
|
-
*
|
|
1987
|
-
* A mutation's top-level fields are write **fragments** — *declarations*, NOT
|
|
1988
|
-
* executed function calls. `m.create(...)` does **not** run a write; it records a
|
|
1989
|
-
* {@link MutationFragment} (intent + target + input + optional `use:`). The
|
|
1990
|
-
* compiler (`src/spec/mutation-command.ts`) expands the fragment list at compile
|
|
1991
|
-
* time into a {@link CommandSpec} / `TransactionSpec`. #83 compiles the
|
|
1992
|
-
* **single-fragment** case end-to-end; the IR is already a **list** so the
|
|
1993
|
-
* N-fragment atomic merge (#90) drops in without reshaping it.
|
|
1994
|
-
*
|
|
1995
|
-
* ## Intent selects the lifecycle; `use:` is optional
|
|
1996
|
-
*
|
|
1997
|
-
* Each fragment's **intent** (`m.create` / `m.update` / `m.remove`) selects the
|
|
1998
|
-
* lifecycle from the target's `writes` save contract (`entityWrites`, §2):
|
|
1999
|
-
* `m.create` → `writes.create`, etc. `use:` is **optional** — it defaults to the
|
|
2000
|
-
* target model's own `writes`; supply it only to adopt a *custom* write contract,
|
|
2001
|
-
* and it takes the **whole** `writes` set (an {@link EntityWritesDefinition}),
|
|
2002
|
-
* never `writes.create` (the intent already picks the lifecycle, so naming the
|
|
2003
|
-
* lifecycle would double-specify it). When the target declares **no** `writes`,
|
|
2004
|
-
* the fragment compiles the **trivial base op** for its intent (create → `Put`
|
|
2005
|
-
* `attribute_not_exists(PK)`, update → `Update`, remove → `Delete`).
|
|
2006
|
-
*
|
|
2007
|
-
* The `$` argument is the **input proxy**: `$.userId` mints a faithful
|
|
2008
|
-
* {@link MutationInputRef} naming an input field, so a fragment's `input` map
|
|
2009
|
-
* binds model fields to input fields declaratively (no arbitrary JS), exactly as
|
|
2010
|
-
* the contract `params` sentinel does.
|
|
2011
|
-
*/
|
|
2012
|
-
|
|
2013
|
-
/**
|
|
2014
|
-
* The intent of a write fragment (proposal §3): `create` / `update` / `remove`
|
|
2015
|
-
* (`remove`, **not** `del`). The intent selects the lifecycle from the target's
|
|
2016
|
-
* save contract, and the base write op (create → `PutItem`, update →
|
|
2017
|
-
* `UpdateItem`, remove → `DeleteItem`).
|
|
2018
|
-
*/
|
|
2019
|
-
type MutationIntent = 'create' | 'update' | 'remove';
|
|
2020
|
-
declare const INPUT_REF_BRAND: unique symbol;
|
|
2021
|
-
/**
|
|
2022
|
-
* A faithful reference to a mutation **input** field, minted by reading `$.<field>`
|
|
2023
|
-
* in a mutation body (e.g. `$.title`). Branded so the compiler tells a faithful
|
|
2024
|
-
* input reference from a literal and renders its `{token}` (`{title}`) into the
|
|
2025
|
-
* derived write template — the declarative binding the proposal's `$.input.*`
|
|
2026
|
-
* path root names (here the root is implied: a fragment `input` value is always
|
|
2027
|
-
* an input field reference or a concrete literal).
|
|
2028
|
-
*/
|
|
2029
|
-
interface MutationInputRef {
|
|
2030
|
-
readonly [INPUT_REF_BRAND]: true;
|
|
2031
|
-
/** The input field name, e.g. `title`. */
|
|
2032
|
-
readonly field: string;
|
|
2033
|
-
/** The template token this reference renders to, e.g. `{title}`. */
|
|
2034
|
-
readonly token: string;
|
|
2035
|
-
}
|
|
2036
|
-
/** Runtime guard: is `value` a {@link MutationInputRef} (a `$.<field>` read)? */
|
|
2037
|
-
declare function isMutationInputRef(value: unknown): value is MutationInputRef;
|
|
2038
|
-
declare const ENTITY_REF_BRAND: unique symbol;
|
|
2039
|
-
/**
|
|
2040
|
-
* A faithful **cross-fragment reference** (`$.entity[i].field`): the value a later
|
|
2041
|
-
* fragment binds from an **earlier** fragment's written field (proposal §3). The
|
|
2042
|
-
* compiler ({@link compileMutationPlan}) resolves it at **compile time** to the
|
|
2043
|
-
* earlier fragment's binding for that field — a `TransactWriteItems` cannot read
|
|
2044
|
-
* one item's value mid-transaction, so the dependency is resolved declaratively
|
|
2045
|
-
* from the shared input (the referenced field must itself trace to an input
|
|
2046
|
-
* reference or a literal), keeping the composed transaction render-time-resolvable
|
|
2047
|
-
* and atomic. A **forward / self** reference (to a same-or-later fragment) is a
|
|
2048
|
-
* circular dependency, rejected at build time.
|
|
2049
|
-
*/
|
|
2050
|
-
interface MutationEntityRef {
|
|
2051
|
-
readonly [ENTITY_REF_BRAND]: true;
|
|
2052
|
-
/** The 0-based index of the producing fragment (declaration order). */
|
|
2053
|
-
readonly fragmentIndex: number;
|
|
2054
|
-
/** The produced field name on that fragment's written entity. */
|
|
2055
|
-
readonly field: string;
|
|
2056
|
-
/** The source path, e.g. `$.entity[0].postId` (documentary; surfaces in errors). */
|
|
2057
|
-
readonly path: string;
|
|
2058
|
-
}
|
|
2059
|
-
/**
|
|
2060
|
-
* The input proxy handed to a mutation body as `$`. Any field read mints a
|
|
2061
|
-
* faithful {@link MutationInputRef}; symbol probes are inert. A field is never
|
|
2062
|
-
* coerced or transformed — a fragment `input` value is either a `$.<field>`
|
|
2063
|
-
* reference or a concrete literal, matching the declarative-only line (§2). The
|
|
2064
|
-
* type is `Record<string, MutationInputRef>` so a body reads `$.anyField`.
|
|
2065
|
-
*/
|
|
2066
|
-
type MutationInputProxy = Record<string, MutationInputRef>;
|
|
2067
|
-
declare const FRAGMENT_BRAND: unique symbol;
|
|
2068
|
-
/**
|
|
2069
|
-
* The per-field input binding of a fragment: model field name → a faithful
|
|
2070
|
-
* {@link MutationInputRef} (a `$.<field>` reference) or a concrete scalar literal
|
|
2071
|
-
* (e.g. a fixed discriminator). One declarative level — the values bind the model
|
|
2072
|
-
* field they key, never a transform.
|
|
2073
|
-
*/
|
|
2074
|
-
type FragmentInput = Readonly<Record<string, MutationInputRef | string | number | boolean | Date | null>>;
|
|
2075
|
-
/**
|
|
2076
|
-
* One recorded write fragment (proposal §3): an **intent** + a target entity + an
|
|
2077
|
-
* optional adopted save contract (`use:`) + the input binding. It is a
|
|
2078
|
-
* *declaration* the compiler expands — never an executed write.
|
|
2079
|
-
*
|
|
2080
|
-
* - `intent` — `create` / `update` / `remove`; selects the lifecycle + base op.
|
|
2081
|
-
* - `entity` — the target entity (resolved at definition time).
|
|
2082
|
-
* - `input` — the model-field → input-field / literal binding (declarative).
|
|
2083
|
-
* - `use` — the adopted {@link EntityWritesDefinition} (whole set), or `undefined`
|
|
2084
|
-
* to default to the target model's own `writes` (and, absent that, the trivial
|
|
2085
|
-
* base op). Per the `use:` rule it is **never** a single lifecycle.
|
|
2086
|
-
*/
|
|
2087
|
-
interface MutationFragment {
|
|
2088
|
-
readonly [FRAGMENT_BRAND]: true;
|
|
2089
|
-
readonly intent: MutationIntent;
|
|
2090
|
-
readonly entity: EntityRef;
|
|
2091
|
-
readonly input: FragmentInput;
|
|
2092
|
-
readonly use?: EntityWritesDefinition;
|
|
2093
|
-
}
|
|
2094
|
-
/** Runtime guard: is `value` a {@link MutationFragment}? */
|
|
2095
|
-
declare function isMutationFragment(value: unknown): value is MutationFragment;
|
|
2096
|
-
/**
|
|
2097
|
-
* Options for a write fragment: the declarative `input` binding and the optional
|
|
2098
|
-
* `use:` save-contract override (the **whole** `writes` set, never a lifecycle).
|
|
2099
|
-
*/
|
|
2100
|
-
interface FragmentOptions {
|
|
2101
|
-
/** The model-field → input-field / literal binding. */
|
|
2102
|
-
readonly input: FragmentInput;
|
|
2103
|
-
/**
|
|
2104
|
-
* An optional custom save contract to adopt (the **whole** {@link
|
|
2105
|
-
* EntityWritesDefinition}). Omit to default to the target model's own `writes`.
|
|
2106
|
-
* Per the proposal's `use:` rule this is never a single lifecycle (`writes.create`)
|
|
2107
|
-
* — the fragment's intent already selects the lifecycle.
|
|
2108
|
-
*/
|
|
2109
|
-
readonly use?: EntityWritesDefinition;
|
|
2110
|
-
}
|
|
2111
|
-
/**
|
|
2112
|
-
* The intent recorder handed to a mutation body as `m`. `m.create` / `m.update` /
|
|
2113
|
-
* `m.remove` each record a {@link MutationFragment} of the corresponding intent;
|
|
2114
|
-
* `remove` (not `del`) is the deletion intent per the proposal.
|
|
2115
|
-
*/
|
|
2116
|
-
interface MutationRecorder {
|
|
2117
|
-
/** Record a `create` fragment (→ lifecycle `create`, base op `PutItem`). */
|
|
2118
|
-
create(target: () => ModelStatic<DDBModel> | (new (...args: unknown[]) => unknown), options: FragmentOptions): MutationFragment;
|
|
2119
|
-
/** Record an `update` fragment (→ lifecycle `update`, base op `UpdateItem`). */
|
|
2120
|
-
update(target: () => ModelStatic<DDBModel> | (new (...args: unknown[]) => unknown), options: FragmentOptions): MutationFragment;
|
|
2121
|
-
/** Record a `remove` fragment (→ lifecycle `remove`, base op `DeleteItem`). */
|
|
2122
|
-
remove(target: () => ModelStatic<DDBModel> | (new (...args: unknown[]) => unknown), options: FragmentOptions): MutationFragment;
|
|
2123
|
-
}
|
|
2124
|
-
declare const COMMAND_PLAN_BRAND: unique symbol;
|
|
2125
|
-
/**
|
|
2126
|
-
* The recorded **CommandPlan** — a mutation's IR (proposal §3). It carries the
|
|
2127
|
-
* mutation `name` and the **list** of {@link MutationFragment}s the body declared
|
|
2128
|
-
* (1..N; #83 compiles the 1-fragment case end-to-end, the list shape is ready for
|
|
2129
|
-
* the #90 N-fragment merge). The body closure is discarded — only the declarative
|
|
2130
|
-
* fragment list is kept, exactly as the contract IR discards its method closures.
|
|
2131
|
-
*/
|
|
2132
|
-
interface CommandPlan {
|
|
2133
|
-
readonly [COMMAND_PLAN_BRAND]: true;
|
|
2134
|
-
/** The mutation name (documentary; surfaces in compiler error messages). */
|
|
2135
|
-
readonly name: string;
|
|
2136
|
-
/** The declared write fragments, in declaration order (the mutation IR). */
|
|
2137
|
-
readonly fragments: readonly MutationFragment[];
|
|
2138
|
-
}
|
|
2139
|
-
/** Runtime guard: is `value` a {@link CommandPlan} (a `mutation(...)` result)? */
|
|
2140
|
-
declare function isCommandPlan(value: unknown): value is CommandPlan;
|
|
2141
|
-
/**
|
|
2142
|
-
* Declare an internal **mutation** — a write-plan composition (proposal §1, §3).
|
|
2143
|
-
* The body receives the intent recorder `m` (`m.create` / `m.update` /
|
|
2144
|
-
* `m.remove`) and the input proxy `$` (`$.field` → a {@link MutationInputRef}),
|
|
2145
|
-
* and returns the **list** of write fragments to compose atomically. The result
|
|
2146
|
-
* is a branded {@link CommandPlan} (the mutation IR) — *not* public; it implements
|
|
2147
|
-
* a Command IF via `publicCommandModel(...).plan(mutation)`.
|
|
2148
|
-
*
|
|
2149
|
-
* @param name The mutation name (documentary; used in compiler errors).
|
|
2150
|
-
* @param body `(m, $) => [fragment, …]` — declares the fragment list.
|
|
2151
|
-
* @throws if the body returns a non-array, an empty list, or a non-fragment value.
|
|
2152
|
-
*/
|
|
2153
|
-
declare function mutation(name: string, body: (m: MutationRecorder, $: MutationInputProxy) => readonly MutationFragment[]): CommandPlan;
|
|
2154
|
-
/**
|
|
2155
|
-
* Alias of {@link mutation} (proposal naming: the internal DSL is `mutation` /
|
|
2156
|
-
* `definePlan`). Identical behavior; provided so a producer may name the plan
|
|
2157
|
-
* declaration `definePlan('…', …)` when that reads more clearly at the call site.
|
|
2158
|
-
*/
|
|
2159
|
-
declare const definePlan: typeof mutation;
|
|
2160
|
-
|
|
2161
|
-
/**
|
|
2162
|
-
* Single-fragment mutation → command-op compiler (issue #83, Epic #80; proposal
|
|
2163
|
-
* `docs/mutation-command-derivation.md` §3).
|
|
2164
|
-
*
|
|
2165
|
-
* A {@link CommandPlan} (a `mutation(...)` result) is the internal IR: a **list**
|
|
2166
|
-
* of write fragments to compose atomically. This module compiles the
|
|
2167
|
-
* **single-fragment** case end-to-end into the **same** {@link ContractMethodOp}
|
|
2168
|
-
* the #58 contract recorder produces, so the existing serializer
|
|
2169
|
-
* (`opToDefinition` → `buildCommandSpec`, `src/spec/contracts.ts` /
|
|
2170
|
-
* `src/spec/operations.ts`) and the existing runtimes consume it with no new
|
|
2171
|
-
* write substrate. The IR is already a list, so the **N-fragment atomic merge is
|
|
2172
|
-
* #90** — this module rejects N>1 with a clear pointer rather than half-merging.
|
|
2173
|
-
*
|
|
2174
|
-
* ## Intent → base op (proposal §3, "single fragment compile")
|
|
2175
|
-
*
|
|
2176
|
-
* | intent | base op |
|
|
2177
|
-
* |----------|-------------------------------------------|
|
|
2178
|
-
* | `create` | `PutItem` (`attribute_not_exists(PK)`) |
|
|
2179
|
-
* | `update` | `UpdateItem` (key + changed fields) |
|
|
2180
|
-
* | `remove` | `DeleteItem` (key) |
|
|
2181
|
-
*
|
|
2182
|
-
* ## `use:` resolution (the resolved fork — proposal §3, pinned spec correction)
|
|
2183
|
-
*
|
|
2184
|
-
* The fragment's **intent** selects the lifecycle; `use:` is **optional**:
|
|
2185
|
-
*
|
|
2186
|
-
* - `use:` present → adopt that {@link EntityWritesDefinition} (the **whole**
|
|
2187
|
-
* `writes` set; never a single lifecycle).
|
|
2188
|
-
* - `use:` omitted → default to the **target model's own** `entityWrites` save
|
|
2189
|
-
* contract ({@link getEntityWrites}).
|
|
2190
|
-
* - the resolved save contract (custom or default) has **no** entry for the
|
|
2191
|
-
* intent's lifecycle, **or the target declares no `writes` at all** → the
|
|
2192
|
-
* **trivial base op** for the intent (create → `Put` `attribute_not_exists(PK)`,
|
|
2193
|
-
* update → `Update`, remove → `Delete`) against the plain model. So
|
|
2194
|
-
* `m.create(() => PostModel, { input })` works on a bare model.
|
|
2195
|
-
*
|
|
2196
|
-
* Either way #83 emits the **same** base op — the resolved
|
|
2197
|
-
* {@link LifecycleContract} only matters as the **per-fragment hook** the
|
|
2198
|
-
* effect-derivation issues (#84–#87) read: the resolved lifecycle's
|
|
2199
|
-
* {@link LifecycleContract.effects} are exposed on {@link CompiledFragment} and
|
|
2200
|
-
* consumed by NONE of #83 (the empty hook).
|
|
2201
|
-
*/
|
|
2202
|
-
|
|
2203
|
-
/**
|
|
2204
|
-
* A **referential-integrity assertion** derived from a `requires`
|
|
2205
|
-
* ({@link RequiresEffect}) effect (issue #84; proposal §2/§3). It is the
|
|
2206
|
-
* realization of `w.exists(() => Entity, { <entityKeyField>: '$.input.<field>' })`:
|
|
2207
|
-
* the referenced entity must already exist for the bound keys, asserted by a
|
|
2208
|
-
* read-only `ConditionCheck` (`attribute_exists(PK)`) item composed into the
|
|
2209
|
-
* mutation's atomic `TransactWriteItems`. A failed assertion cancels the WHOLE
|
|
2210
|
-
* transaction, so the entity / edges / derived updates / events that the same
|
|
2211
|
-
* transaction would write never persist (referential integrity is enforced
|
|
2212
|
-
* atomically, not by a pre-read).
|
|
2213
|
-
*
|
|
2214
|
-
* The shape is **runtime-neutral** — it carries the structural binding (target
|
|
2215
|
-
* entity + the target key fields resolved from the mutation input) that BOTH the
|
|
2216
|
-
* spec serializer (→ a {@link import('./types.js').TransactionItemSpec} with
|
|
2217
|
-
* templated `keyCondition`, for the Python bridge) and the in-process TS runtime
|
|
2218
|
-
* (→ a `ConditionCheck` `TransactWriteItems` entry, keyed from `(key, params)`)
|
|
2219
|
-
* derive their concrete form from. The compiler resolves the key binding once,
|
|
2220
|
-
* here, so both runtimes assert the **same** keyed row from the same input.
|
|
2221
|
-
*/
|
|
2222
|
-
interface DerivedConditionCheck {
|
|
2223
|
-
/** The referenced entity whose existence is asserted (resolved target model). */
|
|
2224
|
-
readonly entity: EntityRef;
|
|
2225
|
-
/**
|
|
2226
|
-
* The asserted row's key binding: target primary-key **input field name** → the
|
|
2227
|
-
* **mutation input field** that supplies it (parsed from the effect's
|
|
2228
|
-
* `$.input.<field>` value). Every target primary-key field is bound (a partial
|
|
2229
|
-
* key cannot identify a row to assert on), so the runtimes build the referenced
|
|
2230
|
-
* entity's full primary key from the mutation params.
|
|
2231
|
-
*/
|
|
2232
|
-
readonly keyBinding: Readonly<Record<string, string>>;
|
|
2233
|
-
}
|
|
2234
|
-
/**
|
|
2235
|
-
* A **derived edge write** — the adjacency `Put` / `Delete` {@link TransactionItemSpec}
|
|
2236
|
-
* set that materializes a relation edge on DynamoDB (issue #85; proposal §2 `edges`),
|
|
2237
|
-
* derived from one `w.putEdge(() => Target, 'relation')` / `w.deleteEdge(...)` effect.
|
|
2238
|
-
*
|
|
2239
|
-
* The effect names a **relation** (`Target.relationProperty`) whose rows form the
|
|
2240
|
-
* edge; the **adjacency entity** — the entity whose row IS that edge — is the
|
|
2241
|
-
* relation's *target model* ({@link RelationMetadata.targetFactory}), resolved here
|
|
2242
|
-
* and round-trip-verified by the #82 primitive ({@link deriveEdgeWriteItemsFor}, with
|
|
2243
|
-
* the #85-tightened PK-partition guard). The resolved item set is lifecycle-distinguished
|
|
2244
|
-
* by the fragment's intent ({@link INTENT_EDGE_LIFECYCLE}):
|
|
2245
|
-
*
|
|
2246
|
-
* - `create` → `onCreate`: a single adjacency `Put` (the new edge row);
|
|
2247
|
-
* - `remove` → `onDelete`: a single adjacency `Delete` (the edge row keyed by PK/SK);
|
|
2248
|
-
* - `update` → `onUpdateKeyChange`: `Delete`(old edge, keyed in the `{old.*}`
|
|
2249
|
-
* namespace) + `Put`(new edge) — the edge **moves** atomically (spec correction).
|
|
2250
|
-
*
|
|
2251
|
-
* The items carry templated key/item fields (`{field}` / `{old.field}`); the
|
|
2252
|
-
* serializer composes them into the atomic `TransactWriteItems` and the in-process
|
|
2253
|
-
* runtime renders the same items from the shared `(key, params)`, so both runtimes
|
|
2254
|
-
* write the **same** adjacency row(s) from the same input.
|
|
2255
|
-
*/
|
|
2256
|
-
interface DerivedEdgeWrite {
|
|
2257
|
-
/** The adjacency entity (the relation's target — whose row is the edge). */
|
|
2258
|
-
readonly entity: EntityRef;
|
|
2259
|
-
/** The relation (`Target.relationProperty`) the edge materializes (documentary). */
|
|
2260
|
-
readonly relation: {
|
|
2261
|
-
readonly target: string;
|
|
2262
|
-
readonly property: string;
|
|
2263
|
-
};
|
|
2264
|
-
/** The lifecycle-distinguished adjacency item set (`Put` / `Delete`). */
|
|
2265
|
-
readonly items: readonly TransactionItemSpec[];
|
|
2266
|
-
}
|
|
2267
|
-
/**
|
|
2268
|
-
* A **derived update** — an atomic `ADD` (`UpdateItem`) on a target row, derived
|
|
2269
|
-
* from one `w.increment(() => Entity, { key }, 'field', delta)` effect (issue #85;
|
|
2270
|
-
* proposal §2 `derive`). It realizes a cascading counter such as
|
|
2271
|
-
* `User.postCount += 1`: an atomic `ADD #field :delta` that needs no prior read and
|
|
2272
|
-
* is safe under concurrency (a `SET` would clobber a concurrent increment).
|
|
2273
|
-
*
|
|
2274
|
-
* The lifecycle decides the sign convention the *declaration* uses (the author
|
|
2275
|
-
* declares `+1` on `create`, `-1` on `remove`, and a `-1`-on-old + `+1`-on-new pair
|
|
2276
|
-
* on `update` / onUpdateKeyChange); the compiler stores the declared delta verbatim.
|
|
2277
|
-
* The target row's key binds from the mutation input — `$.input.<field>` (the new /
|
|
2278
|
-
* current value) or `$.old.<field>` (the pre-mutation value, for decrementing the
|
|
2279
|
-
* **old** parent on a key change; the {@link OLD_VALUE_NAMESPACE} convention).
|
|
2280
|
-
*/
|
|
2281
|
-
interface DerivedUpdate {
|
|
2282
|
-
/** The entity whose attribute is derive-updated (resolved target model). */
|
|
2283
|
-
readonly entity: EntityRef;
|
|
2284
|
-
/**
|
|
2285
|
-
* The target row's key binding: target primary-key **input field name** → the
|
|
2286
|
-
* source param that supplies it. A `$.input.<field>` source binds the param
|
|
2287
|
-
* `<field>`; a `$.old.<field>` source binds the param `old.<field>` (the
|
|
2288
|
-
* pre-mutation value). Every target primary-key field is bound.
|
|
2289
|
-
*/
|
|
2290
|
-
readonly keyBinding: Readonly<Record<string, string>>;
|
|
2291
|
-
/** The attribute to atomically `ADD` to. */
|
|
2292
|
-
readonly attribute: string;
|
|
2293
|
-
/** The signed delta to add (e.g. `+1` create, `-1` remove). */
|
|
2294
|
-
readonly amount: number;
|
|
2295
|
-
}
|
|
2296
|
-
/**
|
|
2297
|
-
* A **derived uniqueness guard** — the marker-row `Put` (and, on a unique-field
|
|
2298
|
-
* change, the old-guard `Delete` + new-guard `Put` swap) that enforces an
|
|
2299
|
-
* application-level uniqueness invariant declared by `w.unique({ name, scope,
|
|
2300
|
-
* fields })` (issue #86; proposal §2 `unique`). It is the realization of a named,
|
|
2301
|
-
* composite-scope uniqueness constraint that a unique GSI cannot express
|
|
2302
|
-
* (e.g. *unique title per user*).
|
|
2303
|
-
*
|
|
2304
|
-
* ## The guard-row key (deterministic, built from `name` / `scope` / `fields`)
|
|
2305
|
-
*
|
|
2306
|
-
* The guard occupies a synthetic marker row whose primary key is built **purely**
|
|
2307
|
-
* from the constraint:
|
|
2308
|
-
*
|
|
2309
|
-
* - `PK` = `UNIQUE#<name>` followed by one `#{<scopeField>}` segment per `scope`
|
|
2310
|
-
* path (a composite scope yields several segments) — the **partition** the
|
|
2311
|
-
* uniqueness applies within, e.g. `UNIQUE#postTitlePerUser#{userId}`;
|
|
2312
|
-
* - `SK` = `VALUE#` followed by one `#{<field>}` segment per `fields` path (the
|
|
2313
|
-
* first carries no leading separator) — the **unique value** within that scope,
|
|
2314
|
-
* e.g. `VALUE#{title}`.
|
|
2315
|
-
*
|
|
2316
|
-
* Each `$.input.<field>` path contributes a `{<field>}` template token (rendered
|
|
2317
|
-
* from the mutation input). The shape is **deterministic** (same constraint →
|
|
2318
|
-
* same key), so a second write of the same scoped value targets the SAME row, and
|
|
2319
|
-
* the `attribute_not_exists` guard on the `Put` then fails — rolling back the
|
|
2320
|
-
* WHOLE transaction so the entity is never created.
|
|
2321
|
-
*
|
|
2322
|
-
* ## create vs. update-swap
|
|
2323
|
-
*
|
|
2324
|
-
* - **create / remove** — a single guard item: `create` → `Put` (claim the value)
|
|
2325
|
-
* guarded by `attribute_not_exists(PK)`; `remove` → `Delete` (release the value);
|
|
2326
|
-
* - **update changing a unique field** — `Delete`(OLD guard) + `Put`(NEW guard):
|
|
2327
|
-
* the old marker row (keyed in the `{old.*}` namespace, the {@link
|
|
2328
|
-
* OLD_VALUE_NAMESPACE} convention shared with the edge `onUpdateKeyChange` and
|
|
2329
|
-
* the derived-counter old-parent decrement) is released and the new value claimed
|
|
2330
|
-
* atomically, so the uniqueness moves with the field. The new `Put` keeps the
|
|
2331
|
-
* `attribute_not_exists` guard so the moved-to value cannot collide.
|
|
2332
|
-
*
|
|
2333
|
-
* The items are **runtime-neutral** {@link TransactionItemSpec}s carrying the
|
|
2334
|
-
* literal-key marker ({@link TransactionItemSpec.literalKey}) and the
|
|
2335
|
-
* {@link MARKER_ROW_ENTITY} sentinel (there is no manifest entity), so BOTH runtimes
|
|
2336
|
-
* write the SAME marker row(s) from the same input and the guard composes into the
|
|
2337
|
-
* entity's atomic `TransactWriteItems` (#90).
|
|
2338
|
-
*/
|
|
2339
|
-
interface DerivedUniqueGuard {
|
|
2340
|
-
/** The constraint name (its key-shape discriminator on DynamoDB). */
|
|
2341
|
-
readonly name: string;
|
|
2342
|
-
/** The guard's marker-row item set (`Put` / `Delete`), lifecycle-distinguished. */
|
|
2343
|
-
readonly items: readonly TransactionItemSpec[];
|
|
2344
|
-
}
|
|
2345
|
-
/**
|
|
2346
|
-
* A **derived outbox event** — the transactional-outbox `Put` ({@link
|
|
2347
|
-
* TransactionItemSpec}) that records a domain event ATOMICALLY with the entity
|
|
2348
|
-
* write, derived from one `w.event(name, payload)` effect (issue #87; proposal §2
|
|
2349
|
-
* `emits`, §4 "delivery is outside"). The event row is written in the **same**
|
|
2350
|
-
* `TransactWriteItems` as the entity, so the event and the state it announces can
|
|
2351
|
-
* never diverge (no "wrote the row, lost the event" gap).
|
|
2352
|
-
*
|
|
2353
|
-
* ## The outbox-row key + shape (deterministic; drainable via `src/cdc/`)
|
|
2354
|
-
*
|
|
2355
|
-
* The row occupies a synthetic marker row in its own `OUTBOX#…` key-space:
|
|
2356
|
-
*
|
|
2357
|
-
* - `PK` = `OUTBOX#<eventName>` followed by one `#{<keyField>}` segment per
|
|
2358
|
-
* primary-key input field of the **writing entity** — so each entity emission has
|
|
2359
|
-
* its own deterministic outbox row, e.g. `OUTBOX#PostCreated#{postId}`;
|
|
2360
|
-
* - `SK` = `EVENT#` followed by the same per-key-field segments — a stable,
|
|
2361
|
-
* single-row-per-emission sort key;
|
|
2362
|
-
* - the item also stores `eventName` plus every declared `payload` field (each a
|
|
2363
|
-
* `{field}` template rendered from the shared method params), so the **`newImage`**
|
|
2364
|
-
* of the row carries the complete, typed event body.
|
|
2365
|
-
*
|
|
2366
|
-
* Because the row is a normal table item, the `src/cdc/` Streams substrate captures
|
|
2367
|
-
* its write as a {@link import('../cdc/types.js').ChangeEvent} (`eventName:
|
|
2368
|
-
* 'INSERT'`, `newImage` = this item). A consumer drains the outbox by filtering the
|
|
2369
|
-
* stream on `PK begins_with 'OUTBOX#'` and reading the event body off `newImage` —
|
|
2370
|
-
* **delivery / handlers / business reactions live OUTSIDE the mutation** (proposal
|
|
2371
|
-
* §4); #87 only writes the drainable RECORD.
|
|
2372
|
-
*/
|
|
2373
|
-
interface DerivedOutboxEvent {
|
|
2374
|
-
/** The event name (e.g. `PostCreated`). */
|
|
2375
|
-
readonly name: string;
|
|
2376
|
-
/** The outbox marker-row `Put` (one item; `literalKey`, `MARKER_ROW_ENTITY`). */
|
|
2377
|
-
readonly items: readonly TransactionItemSpec[];
|
|
2378
|
-
}
|
|
2379
|
-
/**
|
|
2380
|
-
* A **derived idempotency guard** — the client-token guard `Put` ({@link
|
|
2381
|
-
* TransactionItemSpec}) that makes a same-token re-execution a NO-OP, derived from
|
|
2382
|
-
* the lifecycle's `w.idempotentBy(token)` effect (issue #87; proposal §2
|
|
2383
|
-
* `idempotency`). It is the transactional realization of "process this command at
|
|
2384
|
-
* most once for this client token".
|
|
2385
|
-
*
|
|
2386
|
-
* ## The guard-row key (deterministic, built from the token)
|
|
2387
|
-
*
|
|
2388
|
-
* The guard occupies a synthetic marker row keyed **purely** by the client token:
|
|
2389
|
-
*
|
|
2390
|
-
* - `PK` = `IDEMP#{<tokenField>}` — the `$.input.<field>` token (e.g.
|
|
2391
|
-
* `IDEMP#{requestId}`), rendered from the shared method params;
|
|
2392
|
-
* - `SK` = `TOKEN` (a fixed discriminator — one guard row per token).
|
|
2393
|
-
*
|
|
2394
|
-
* The `Put` is guarded by `attribute_not_exists(PK)`: the FIRST execution claims the
|
|
2395
|
-
* token row; a SECOND execution with the same token targets the SAME row and the
|
|
2396
|
-
* guard fails, cancelling the WHOLE `TransactWriteItems` — so the entity write, the
|
|
2397
|
-
* outbox event, and every other effect roll back and **no effect is double-applied**.
|
|
2398
|
-
* The shape is identical to the uniqueness guard's claim `Put` (the same `literalKey`
|
|
2399
|
-
* marker mechanism), so BOTH runtimes write / assert the SAME row from the same input.
|
|
2400
|
-
*/
|
|
2401
|
-
interface DerivedIdempotencyGuard {
|
|
2402
|
-
/** The mutation-input field the token binds from (documentary; surfaces in errors). */
|
|
2403
|
-
readonly tokenField: string;
|
|
2404
|
-
/** The guard marker-row `Put` (one item; `attribute_not_exists`, `literalKey`). */
|
|
2405
|
-
readonly items: readonly TransactionItemSpec[];
|
|
2406
|
-
}
|
|
2407
|
-
/**
|
|
2408
|
-
* The result of compiling one fragment: the {@link ContractMethodOp} the existing
|
|
2409
|
-
* serializer / runtime consume, the resolved contract Key fields (the model's
|
|
2410
|
-
* primary-key input fields, also the consistent read-back key), and the **opaque
|
|
2411
|
-
* per-fragment hook** — the resolved lifecycle's §2 effect set (empty-consumed by
|
|
2412
|
-
* #83; read by #84–#87).
|
|
2413
|
-
*/
|
|
2414
|
-
interface CompiledFragment {
|
|
2415
|
-
/** The resolved write op (`put` / `update` / `delete`), serializer-ready. */
|
|
2416
|
-
readonly op: ContractMethodOp;
|
|
2417
|
-
/** The contract Key fields (the model primary-key input fields). */
|
|
2418
|
-
readonly keyFields: readonly string[];
|
|
2419
|
-
/**
|
|
2420
|
-
* The resolved lifecycle's §2 effect set, or `undefined` when the fragment
|
|
2421
|
-
* resolved to the trivial base op (no `writes` entry). **The per-fragment hook
|
|
2422
|
-
* for #84–#87** — #83 derives NO items from it. #84 reads the `requires` arm to
|
|
2423
|
-
* derive {@link conditionChecks}; the remaining arms (unique / edges / derive /
|
|
2424
|
-
* emits / idempotency) are still stored opaquely for #85–#87.
|
|
2425
|
-
*/
|
|
2426
|
-
readonly effects?: LifecycleEffects;
|
|
2427
|
-
/**
|
|
2428
|
-
* The referential-integrity assertions derived from the resolved lifecycle's
|
|
2429
|
-
* `requires` effects (issue #84): one {@link DerivedConditionCheck} per
|
|
2430
|
-
* `w.exists(...)`. Present (non-empty) only when the fragment's lifecycle
|
|
2431
|
-
* declares `requires`; absent otherwise (no regression — a fragment with no
|
|
2432
|
-
* `requires` compiles to exactly the #83/#90 base op). Each becomes a read-only
|
|
2433
|
-
* `ConditionCheck` (`attribute_exists`) item in the mutation's atomic
|
|
2434
|
-
* `TransactWriteItems`, and their presence **promotes** an otherwise single-op
|
|
2435
|
-
* mutation to a transaction (a `ConditionCheck` only exists inside a
|
|
2436
|
-
* `TransactWriteItems`).
|
|
2437
|
-
*/
|
|
2438
|
-
readonly conditionChecks?: readonly DerivedConditionCheck[];
|
|
2439
|
-
/**
|
|
2440
|
-
* The adjacency edge writes derived from the resolved lifecycle's `edges` effects
|
|
2441
|
-
* (issue #85): one {@link DerivedEdgeWrite} per `w.putEdge(...)` / `w.deleteEdge(...)`,
|
|
2442
|
-
* lifecycle-distinguished by the fragment intent (`create` → `Put`, `remove` →
|
|
2443
|
-
* `Delete`, `update` → `Delete(old) + Put(new)`). Present (non-empty) only when the
|
|
2444
|
-
* lifecycle declares `edges`; absent otherwise (no regression). Each derived item is
|
|
2445
|
-
* composed into the method's atomic `TransactWriteItems`.
|
|
2446
|
-
*/
|
|
2447
|
-
readonly edgeWrites?: readonly DerivedEdgeWrite[];
|
|
2448
|
-
/**
|
|
2449
|
-
* The derived (cascading) updates from the resolved lifecycle's `derive` effects
|
|
2450
|
-
* (issue #85): one {@link DerivedUpdate} per `w.increment(...)`, each an atomic
|
|
2451
|
-
* `ADD` on a target counter. Present (non-empty) only when the lifecycle declares
|
|
2452
|
-
* `derive`; absent otherwise (no regression). Each becomes an `UpdateItem` (`ADD`)
|
|
2453
|
-
* in the method's atomic `TransactWriteItems`.
|
|
2454
|
-
*/
|
|
2455
|
-
readonly derivedUpdates?: readonly DerivedUpdate[];
|
|
2456
|
-
/**
|
|
2457
|
-
* The uniqueness guards derived from the resolved lifecycle's `unique` effects
|
|
2458
|
-
* (issue #86): one {@link DerivedUniqueGuard} per `w.unique({ name, scope,
|
|
2459
|
-
* fields })`, lifecycle-distinguished by the fragment intent (`create` → a guarded
|
|
2460
|
-
* marker `Put`, `remove` → a marker `Delete`, `update` → `Delete(old) + Put(new)`
|
|
2461
|
-
* — the value swap). Present (non-empty) only when the lifecycle declares
|
|
2462
|
-
* `unique`; absent otherwise (no regression — a fragment with no `unique` compiles
|
|
2463
|
-
* to exactly the #83/#90 base op). Each guard's items compose into the method's
|
|
2464
|
-
* atomic `TransactWriteItems`; the guard `Put`'s `attribute_not_exists` makes a
|
|
2465
|
-
* duplicate roll the WHOLE transaction back, so the entity is never created.
|
|
2466
|
-
*/
|
|
2467
|
-
readonly uniqueGuards?: readonly DerivedUniqueGuard[];
|
|
2468
|
-
/**
|
|
2469
|
-
* The outbox events derived from the resolved lifecycle's `emits` effects (issue
|
|
2470
|
-
* #87): one {@link DerivedOutboxEvent} per `w.event(name, payload)`, each a
|
|
2471
|
-
* transactional-outbox `Put` recording the event ATOMICALLY with the entity write
|
|
2472
|
-
* (so event and state cannot diverge). Present (non-empty) only when the lifecycle
|
|
2473
|
-
* declares `emits`; absent otherwise (no regression — a fragment with no `emits`
|
|
2474
|
-
* compiles to exactly the #83/#90 base op). Each event's `Put` composes into the
|
|
2475
|
-
* method's atomic `TransactWriteItems`; its row is drainable via `src/cdc/`
|
|
2476
|
-
* (`PK begins_with 'OUTBOX#'`). Delivery / handlers are OUT of scope (proposal §4).
|
|
2477
|
-
*/
|
|
2478
|
-
readonly outboxEvents?: readonly DerivedOutboxEvent[];
|
|
2479
|
-
/**
|
|
2480
|
-
* The idempotency guard derived from the resolved lifecycle's `idempotency` effect
|
|
2481
|
-
* (issue #87): a single {@link DerivedIdempotencyGuard} (the `w.idempotentBy(token)`
|
|
2482
|
-
* client-token guard `Put`, `attribute_not_exists`). Present only when the lifecycle
|
|
2483
|
-
* declares `idempotency`; absent otherwise (no regression). Its `Put` composes into
|
|
2484
|
-
* the method's atomic `TransactWriteItems`; a same-token re-execution fails the
|
|
2485
|
-
* guard and rolls the WHOLE transaction back — so no effect is double-applied.
|
|
2486
|
-
*/
|
|
2487
|
-
readonly idempotencyGuard?: DerivedIdempotencyGuard;
|
|
2488
|
-
}
|
|
2489
|
-
/**
|
|
2490
|
-
* Resolve the {@link LifecycleContract} a fragment adopts for its intent:
|
|
2491
|
-
* `use:` (the whole save contract) when present, else the target model's own
|
|
2492
|
-
* `entityWrites`, then the lifecycle entry for the intent's phase. Returns
|
|
2493
|
-
* `undefined` (→ the trivial base op) when no save contract declares the phase or
|
|
2494
|
-
* the target declares no `writes`.
|
|
2495
|
-
*/
|
|
2496
|
-
declare function resolveLifecycle(fragment: MutationFragment): LifecycleContract | undefined;
|
|
2497
|
-
/**
|
|
2498
|
-
* A resolver for a {@link MutationEntityRef} (`$.entity[i].field`) — supplied by
|
|
2499
|
-
* the multi-fragment compiler ({@link compileMutationPlan}) so a later fragment's
|
|
2500
|
-
* leaf can be resolved to the **earlier** fragment's binding for that field. It
|
|
2501
|
-
* returns the already-rendered value the producer fragment stores at that field
|
|
2502
|
-
* (a {@link ContractParamRef} / {@link ContractKeyFieldRef} / literal), so the
|
|
2503
|
-
* cross-fragment value renders from the **same** shared input at execution time
|
|
2504
|
-
* (a `TransactWriteItems` cannot read another item mid-transaction). `null` for a
|
|
2505
|
-
* single-fragment compile, where a cross-fragment reference is meaningless.
|
|
2506
|
-
*/
|
|
2507
|
-
type EntityRefResolver = (ref: MutationEntityRef, consumerIndex: number, consumerField: string) => unknown;
|
|
2508
|
-
/**
|
|
2509
|
-
* Compile one fragment into a {@link CompiledFragment}. Builds the intent's base
|
|
2510
|
-
* op as a {@link ContractMethodOp}:
|
|
2511
|
-
*
|
|
2512
|
-
* - `create` → a `put` whose `item` is the fragment input (primary-key fields as
|
|
2513
|
-
* {@link ContractKeyFieldRef}, the rest as {@link ContractParamRef}), guarded by
|
|
2514
|
-
* `attribute_not_exists(PK)` (`{ notExists: true }`);
|
|
2515
|
-
* - `update` → an `update` whose `keys` is the whole-keys sentinel with
|
|
2516
|
-
* `keyFields` = the primary-key fields (bound from `$.input.*`), and whose
|
|
2517
|
-
* `changes` is the **non-key** fragment input;
|
|
2518
|
-
* - `remove` → a `delete` whose `keys` is the whole-keys sentinel + `keyFields`.
|
|
2519
|
-
*
|
|
2520
|
-
* The resolved lifecycle's effect set is attached as the empty per-fragment hook.
|
|
2521
|
-
*
|
|
2522
|
-
* @param fragment The fragment to compile.
|
|
2523
|
-
* @param index Its 0-based position in the mutation's fragment list (the
|
|
2524
|
-
* cross-fragment reference index). `0` for a single-fragment compile.
|
|
2525
|
-
* @param resolveEntityRef Resolver for a `$.entity[i].field` cross-fragment
|
|
2526
|
-
* reference (multi-fragment compile), or `null` for a single-fragment compile
|
|
2527
|
-
* (where such a reference is a hard error).
|
|
2528
|
-
*/
|
|
2529
|
-
declare function compileFragment(fragment: MutationFragment, index?: number, resolveEntityRef?: EntityRefResolver | null): CompiledFragment;
|
|
2530
|
-
/**
|
|
2531
|
-
* Compile a single-fragment {@link CommandPlan} into its {@link CompiledFragment}.
|
|
2532
|
-
* The N-fragment atomic merge is **#90** ({@link compileMutationPlan}); this entry
|
|
2533
|
-
* point is the single-fragment fast path used by #83 and by the multi-fragment
|
|
2534
|
-
* compiler for `N === 1` (no behavioral change). A cross-fragment reference is a
|
|
2535
|
-
* hard error here — there is no earlier fragment to read from.
|
|
2536
|
-
*
|
|
2537
|
-
* @throws if `plan` is not a {@link CommandPlan}, or declares more than one fragment.
|
|
2538
|
-
*/
|
|
2539
|
-
declare function compileSingleFragmentPlan(plan: CommandPlan): CompiledFragment;
|
|
2540
|
-
/**
|
|
2541
|
-
* The result of compiling a (1..N)-fragment {@link CommandPlan} (#90): the ordered
|
|
2542
|
-
* per-fragment {@link CompiledFragment}s, merged so they form **one atomic
|
|
2543
|
-
* `TransactWriteItems`**. For a single fragment this is exactly the #83 result with
|
|
2544
|
-
* `fragments.length === 1` (no regression); for N≥2 the fragments are validated
|
|
2545
|
-
* together — same-item conflicts rejected, cross-fragment `$.entity[i].field`
|
|
2546
|
-
* dependencies resolved (circular deps rejected), and the composed item count
|
|
2547
|
-
* capped at {@link MAX_TRANSACT_COMPOSE_ITEMS}.
|
|
2548
|
-
*
|
|
2549
|
-
* The **primary fragment** is fragment 0 — the entity the public Command IF is
|
|
2550
|
-
* keyed by and read back from (`returnSelection`); the proposal's `CreatePost`
|
|
2551
|
-
* keys on `Post` (fragment 0), with `AuditLog` (fragment 1) composed atomically.
|
|
2552
|
-
*/
|
|
2553
|
-
interface CompiledMutationPlan {
|
|
2554
|
-
/** The mutation name (documentary; surfaces in compiler errors). */
|
|
2555
|
-
readonly name: string;
|
|
2556
|
-
/** Each fragment compiled to its base op, in declaration order. */
|
|
2557
|
-
readonly fragments: readonly CompiledFragment[];
|
|
2558
|
-
}
|
|
2559
|
-
/**
|
|
2560
|
-
* Compile a (1..N)-fragment {@link CommandPlan} into one **atomically composable**
|
|
2561
|
-
* {@link CompiledMutationPlan} (issue #90; proposal §3, "CROSS-FRAGMENT composition
|
|
2562
|
-
* + validation"). Each fragment is compiled with #83's per-fragment compiler; the
|
|
2563
|
-
* resulting items are **merged into one `TransactWriteItems`**, with every MUST AC
|
|
2564
|
-
* enforced at build time:
|
|
2565
|
-
*
|
|
2566
|
-
* 1. **Cross-fragment data dependencies** — a fragment's `$.entity[i].field` leaf
|
|
2567
|
-
* is resolved to fragment `i`'s binding for `field` (a build-time alias, so the
|
|
2568
|
-
* value renders from the same shared input at execution; a `TransactWriteItems`
|
|
2569
|
-
* cannot read another item mid-transaction). A reference to a **same-or-later**
|
|
2570
|
-
* fragment (`i ≥ consumer`) is a **circular** dependency and is rejected; an
|
|
2571
|
-
* out-of-range index, or a field the producer does not write, is rejected.
|
|
2572
|
-
* 2. **Same-item conflict** — two fragments that write the **same primary key**
|
|
2573
|
-
* ({@link keySignature}) in one transaction are rejected (DynamoDB rejects a
|
|
2574
|
-
* transaction touching one item twice), naming the conflicting fragments.
|
|
2575
|
-
* 3. **≤25 items** — a composed plan over {@link MAX_TRANSACT_COMPOSE_ITEMS} base
|
|
2576
|
-
* items is a hard error (an atomic transaction is NEVER split — atomicity, #64).
|
|
2577
|
-
*
|
|
2578
|
-
* Because fragment `i` is fully compiled before fragment `i+1`, and a cross-fragment
|
|
2579
|
-
* reference may only point **backward** (to an already-compiled fragment), resolving
|
|
2580
|
-
* dependencies in declaration order is itself the topological order — a forward /
|
|
2581
|
-
* self reference is the only cycle shape and is rejected directly, so no separate
|
|
2582
|
-
* cycle search is needed.
|
|
2583
|
-
*
|
|
2584
|
-
* @throws if `plan` is not a {@link CommandPlan}; on a malformed / circular / dangling
|
|
2585
|
-
* cross-fragment reference; on a same-item conflict; or on >25 composed items.
|
|
2586
|
-
*/
|
|
2587
|
-
declare function compileMutationPlan(plan: CommandPlan): CompiledMutationPlan;
|
|
2588
|
-
|
|
2589
|
-
/**
|
|
2590
|
-
* Contract DSL — `publicQueryModel` / `publicCommandModel` (issue #58, CQRS
|
|
2591
|
-
* Contract layer, Epic #57; spec `docs/cqrs-contract.md`).
|
|
2592
|
-
*
|
|
2593
|
-
* A **QueryModel / CommandModel** is the public, storage-independent interface
|
|
2594
|
-
* of a CQRS (logical) service. It is **keyed** — its Key *is* the access pattern
|
|
2595
|
-
* — and holds **one or more named Methods**, each a distinct **use case** (e.g.
|
|
2596
|
-
* `get` + `summary` over the same key, differing only in projection). The Key is
|
|
2597
|
-
* the access pattern; the Method is the use case.
|
|
2598
|
-
*
|
|
2599
|
-
* This module sits **on top of** the existing definition DSL
|
|
2600
|
-
* (`defineQuery` / `defineList` / `definePut` / `defineUpdate` / `defineDelete`,
|
|
2601
|
-
* `src/define/define.ts`) and shares its internal representation
|
|
2602
|
-
* ({@link OperationDefinition}). A contract method body resolves to a single
|
|
2603
|
-
* declarative operation on the underlying model; the closure is **never**
|
|
2604
|
-
* serialized — only the resolved declaration is captured into the IR.
|
|
2605
|
-
*
|
|
2606
|
-
* ```ts
|
|
2607
|
-
* const ArticleById = publicQueryModel<ArticleIdKey>()({
|
|
2608
|
-
* get: (keys, params) =>
|
|
2609
|
-
* Article.query(keys, { articleId: true, title: true, body: true }, params),
|
|
2610
|
-
* });
|
|
2611
|
-
* ```
|
|
2612
|
-
*
|
|
2613
|
-
* ## Method signatures (the generation source)
|
|
2614
|
-
*
|
|
2615
|
-
* Every method has one uniform signature, accepting **either one key or an array
|
|
2616
|
-
* of keys** (the array form is what makes a contract uniformly batch-resolvable):
|
|
2617
|
-
*
|
|
2618
|
-
* ```ts
|
|
2619
|
-
* type QueryMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
|
|
2620
|
-
* type CommandMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
|
|
2621
|
-
* ```
|
|
2622
|
-
*
|
|
2623
|
-
* These two types are the **generation source** for every binding target
|
|
2624
|
-
* (OpenAPI / Python / PHP / Go / Rust) — see the proposal.
|
|
2625
|
-
*
|
|
2626
|
-
* ## Method names are use cases, not operations
|
|
2627
|
-
*
|
|
2628
|
-
* A method is named for its **use case** (`get` / `summary` / `recent` /
|
|
2629
|
-
* `disable`), never for the storage op it runs. Whether it resolves internally
|
|
2630
|
-
* to a `query` (point lookup) or a `list` (partition read) is the **Runtime's**
|
|
2631
|
-
* concern and is invisible to callers and to generated bindings. The internal op
|
|
2632
|
-
* still determines the **derived** `resolution` / `inputArity` facts (below), but
|
|
2633
|
-
* those are bookkeeping, not part of the method's public name.
|
|
2634
|
-
*
|
|
2635
|
-
* ## Derived facts: `resolution` and `inputArity`
|
|
2636
|
-
*
|
|
2637
|
-
* Per the proposal's N+1 Safety section these are **derived** from the internal
|
|
2638
|
-
* op kind, never hand-written:
|
|
2639
|
-
*
|
|
2640
|
-
* - a unique-key `query` / `GetItem` → `resolution: 'point'`, `inputArity:
|
|
2641
|
-
* 'either'` (a key array becomes one `BatchGetItem`);
|
|
2642
|
-
* - a partition `list` / `Query` → `resolution: 'range'`, `inputArity: 'single'`
|
|
2643
|
-
* (DynamoDB cannot coalesce N partition `Query`s — feeding an array would be an
|
|
2644
|
-
* N+1 fan-out, so the range method only accepts a single key).
|
|
2645
|
-
*
|
|
2646
|
-
* Command methods additionally carry an explicit **result type** (`void` | a
|
|
2647
|
-
* `Result` | the updated entity) as part of the contract; see
|
|
2648
|
-
* {@link CommandResultKind}.
|
|
2649
|
-
*
|
|
2650
|
-
* ## Build-time hardening (reused from `defineTransaction`)
|
|
2651
|
-
*
|
|
2652
|
-
* The method body is evaluated at definition time with **throwing sentinels** in
|
|
2653
|
-
* place of `keys` / `params`, recorded through a model recorder, and subjected to
|
|
2654
|
-
* the **same** hardening `defineTransaction` (`src/define/transaction.ts`) applies
|
|
2655
|
-
* to value positions:
|
|
2656
|
-
*
|
|
2657
|
-
* 1. **Throwing sentinel Proxy** — every property / method access other than the
|
|
2658
|
-
* internal brand reads and primitive coercion throws at the access site.
|
|
2659
|
-
* 2. **Per-access coercion ledger (primary boundary)** — a faithful reference is
|
|
2660
|
-
* stored as the Proxy object and is *never* coerced, so any coercion of a
|
|
2661
|
-
* `params` field is positive proof the body consumed it into a transform /
|
|
2662
|
-
* branch / interpolation and the build is rejected.
|
|
2663
|
-
* 3. **Differential evaluation (secondary)** — the body runs twice with disjoint
|
|
2664
|
-
* per-field markers; every recorded leaf must be a faithful reference or a
|
|
2665
|
-
* byte-identical literal across passes.
|
|
2666
|
-
* 4. **Consumed-field check** — a `params` field the body accessed but that never
|
|
2667
|
-
* reached the recorded operation (a silent value branch) is rejected.
|
|
2668
|
-
*
|
|
2669
|
-
* So any non-declarative body (a transform, a value branch, a coercion, arbitrary
|
|
2670
|
-
* JS over `keys` / `params`) is rejected at build time — never silently emitted
|
|
2671
|
-
* as a wrong spec. The #58 issue was the **DSL + IR + type-level surface only**:
|
|
2672
|
-
* serialization to the JSON SSoT (#59) and execution against DynamoDB (#62) were
|
|
2673
|
-
* out of scope there.
|
|
2674
|
-
*
|
|
2675
|
-
* ## External Query composition (`query` / `from`, issue #63)
|
|
2676
|
-
*
|
|
2677
|
-
* The {@link query} / {@link from} primitive (added by #63) extends a read method
|
|
2678
|
-
* body with a **build-time-resolved, in-process** reference to **another query
|
|
2679
|
-
* contract's** method — relation chaining where the link is a contract reference
|
|
2680
|
-
* instead of a model relation (there is no protocol / transport). Placed at a
|
|
2681
|
-
* `select` property (`as: query(OtherContract.method, { childKey: from("$.field") })`),
|
|
2682
|
-
* it is recorded as a declarative {@link RecordedCompose} edge on the method op
|
|
2683
|
-
* (the property name becomes `as`, and it is removed from the read projection).
|
|
2684
|
-
* The binding stays declarative — `from` admits only a literal `$`-rooted path —
|
|
2685
|
-
* so no arbitrary JS reaches a key binding. The serializer (#59) recovers the
|
|
2686
|
-
* referenced contract name by identity and emits the proposal's `compose` shape;
|
|
2687
|
-
* the N+1 checker (#60) enforces the `point`-child rule; the runtimes (#62 infra
|
|
2688
|
-
* reused) resolve the child once, batched, across all parent records.
|
|
2689
|
-
*/
|
|
2690
|
-
|
|
2691
|
-
/**
|
|
2692
|
-
* The **resolution kind** of a method, derived from its internal op:
|
|
2693
|
-
*
|
|
2694
|
-
* - `'point'` — target keys are known (unique-key `query` / `GetItem`, fixed key
|
|
2695
|
-
* sets); coalesces to a `BatchGetItem` for a key array.
|
|
2696
|
-
* - `'range'` — target key set is unknown (partition `list` / `Query`); one
|
|
2697
|
-
* request per partition key, so it is only ever safe for a single key.
|
|
2698
|
-
*/
|
|
2699
|
-
type Resolution = 'point' | 'range';
|
|
2700
|
-
/**
|
|
2701
|
-
* What input arity a method accepts, derived from its `resolution`:
|
|
2702
|
-
*
|
|
2703
|
-
* - `'either'` — a single key **or** an array (a `point` read; the array form is
|
|
2704
|
-
* one `BatchGetItem`).
|
|
2705
|
-
* - `'single'` — a single key only (a `range` read; an array would be an N+1
|
|
2706
|
-
* fan-out and is rejected by construction).
|
|
2707
|
-
* - `'array'` — an array only (reserved; not produced by the current resolvers).
|
|
2708
|
-
*/
|
|
2709
|
-
type InputArity = 'single' | 'array' | 'either';
|
|
2710
|
-
/**
|
|
2711
|
-
* The category of a {@link CommandMethod}'s declared result, part of the
|
|
2712
|
-
* contract (it surfaces in OpenAPI and every binding):
|
|
2713
|
-
*
|
|
2714
|
-
* - `'void'` — fire-and-forget write (no body);
|
|
2715
|
-
* - `'result'` — an outcome/status object (e.g. `{ ok, version }`);
|
|
2716
|
-
* - `'entity'` — the updated entity (the post-write projection).
|
|
2717
|
-
*
|
|
2718
|
-
* The concrete `TResult` type is preserved at the type level on
|
|
2719
|
-
* {@link CommandMethodSpec}; this discriminant is the runtime/SSoT-facing label.
|
|
2720
|
-
*/
|
|
2721
|
-
type CommandResultKind = 'void' | 'result' | 'entity';
|
|
2722
|
-
/**
|
|
2723
|
-
* How a command method resolves an **array of keys** to the underlying write
|
|
2724
|
-
* surface (proposal "Consistency with the existing write surface"; issue #64).
|
|
2725
|
-
* A single key always maps to one write op; an array maps to a batched write,
|
|
2726
|
-
* and a contract **must declare which** form it resolves to:
|
|
2727
|
-
*
|
|
2728
|
-
* - `'transact'` — a `TransactWriteItems`: the per-key write is applied
|
|
2729
|
-
* **atomically** (all-or-nothing) and **may carry the write condition subset**
|
|
2730
|
-
* ({@link ConditionInput}, `{ notExists }` / equality). DynamoDB caps a
|
|
2731
|
-
* `TransactWriteItems` at **25 items** and the call is atomic, so an array of
|
|
2732
|
-
* >25 keys **cannot** be split (that would break atomicity); the runtime
|
|
2733
|
-
* rejects it with a clear error rather than silently truncating or splitting.
|
|
2734
|
-
* - `'batchWrite'` — a `BatchWriteItem`: the per-key writes are applied
|
|
2735
|
-
* **non-atomically** and **carry no conditions** (DynamoDB's `BatchWriteItem`
|
|
2736
|
-
* cannot express a `ConditionExpression`). An array of any size is permitted;
|
|
2737
|
-
* the runtime chunks it per DynamoDB's 25-items-per-request limit and retries
|
|
2738
|
-
* `UnprocessedItems`, reusing the existing batch machinery.
|
|
2739
|
-
*
|
|
2740
|
-
* The single-key form is always one write op regardless of this declaration;
|
|
2741
|
-
* `batchMode` only decides how a key **array** is executed.
|
|
2742
|
-
*/
|
|
2743
|
-
type CommandBatchMode = 'transact' | 'batchWrite';
|
|
2744
|
-
/**
|
|
2745
|
-
* Runtime guard: is `value` a {@link CommandBatchMode} literal (`'transact'` /
|
|
2746
|
-
* `'batchWrite'`)? Used by {@link recordContractOp} to validate a declared batch
|
|
2747
|
-
* mode at the recording boundary.
|
|
2748
|
-
*/
|
|
2749
|
-
declare function isCommandBatchMode(value: unknown): value is CommandBatchMode;
|
|
2750
|
-
/**
|
|
2751
|
-
* The formal **query** method signature (proposal: "Query Method — Common
|
|
2752
|
-
* Interface"). Accepts a single key **or** an array of keys and returns
|
|
2753
|
-
* `TResult`. This one type is the generation source for every binding target.
|
|
2754
|
-
*
|
|
2755
|
-
* @typeParam TKey - The contract Key (the access pattern / join key).
|
|
2756
|
-
* @typeParam TParams - The retrieval options (pagination / cursor / consistency).
|
|
2757
|
-
* @typeParam TResult - The method's result type.
|
|
2758
|
-
*/
|
|
2759
|
-
type QueryMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
|
|
2760
|
-
/**
|
|
2761
|
-
* The formal **command** method signature (proposal: "Command Definition").
|
|
2762
|
-
* Symmetric to {@link QueryMethod}, with an explicit result type. A single key
|
|
2763
|
-
* maps to one write op; an array maps to a batched write.
|
|
2764
|
-
*
|
|
2765
|
-
* @typeParam TKey - The contract Key (the target identity).
|
|
2766
|
-
* @typeParam TParams - The call params.
|
|
2767
|
-
* @typeParam TResult - The declared result type (`void` | a `Result` | entity).
|
|
2768
|
-
*/
|
|
2769
|
-
type CommandMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
|
|
2770
|
-
/**
|
|
2771
|
-
* The options a **command** contract method body passes to a model write
|
|
2772
|
-
* (`Model.put` / `Model.update` / `Model.delete`) to declare its batched-write
|
|
2773
|
-
* resolution (issue #64) and, optionally, a write condition:
|
|
2774
|
-
*
|
|
2775
|
-
* ```ts
|
|
2776
|
-
* disableMany: (keys, params: { reason: string }) =>
|
|
2777
|
-
* User.update(
|
|
2778
|
-
* keys,
|
|
2779
|
-
* { status: 'disabled', disableReason: params.reason },
|
|
2780
|
-
* { batch: 'transact', condition: { notExists: true } },
|
|
2781
|
-
* ),
|
|
2782
|
-
* ```
|
|
2783
|
-
*
|
|
2784
|
-
* This is the **type-safe** declaration point: `batch` is constrained to a
|
|
2785
|
-
* {@link CommandBatchMode} literal, so a typo'd mode is a compile error, and the
|
|
2786
|
-
* recorder ({@link resolveBatchMode}) re-validates at build time (e.g. it rejects
|
|
2787
|
-
* a `condition` under `'batchWrite'`, which DynamoDB cannot express). The
|
|
2788
|
-
* single-key execution form ignores `batch`; only a key **array** consults it.
|
|
2789
|
-
*/
|
|
2790
|
-
interface ContractWriteOptions {
|
|
2791
|
-
/**
|
|
2792
|
-
* How a **key array** resolves to the underlying write surface: `'transact'`
|
|
2793
|
-
* (atomic `TransactWriteItems`, ≤25, condition-capable) or `'batchWrite'`
|
|
2794
|
-
* (non-atomic `BatchWriteItem`, no conditions, chunked). Omit to resolve only a
|
|
2795
|
-
* single key (an array is then rejected at execution).
|
|
2796
|
-
*/
|
|
2797
|
-
readonly batch?: CommandBatchMode;
|
|
2798
|
-
/**
|
|
2799
|
-
* An optional declarative write condition — the existing subset
|
|
2800
|
-
* ({@link ConditionInput}: `{ notExists }` or equality). Valid for a single
|
|
2801
|
-
* write and for a `'transact'` batch; **not** for `'batchWrite'` (rejected at
|
|
2802
|
-
* build time).
|
|
2803
|
-
*/
|
|
2804
|
-
readonly condition?: ConditionInput;
|
|
2805
|
-
}
|
|
2806
|
-
declare const KEY_REF_BRAND: unique symbol;
|
|
2807
|
-
declare const KEY_FIELD_REF_BRAND: unique symbol;
|
|
2808
|
-
/**
|
|
2809
|
-
* The captured `keys` argument of a contract method body. A branded sentinel that
|
|
2810
|
-
* supports the two faithful uses the proposal's examples need:
|
|
2811
|
-
*
|
|
2812
|
-
* - **passed whole** into a model op's key position
|
|
2813
|
-
* (`Model.query(keys, …)` / `Model.update(keys, …)`) — the point / write form;
|
|
2814
|
-
* - **destructured per field** to rebuild a partition key
|
|
2815
|
-
* (`Model.list({ categoryId: keys.categoryId }, …)`) — the range form. Each
|
|
2816
|
-
* `keys.<field>` mints a faithful {@link ContractKeyFieldRef}.
|
|
2817
|
-
*
|
|
2818
|
-
* Both forms are declarative: the recorded key slot is either this sentinel
|
|
2819
|
-
* itself (whole) or a `{ field: ContractKeyFieldRef }` record. Any non-faithful
|
|
2820
|
-
* use — coercing `keys`, or transforming a `keys.<field>` ref — is rejected by the
|
|
2821
|
-
* same coercion ledger / differential checks as `params`.
|
|
2822
|
-
*/
|
|
2823
|
-
interface ContractKeyRef {
|
|
2824
|
-
readonly [KEY_REF_BRAND]: true;
|
|
2825
|
-
}
|
|
2826
|
-
/**
|
|
2827
|
-
* A captured reference to a single field of the method's `keys` argument
|
|
2828
|
-
* (`keys.<field>`), minted when a range body rebuilds a partition key. Branded so
|
|
2829
|
-
* the verifier can distinguish a faithful key-field reference from a literal, and
|
|
2830
|
-
* so it cannot be silently coerced.
|
|
2831
|
-
*/
|
|
2832
|
-
interface ContractKeyFieldRef {
|
|
2833
|
-
readonly [KEY_FIELD_REF_BRAND]: true;
|
|
2834
|
-
/** The key field name, e.g. `categoryId`. */
|
|
2835
|
-
readonly field: string;
|
|
2836
|
-
/** The template token this reference renders to, e.g. `{key.categoryId}`. */
|
|
2837
|
-
readonly token: string;
|
|
2838
|
-
}
|
|
2839
|
-
/** Runtime guard: is `value` a {@link ContractKeyRef} (the whole key sentinel)? */
|
|
2840
|
-
declare function isContractKeyRef(value: unknown): value is ContractKeyRef;
|
|
2841
|
-
/**
|
|
2842
|
-
* Mint a faithful {@link ContractKeyFieldRef} for a named key field — a plain
|
|
2843
|
-
* (non-Proxy) reference rendering the `{key.field}` differential token but, at a
|
|
2844
|
-
* **value** position (a `put` item field), serialized as `{field}` (see
|
|
2845
|
-
* `templateLeaf`, `src/spec/operations.ts`). Used by the **mutation compiler**
|
|
2846
|
-
* (#83) to mark which of a `create` fragment's item fields are the model's
|
|
2847
|
-
* primary-key (contract Key) fields, so the existing serializer recovers the
|
|
2848
|
-
* contract Key from the put item (`keyFieldsOf`) exactly as it does for a
|
|
2849
|
-
* hand-written #64 `put` that binds `keys.<field>` into the item. Mirrors
|
|
2850
|
-
* {@link mintContractParamRef}.
|
|
2851
|
-
*
|
|
2852
|
-
* @param field The key field name (rendered `{field}` at a value position).
|
|
2853
|
-
*/
|
|
2854
|
-
declare function mintContractKeyFieldRef(field: string): ContractKeyFieldRef;
|
|
2855
|
-
/**
|
|
2856
|
-
* The whole-`keys` sentinel value an `update` / `delete` op records in its key
|
|
2857
|
-
* slot when the body passed `keys` whole into the op's key position. Exposed so
|
|
2858
|
-
* the **mutation compiler** (#83) can build an equivalent planned `update` /
|
|
2859
|
-
* `delete` op without re-entering the hardening machinery (a mutation has no
|
|
2860
|
-
* `keys` argument — its key fields are bound from `$.input.*`, captured via
|
|
2861
|
-
* {@link ContractMethodOp.keyFields}).
|
|
2862
|
-
*/
|
|
2863
|
-
declare function wholeKeysSentinel(): ContractKeyRef;
|
|
2864
|
-
/** Runtime guard: is `value` a {@link ContractKeyFieldRef}? */
|
|
2865
|
-
declare function isContractKeyFieldRef(value: unknown): value is ContractKeyFieldRef;
|
|
2866
|
-
declare const PARAM_REF_BRAND: unique symbol;
|
|
2867
|
-
/**
|
|
2868
|
-
* A captured reference to a field of the method's `params` argument (`{<field>}`).
|
|
2869
|
-
* Branded so the recorder can distinguish a faithful reference from a concrete
|
|
2870
|
-
* literal, and so a stray reference cannot be silently coerced.
|
|
2871
|
-
*/
|
|
2872
|
-
interface ContractParamRef {
|
|
2873
|
-
readonly [PARAM_REF_BRAND]: true;
|
|
2874
|
-
/** The template token this reference renders to, e.g. `{limit}`. */
|
|
2875
|
-
readonly token: string;
|
|
2876
|
-
/** The field name on `params`, e.g. `limit`. */
|
|
2877
|
-
readonly field: string;
|
|
2878
|
-
}
|
|
2879
|
-
/** Runtime guard: is `value` a {@link ContractParamRef}? */
|
|
2880
|
-
declare function isContractParamRef(value: unknown): value is ContractParamRef;
|
|
2881
|
-
/**
|
|
2882
|
-
* Mint a faithful {@link ContractParamRef} for a named field — a concrete
|
|
2883
|
-
* (non-Proxy) reference rendering the `{field}` token. Unlike the hardening
|
|
2884
|
-
* sentinels {@link makeParamFieldRef} builds during a contract method body (which
|
|
2885
|
-
* throw on any transform to detect non-declarative use), this is a *plain* ref
|
|
2886
|
-
* used by the **mutation compiler** (#83, `src/spec/mutation-command.ts`): a
|
|
2887
|
-
* mutation fragment's `input` binding is captured as a {@link MutationInputRef}
|
|
2888
|
-
* (its own declarative sentinel), and the compiler translates each one into this
|
|
2889
|
-
* `ContractParamRef` so the **existing** contract serializer
|
|
2890
|
-
* ({@link opToDefinition} → {@link buildCommandSpec}) and TS runtime
|
|
2891
|
-
* ({@link renderLeaf}) consume it with **zero** new template / param machinery —
|
|
2892
|
-
* the param name and `{token}` are the input field name, exactly as a hand-written
|
|
2893
|
-
* `params.field` reference would render.
|
|
2894
|
-
*
|
|
2895
|
-
* @param field The param / input field name (the rendered token is `{field}`).
|
|
2896
|
-
*/
|
|
2897
|
-
declare function mintContractParamRef(field: string): ContractParamRef;
|
|
2898
|
-
declare const FROM_REF_BRAND: unique symbol;
|
|
2899
|
-
declare const COMPOSE_NODE_BRAND: unique symbol;
|
|
2900
|
-
/**
|
|
2901
|
-
* A parent-result source path produced by {@link from} (proposal "External
|
|
2902
|
-
* Query"). It names the field of the **parent** method's result that supplies a
|
|
2903
|
-
* composed child's key, e.g. `from("$.billingAccountId")`. The path is a plain
|
|
2904
|
-
* `"$.field"` string — the binding is **declarative** (no arbitrary JS): the
|
|
2905
|
-
* value is read from the resolved parent record at execution time, never computed.
|
|
2906
|
-
*
|
|
2907
|
-
* The grammar is intentionally minimal — a single dotted field path rooted at
|
|
2908
|
-
* `$` (the parent record). `$.a.b` reaches a nested field; anything else (a bare
|
|
2909
|
-
* field with no `$`, an index, a wildcard, an empty path) is rejected by
|
|
2910
|
-
* {@link from} at build time so a malformed binding can never reach the SSoT.
|
|
2911
|
-
*/
|
|
2912
|
-
interface ContractFromRef {
|
|
2913
|
-
readonly [FROM_REF_BRAND]: true;
|
|
2914
|
-
/** The `$`-rooted source path on the parent result, e.g. `$.billingAccountId`. */
|
|
2915
|
-
readonly path: string;
|
|
2916
|
-
}
|
|
2917
|
-
/** Runtime guard: is `value` a {@link ContractFromRef} (a `from(...)` binding)? */
|
|
2918
|
-
declare function isContractFromRef(value: unknown): value is ContractFromRef;
|
|
2919
|
-
/**
|
|
2920
|
-
* A resolved External Query composition node produced by {@link query} and placed
|
|
2921
|
-
* at a `select` property of a contract method body (proposal "External Query"):
|
|
2922
|
-
*
|
|
2923
|
-
* ```ts
|
|
2924
|
-
* Account.query(keys, {
|
|
2925
|
-
* accountId: true,
|
|
2926
|
-
* billing: query(BillingPlanQueries.get, { accountId: from("$.billingAccountId") }),
|
|
2927
|
-
* })
|
|
2928
|
-
* ```
|
|
2929
|
-
*
|
|
2930
|
-
* It carries a faithful reference to the **referenced method spec** (so the
|
|
2931
|
-
* serializer can recover the referenced contract's *name* by identity against the
|
|
2932
|
-
* contract map, and its derived `resolution` / `cardinality` facts) plus the
|
|
2933
|
-
* declarative child-key binding (child key field → a {@link ContractFromRef}).
|
|
2934
|
-
* The owning `select` key becomes the composition's `as` property at record time
|
|
2935
|
-
* (it is not known to {@link query} itself).
|
|
2936
|
-
*/
|
|
2937
|
-
interface ContractComposeNode {
|
|
2938
|
-
readonly [COMPOSE_NODE_BRAND]: true;
|
|
2939
|
-
/** The referenced query method spec (`OtherContract.method`). */
|
|
2940
|
-
readonly method: QueryMethodSpec<unknown, unknown, unknown>;
|
|
2941
|
-
/** The child-key binding: child key field → parent-result `from` path. */
|
|
2942
|
-
readonly bind: Readonly<Record<string, ContractFromRef>>;
|
|
2943
|
-
}
|
|
2944
|
-
/** Runtime guard: is `value` a {@link ContractComposeNode} (a `query(...)` node)? */
|
|
2945
|
-
declare function isContractComposeNode(value: unknown): value is ContractComposeNode;
|
|
2946
|
-
/**
|
|
2947
|
-
* Declare a parent-result source path for an External Query child key binding
|
|
2948
|
-
* (proposal "External Query"). Used **only** inside a {@link query} binding:
|
|
2949
|
-
*
|
|
2950
|
-
* ```ts
|
|
2951
|
-
* query(BillingPlanQueries.get, { accountId: from("$.billingAccountId") })
|
|
2952
|
-
* ```
|
|
2953
|
-
*
|
|
2954
|
-
* The path is `$`-rooted (the parent record) and dotted; the bound child-key
|
|
2955
|
-
* field is read from that path of each resolved parent record at execution time.
|
|
2956
|
-
* The binding is **declarative** — `from` accepts only a literal path string and
|
|
2957
|
-
* rejects any other shape, so no arbitrary JS can sneak into a key binding.
|
|
2958
|
-
*
|
|
2959
|
-
* @param path A `$`-rooted dotted path, e.g. `"$.billingAccountId"` or `"$.a.b"`.
|
|
2960
|
-
* @throws if `path` is not a non-empty `$`-rooted dotted field path.
|
|
2961
|
-
*/
|
|
2962
|
-
declare function from(path: string): ContractFromRef;
|
|
2963
|
-
/**
|
|
2964
|
-
* Reference another query contract's method as an **External Query** composition
|
|
2965
|
-
* child, bound to the parent result by a declarative `from` mapping (proposal
|
|
2966
|
-
* "Query Composition" / "External Query"). Place the result at a `select`
|
|
2967
|
-
* property of a contract method body; the property name becomes the composed
|
|
2968
|
-
* value's `as`:
|
|
2969
|
-
*
|
|
2970
|
-
* ```ts
|
|
2971
|
-
* export const AccountAccess = publicQueryModel<AccountKey>()({
|
|
2972
|
-
* get: (keys, params) => Account.query(keys, {
|
|
2973
|
-
* accountId: true,
|
|
2974
|
-
* billing: query(BillingPlanQueries.get, { accountId: from("$.billingAccountId") }),
|
|
2975
|
-
* }, params),
|
|
2976
|
-
* });
|
|
2977
|
-
* ```
|
|
2978
|
-
*
|
|
2979
|
-
* This is **build-time-resolved, in-process** contract chaining — the relation
|
|
2980
|
-
* primitive extended to point at another contract's method instead of a model
|
|
2981
|
-
* relation. There is no protocol / transport: the runtime collects every bound
|
|
2982
|
-
* key produced by the parent step and resolves the referenced contract **once,
|
|
2983
|
-
* batched** (proposal "External Query"). The child MUST be `point` (the parent
|
|
2984
|
-
* may yield N records, so a `range` child would be an N+1 fan-out) — that rule is
|
|
2985
|
-
* owned by the N+1 checker (#60), which rejects a `range` child at build time.
|
|
2986
|
-
*
|
|
2987
|
-
* @param method The referenced query method (`OtherContract.method`), a resolved
|
|
2988
|
-
* {@link QueryMethodSpec}.
|
|
2989
|
-
* @param bind The child-key binding: child key field → a {@link from} path on
|
|
2990
|
-
* the parent result. Must be non-empty and every value a `from(...)` ref.
|
|
2991
|
-
* @throws if `method` is not a query method spec, or `bind` is empty / carries a
|
|
2992
|
-
* non-`from` value.
|
|
2993
|
-
*/
|
|
2994
|
-
declare function query(method: QueryMethodSpec<unknown, unknown, unknown>, bind: Record<string, ContractFromRef>): ContractComposeNode;
|
|
2995
|
-
/**
|
|
2996
|
-
* The {@link QueryModelContract} that owns a resolved query method spec, or
|
|
2997
|
-
* `undefined` if the spec was not produced by {@link publicQueryModel} (so it has
|
|
2998
|
-
* no registered owner). Used by the serializer to resolve a composition's
|
|
2999
|
-
* referenced contract by identity.
|
|
3000
|
-
*/
|
|
3001
|
-
declare function contractOfMethodSpec(method: QueryMethodSpec<unknown, unknown, unknown>): QueryModelContract<unknown, Record<string, QueryMethodSpec<unknown, unknown, unknown>>> | undefined;
|
|
3002
|
-
/**
|
|
3003
|
-
* The declarative internal operation a contract method body resolves to — the
|
|
3004
|
-
* resolved declaration captured into the IR (the closure itself is discarded).
|
|
3005
|
-
* Mirrors {@link OperationDefinition} but is produced by the model recorder
|
|
3006
|
-
* handed to a contract method body rather than a `define*` entry point.
|
|
3007
|
-
*
|
|
3008
|
-
* @typeParam Op - The internal op kind (`query` | `list` | `put` | `update` |
|
|
3009
|
-
* `delete`).
|
|
3010
|
-
*/
|
|
3011
|
-
interface ContractMethodOp<Op extends OperationKind = OperationKind> {
|
|
3012
|
-
/** @internal Marks this object as a recorded contract method op. */
|
|
3013
|
-
readonly __isContractMethodOp: true;
|
|
3014
|
-
/** The model the op targets (name + runtime class). */
|
|
3015
|
-
readonly entity: EntityRef;
|
|
3016
|
-
/** The internal op kind. Determines the derived `resolution` / `inputArity`. */
|
|
3017
|
-
readonly operation: Op;
|
|
3018
|
-
/**
|
|
3019
|
-
* The captured key slot. Either the whole {@link ContractKeyRef} sentinel
|
|
3020
|
-
* (passed faithfully into the op's key position — the point / write form) or a
|
|
3021
|
-
* `{ field: ContractKeyFieldRef }` record rebuilt from `keys.<field>` (the range
|
|
3022
|
-
* form). For `put` it is the inert {@link PUT_HAS_NO_KEY} marker. Stored as
|
|
3023
|
-
* evidence of faithfulness; never serialized.
|
|
3024
|
-
*/
|
|
3025
|
-
readonly keys: ContractKeyRef | Readonly<Record<string, unknown>>;
|
|
3026
|
-
/**
|
|
3027
|
-
* The **explicit contract Key field names** captured from the factory's
|
|
3028
|
-
* key-field argument (`publicQueryModel<EmailKey>(['email'])(...)`, issue #71).
|
|
3029
|
-
* Present only for a **whole-`keys`** op (the {@link keys} slot is the whole
|
|
3030
|
-
* {@link ContractKeyRef} sentinel) when the author supplied a field list, or
|
|
3031
|
-
* when the model-derived factory form defaulted it to the model's primary-key
|
|
3032
|
-
* input fields. It is the SSoT for "which fields the Key is composed of" — fed
|
|
3033
|
-
* through {@link resolveKey} so the serializer emits the correct base-table PK
|
|
3034
|
-
* **or** GSI (`indexName` + GSI key template) key, and so the planner can tell a
|
|
3035
|
-
* coalescible base-table point (`GetItem` / `BatchGetItem`) from a non-coalescible
|
|
3036
|
-
* GSI point (a unique-GSI `Query`).
|
|
3037
|
-
*
|
|
3038
|
-
* Absent when the body rebuilds a partition key from `keys.<field>` references
|
|
3039
|
-
* (the range form already captures field names in the {@link keys} record) or
|
|
3040
|
-
* when no explicit list was given to a `publicQueryModel<TKey>()` whole-key form
|
|
3041
|
-
* (the legacy backward-compatible path, which then defaults to the primary key).
|
|
3042
|
-
*/
|
|
3043
|
-
readonly keyFields?: readonly string[];
|
|
3044
|
-
/** The select projection for `query` / `list`; `undefined` for writes. */
|
|
3045
|
-
readonly select?: Readonly<Record<string, unknown>>;
|
|
3046
|
-
/** The changes structure for `update`; `undefined` otherwise. */
|
|
3047
|
-
readonly changes?: Readonly<Record<string, unknown>>;
|
|
3048
|
-
/** The item structure for `put`; `undefined` otherwise. */
|
|
3049
|
-
readonly item?: Readonly<Record<string, unknown>>;
|
|
3050
|
-
/** The optional declarative write condition for writes. */
|
|
3051
|
-
readonly condition?: ConditionInput;
|
|
3052
|
-
/**
|
|
3053
|
-
* The declared batched-write resolution mode for a **write** op (issue #64):
|
|
3054
|
-
* how a *key array* resolves to the underlying write surface — a
|
|
3055
|
-
* `TransactWriteItems` (`'transact'`, atomic, condition-capable, ≤25) or a
|
|
3056
|
-
* `BatchWriteItem` (`'batchWrite'`, non-atomic, no conditions, chunked). Absent
|
|
3057
|
-
* when the author did not declare a batched form (the method then accepts a
|
|
3058
|
-
* single key only at execution; see {@link CommandBatchMode}). Never present on
|
|
3059
|
-
* a read op. The serializer (#59) reads this to emit the method's `batch`
|
|
3060
|
-
* resolution target.
|
|
3061
|
-
*/
|
|
3062
|
-
readonly batch?: CommandBatchMode;
|
|
3063
|
-
/**
|
|
3064
|
-
* External Query compositions (#63) extracted from the recorded `select`: each
|
|
3065
|
-
* `query(OtherContract.method, { childKey: from("$.parentField") })` node placed
|
|
3066
|
-
* at a `select` property becomes one {@link RecordedCompose} here (the property
|
|
3067
|
-
* name is its `as`), and the property is removed from the read projection. Absent
|
|
3068
|
-
* when the body declares no composition.
|
|
3069
|
-
*/
|
|
3070
|
-
readonly compose?: readonly RecordedCompose[];
|
|
3071
|
-
}
|
|
3072
|
-
/**
|
|
3073
|
-
* A recorded External Query composition edge on a contract method op (#63). The
|
|
3074
|
-
* declarative result of a `query(OtherContract.method, { … })` call placed at a
|
|
3075
|
-
* `select` property: it pairs the property name (`as`) with a faithful reference
|
|
3076
|
-
* to the referenced method spec and the declarative child-key binding. The
|
|
3077
|
-
* referenced contract's *name* and the child's derived `resolution` / `cardinality`
|
|
3078
|
-
* are recovered by the serializer (`src/spec/contracts.ts`) from the method spec
|
|
3079
|
-
* by identity against the contract map; they are not duplicated here.
|
|
3080
|
-
*/
|
|
3081
|
-
interface RecordedCompose {
|
|
3082
|
-
/** The result property the composed value attaches to (the `select` key). */
|
|
3083
|
-
readonly as: string;
|
|
3084
|
-
/** The referenced query method spec (`OtherContract.method`). */
|
|
3085
|
-
readonly method: QueryMethodSpec<unknown, unknown, unknown>;
|
|
3086
|
-
/** The child-key binding: child key field → parent-result `from` path. */
|
|
3087
|
-
readonly bind: Readonly<Record<string, ContractFromRef>>;
|
|
3088
|
-
}
|
|
3089
|
-
/**
|
|
3090
|
-
* The **call signature** a contract method exposes to a caller, narrowed by its
|
|
3091
|
-
* decided {@link InputArity} — the type-level half of N+1 rule (a) ("array into a
|
|
3092
|
-
* `range` method", #60). Given a method's `inputArity`:
|
|
3093
|
-
*
|
|
3094
|
-
* - `'single'` (a `range` method) — the key argument is a **single** `TKey` only;
|
|
3095
|
-
* passing an array (`readonly TKey[]`) is a **type error** by construction, so a
|
|
3096
|
-
* `range` method's array overload does not type-check.
|
|
3097
|
-
* - `'either'` (a `point` read / known-key write) — a single key **or** an array
|
|
3098
|
-
* (the array form coalesces to one `BatchGetItem` / batched write).
|
|
3099
|
-
* - `'array'` — an array only (reserved; not produced by the current resolvers).
|
|
3100
|
-
*
|
|
3101
|
-
* This mirrors the proposal's "the generated binding's types encode `inputArity`
|
|
3102
|
-
* (single → bare argument, array → array argument)". It is the type-level narrowing
|
|
3103
|
-
* used by the runtime call surface (`executeQueryMethod`, `src/runtime/contract-runtime.ts`):
|
|
3104
|
-
* the executor keys its key-argument type on the named method's **literal** arity
|
|
3105
|
-
* `A` (carried on {@link QueryMethodSpec} when the body is tagged with
|
|
3106
|
-
* {@link point} / {@link range}), so feeding an array into a `'single'` method is a
|
|
3107
|
-
* `tsc` compile error — the **primary** N+1 rule (a) layer, complementing the
|
|
3108
|
-
* build-time SSoT check ({@link assertContractN1Safe}) and the runtime backstop.
|
|
3109
|
-
*
|
|
3110
|
-
* @typeParam A - The decided input arity (a literal `InputArity`).
|
|
3111
|
-
* @typeParam TKey - The contract Key.
|
|
3112
|
-
* @typeParam TParams - The method's params type.
|
|
3113
|
-
* @typeParam TResult - The method's result type.
|
|
3114
|
-
*/
|
|
3115
|
-
type ContractCallSignature<A extends InputArity, TKey, TParams, TResult> = (key: A extends 'single' ? TKey : A extends 'array' ? readonly TKey[] : TKey | readonly TKey[], params: TParams) => TResult;
|
|
3116
|
-
/**
|
|
3117
|
-
* @internal The non-enumerable property an arity-tagged body ({@link point} /
|
|
3118
|
-
* {@link range}) carries its declared literal {@link InputArity} on. The factory
|
|
3119
|
-
* reads it to (1) thread the literal into the resolved {@link QueryMethodSpec}'s
|
|
3120
|
-
* `inputArity` type and (2) **verify** the declared literal equals the
|
|
3121
|
-
* runtime-derived arity ({@link deriveReadFacts}) at build time — a mismatch is a
|
|
3122
|
-
* build error, so the type-level arity cannot lie.
|
|
3123
|
-
*/
|
|
3124
|
-
declare const ARITY_TAG: "__contractArity";
|
|
3125
|
-
/**
|
|
3126
|
-
* A contract query-method body **tagged** with its declared literal
|
|
3127
|
-
* {@link InputArity} (issue #71). Structurally an {@link AuthoredMethod} (single-key
|
|
3128
|
-
* `(keys, params) => …`) carrying a phantom `A`, so `<const M>` factory inference
|
|
3129
|
-
* preserves the literal arity into the resolved {@link QueryMethodSpec} and on into
|
|
3130
|
-
* the `executeQueryMethod` call surface, where {@link ContractCallSignature} narrows
|
|
3131
|
-
* the key argument by `A` — making an array fed into a `'single'` method a `tsc`
|
|
3132
|
-
* error. The runtime value is the original body function with a non-enumerable
|
|
3133
|
-
* `ARITY_TAG`, so it still records its op exactly as an untagged body does.
|
|
3134
|
-
*
|
|
3135
|
-
* @typeParam A - The declared literal arity (`'single'` | `'either'`).
|
|
3136
|
-
* @typeParam TKey - The contract Key (single key at authoring time).
|
|
3137
|
-
* @typeParam TParams - The method's params type.
|
|
3138
|
-
* @typeParam TResult - The method's result type.
|
|
3139
|
-
*/
|
|
3140
|
-
interface ArityTaggedQueryMethod<A extends InputArity, TKey, TParams, TResult> {
|
|
3141
|
-
(keys: TKey, params: TParams): TResult;
|
|
3142
|
-
/** @internal The declared literal arity, read by the factory and build-verified. */
|
|
3143
|
-
readonly [ARITY_TAG]: A;
|
|
3144
|
-
}
|
|
3145
|
-
/**
|
|
3146
|
-
* Declare a query method as a **`point`** read — a unique-key lookup whose per-key
|
|
3147
|
-
* result is at most one item (proposal: `resolution: 'point'`). Wrap the method
|
|
3148
|
-
* body to thread the read's **literal** {@link InputArity} into the type-level
|
|
3149
|
-
* call surface so an array fed into a non-coalescible point is a `tsc` error
|
|
3150
|
-
* (issue #71, N+1 rule (a)):
|
|
3151
|
-
*
|
|
3152
|
-
* ```ts
|
|
3153
|
-
* // A base-table primary-key point — a key array coalesces to one BatchGetItem.
|
|
3154
|
-
* const GroupById = publicQueryModel<{ groupId: string }>()({
|
|
3155
|
-
* get: point((keys, params: { consistentRead?: boolean }) =>
|
|
3156
|
-
* Group.query(keys, { groupId: true, name: true }, params)),
|
|
3157
|
-
* });
|
|
3158
|
-
*
|
|
3159
|
-
* // A UNIQUE-GSI point — a per-key GSI `Query` (BatchGetItem cannot read a GSI),
|
|
3160
|
-
* // so it is NOT coalescible: declare `coalesce: false`.
|
|
3161
|
-
* const UserByEmail = publicQueryModel<{ email: string }>(['email'])({
|
|
3162
|
-
* get: point((keys) => User.query(keys, { userId: true, email: true }),
|
|
3163
|
-
* { coalesce: false }),
|
|
3164
|
-
* });
|
|
3165
|
-
* ```
|
|
3166
|
-
*
|
|
3167
|
-
* The default (`coalesce: true`) is `inputArity: 'either'` (a base-table PK point);
|
|
3168
|
-
* `{ coalesce: false }` is `inputArity: 'single'` (a unique-GSI point). The declared
|
|
3169
|
-
* arity is **verified against the build-derived arity** ({@link deriveReadFacts}):
|
|
3170
|
-
* declaring `coalesce: true` on a GSI point — or `coalesce: false` on a base-table
|
|
3171
|
-
* point — is a build error, so the type never disagrees with the resolved access
|
|
3172
|
-
* pattern. Tagging is **optional**: an untagged body keeps the wide `InputArity`
|
|
3173
|
-
* and relies on the build-time checker + runtime backstop only.
|
|
3174
|
-
*
|
|
3175
|
-
* @param body The single-key method body (`(keys, params) => Model.query(...)`).
|
|
3176
|
-
* @param options `{ coalesce }` — `true` (default) for a base-table PK point
|
|
3177
|
-
* (`'either'`), `false` for a unique-GSI point (`'single'`).
|
|
3178
|
-
*/
|
|
3179
|
-
declare function point<TKey, TParams, TResult>(body: (keys: TKey, params: TParams) => TResult, options: {
|
|
3180
|
-
readonly coalesce: false;
|
|
3181
|
-
}): ArityTaggedQueryMethod<'single', TKey, TParams, TResult>;
|
|
3182
|
-
declare function point<TKey, TParams, TResult>(body: (keys: TKey, params: TParams) => TResult, options?: {
|
|
3183
|
-
readonly coalesce?: true;
|
|
3184
|
-
}): ArityTaggedQueryMethod<'either', TKey, TParams, TResult>;
|
|
3185
|
-
/**
|
|
3186
|
-
* Declare a query method as a **`range`** read — a partition `Query` (`Model.list`)
|
|
3187
|
-
* whose per-key result is a connection (`resolution: 'range'`). A range read is
|
|
3188
|
-
* **never** coalescible (DynamoDB cannot coalesce N partition `Query`s), so its
|
|
3189
|
-
* arity is fixed to `'single'`; wrapping the body threads that literal into the
|
|
3190
|
-
* call surface so an array fed into a range method is a `tsc` error (issue #71,
|
|
3191
|
-
* N+1 rule (a)):
|
|
3192
|
-
*
|
|
3193
|
-
* ```ts
|
|
3194
|
-
* const MembersByGroup = publicQueryModel<{ groupId: string }>()({
|
|
3195
|
-
* list: range((key, params: { limit?: number }) =>
|
|
3196
|
-
* Member.list({ groupId: key.groupId }, { select: { userId: true }, ...params })),
|
|
3197
|
-
* });
|
|
3198
|
-
* ```
|
|
3199
|
-
*
|
|
3200
|
-
* The declared `'single'` arity is **verified against the build-derived arity**
|
|
3201
|
-
* ({@link deriveReadFacts}): tagging a body that resolves to a coalescible base-table
|
|
3202
|
-
* point with `range(...)` is a build error. Tagging is **optional** (see
|
|
3203
|
-
* {@link point}).
|
|
3204
|
-
*
|
|
3205
|
-
* @param body The single-key method body (`(keys, params) => Model.list(...)`).
|
|
3206
|
-
*/
|
|
3207
|
-
declare function range<TKey, TParams, TResult>(body: (keys: TKey, params: TParams) => TResult): ArityTaggedQueryMethod<'single', TKey, TParams, TResult>;
|
|
3208
|
-
/**
|
|
3209
|
-
* The resolved IR of one **query** method. Carries the formal method type at the
|
|
3210
|
-
* type level (`QueryMethod<TKey, TParams, TResult>`) and, at runtime, the resolved
|
|
3211
|
-
* internal op plus the **derived** facts (`resolution` / `inputArity`).
|
|
3212
|
-
*
|
|
3213
|
-
* @typeParam TKey - The contract Key.
|
|
3214
|
-
* @typeParam TParams - The method's params type.
|
|
3215
|
-
* @typeParam TResult - The method's result type.
|
|
3216
|
-
* @typeParam A - The method's **literal** {@link InputArity} when the author
|
|
3217
|
-
* tagged the body with {@link point} / {@link range} (the type-level half of
|
|
3218
|
-
* N+1 rule (a), threaded into the call surface so an array fed into a `'single'`
|
|
3219
|
-
* method is a `tsc` error — see {@link ContractCallSignature} and
|
|
3220
|
-
* `executeQueryMethod`). Defaults to the wide `InputArity` for an untagged body,
|
|
3221
|
-
* which then relies on the build-time checker + the runtime backstop only.
|
|
3222
|
-
*/
|
|
3223
|
-
interface QueryMethodSpec<TKey, TParams, TResult, A extends InputArity = InputArity> {
|
|
3224
|
-
/** @internal Marks this object as a query method spec. */
|
|
3225
|
-
readonly __methodKind: 'query';
|
|
3226
|
-
/** The resolved internal operation (closure discarded). */
|
|
3227
|
-
readonly op: ContractMethodOp;
|
|
3228
|
-
/** Derived: `'point'` (unique-key query) or `'range'` (partition list). */
|
|
3229
|
-
readonly resolution: Resolution;
|
|
3230
|
-
/**
|
|
3231
|
-
* Derived: `'either'` for a base-table `point`, `'single'` for a `range` or a
|
|
3232
|
-
* unique-GSI `point`. Typed as the method's **literal** arity `A` when the body
|
|
3233
|
-
* was tagged ({@link point} / {@link range}); a tagged literal is verified
|
|
3234
|
-
* against this derived value at build time (a mismatch is a build error), so the
|
|
3235
|
-
* type cannot lie about the runtime arity.
|
|
3236
|
-
*/
|
|
3237
|
-
readonly inputArity: A;
|
|
3238
|
-
/**
|
|
3239
|
-
* @internal The method's own name in its contract (`'get'`, `'summary'`, …),
|
|
3240
|
-
* stamped by the factory. An External Query composition (#63) references this
|
|
3241
|
-
* spec object; the serializer recovers the referenced method name from here and
|
|
3242
|
-
* the referenced contract name by identity ({@link contractOfMethodSpec}).
|
|
3243
|
-
*/
|
|
3244
|
-
readonly __methodName?: string;
|
|
3245
|
-
/** @internal Phantom carrier retaining the formal `QueryMethod` type. */
|
|
3246
|
-
readonly __signature?: QueryMethod<TKey, TParams, TResult>;
|
|
3247
|
-
}
|
|
3248
|
-
/**
|
|
3249
|
-
* The resolved IR of one **command** method. Symmetric to {@link QueryMethodSpec},
|
|
3250
|
-
* with an explicit declared result (`void` | a `Result` | the updated entity).
|
|
3251
|
-
*
|
|
3252
|
-
* @typeParam TKey - The contract Key.
|
|
3253
|
-
* @typeParam TParams - The method's params type.
|
|
3254
|
-
* @typeParam TResult - The method's declared result type.
|
|
3255
|
-
*/
|
|
3256
|
-
interface CommandMethodSpec<TKey, TParams, TResult> {
|
|
3257
|
-
/** @internal Marks this object as a command method spec. */
|
|
3258
|
-
readonly __methodKind: 'command';
|
|
3259
|
-
/** The resolved internal write operation (closure discarded). */
|
|
3260
|
-
readonly op: ContractMethodOp;
|
|
3261
|
-
/**
|
|
3262
|
-
* Derived input arity: a single key maps to one write op; an array maps to a
|
|
3263
|
-
* batched write (a transaction / `BatchWriteItem`). Writes accept `'either'`.
|
|
3264
|
-
*/
|
|
3265
|
-
readonly inputArity: InputArity;
|
|
3266
|
-
/**
|
|
3267
|
-
* The category of the declared result type (`void` | `result` | `entity`),
|
|
3268
|
-
* part of the contract (it surfaces in OpenAPI and every binding). Derived from
|
|
3269
|
-
* the internal op when not explicitly annotated: an `update` returning the
|
|
3270
|
-
* entity → `'entity'`; a `delete` → `'void'`.
|
|
3271
|
-
*/
|
|
3272
|
-
readonly result: CommandResultKind;
|
|
3273
|
-
/**
|
|
3274
|
-
* The declared batched-write mode (issue #64): how a **key array** resolves —
|
|
3275
|
-
* `'transact'` (atomic `TransactWriteItems`, ≤25, condition-capable) or
|
|
3276
|
-
* `'batchWrite'` (non-atomic `BatchWriteItem`, no conditions, chunked). Absent
|
|
3277
|
-
* when the author declared no batched form — the method then accepts only a
|
|
3278
|
-
* single key at execution; an array is rejected with a clear error. The
|
|
3279
|
-
* single-key form is unaffected by this declaration.
|
|
3280
|
-
*/
|
|
3281
|
-
readonly batch?: CommandBatchMode;
|
|
3282
|
-
/**
|
|
3283
|
-
* The return projection of a {@link mutation}-derived command method (issue
|
|
3284
|
-
* #83): a boolean field map applied to the written entity as a **consistent
|
|
3285
|
-
* read-back** projection after the write commits (the proposal's "return =
|
|
3286
|
-
* read projection"). Present only on a `.plan(mutation)` method; a hand-written
|
|
3287
|
-
* #64 method (the closure form) omits it. JSON-safe (only boolean leaves), so
|
|
3288
|
-
* it serializes verbatim into {@link CommandContractMethodSpec.returnSelection}
|
|
3289
|
-
* and both runtimes apply the **same** projection.
|
|
3290
|
-
*/
|
|
3291
|
-
readonly returnSelection?: Readonly<Record<string, boolean>>;
|
|
3292
|
-
/**
|
|
3293
|
-
* The composed write ops of a **multi-fragment** `mutation`-derived method
|
|
3294
|
-
* (issue #90): when a mutation declares **2+ fragments**, each fragment compiles
|
|
3295
|
-
* to one write {@link ContractMethodOp} and the whole set executes as **one
|
|
3296
|
-
* atomic `TransactWriteItems`** (NOT sequential writes). Present only for N≥2; a
|
|
3297
|
-
* single-fragment method carries only {@link op} (no regression — #83 behavior).
|
|
3298
|
-
* The merge (same-item conflict / cross-fragment dep / ≤25 enforcement) is done
|
|
3299
|
-
* at build time by `compileMutationPlan`; `op` is set to the **primary** fragment
|
|
3300
|
-
* (fragment 0 — the entity the method is keyed by and read back from) so the
|
|
3301
|
-
* Key / read-back machinery is unchanged.
|
|
3302
|
-
*/
|
|
3303
|
-
readonly ops?: readonly ContractMethodOp[];
|
|
3304
|
-
/**
|
|
3305
|
-
* The referential-integrity assertions derived from the mutation's `requires`
|
|
3306
|
-
* effects (issue #84): one {@link DerivedConditionCheck} per
|
|
3307
|
-
* `w.exists(...)`, flattened across every fragment. Present (non-empty) only when
|
|
3308
|
-
* a fragment's lifecycle declares `requires`; absent otherwise (no regression —
|
|
3309
|
-
* the method then carries only its base op(s)). Each becomes a read-only
|
|
3310
|
-
* `ConditionCheck` (`attribute_exists`) item in the method's atomic
|
|
3311
|
-
* `TransactWriteItems`, and their presence **promotes** an otherwise single-op
|
|
3312
|
-
* method to a transaction: the serializer emits a {@link
|
|
3313
|
-
* import('../spec/types.js').TransactionItemSpec}-based `TransactionSpec` and
|
|
3314
|
-
* `single: { mode: 'transaction' }`, and the in-process runtime composes the base
|
|
3315
|
-
* write(s) + the ConditionCheck(s) into one transaction so an absent referent
|
|
3316
|
-
* rolls the WHOLE write back.
|
|
3317
|
-
*/
|
|
3318
|
-
readonly conditionChecks?: readonly DerivedConditionCheck[];
|
|
3319
|
-
/**
|
|
3320
|
-
* The adjacency edge writes derived from the mutation's `edges` effects (issue
|
|
3321
|
-
* #85), flattened across every fragment in declaration order. Present (non-empty)
|
|
3322
|
-
* only when a fragment's lifecycle declares `edges`; absent otherwise (no
|
|
3323
|
-
* regression). Each {@link DerivedEdgeWrite} contributes its adjacency `Put` /
|
|
3324
|
-
* `Delete` item(s) to the method's atomic `TransactWriteItems`, and their presence
|
|
3325
|
-
* (like {@link conditionChecks}) **promotes** an otherwise single-op method to a
|
|
3326
|
-
* transaction so the edge moves / removes atomically with the entity write.
|
|
3327
|
-
*/
|
|
3328
|
-
readonly edgeWrites?: readonly DerivedEdgeWrite[];
|
|
3329
|
-
/**
|
|
3330
|
-
* The derived (cascading) updates from the mutation's `derive` effects (issue
|
|
3331
|
-
* #85), flattened across every fragment in declaration order: each an atomic `ADD`
|
|
3332
|
-
* (`UpdateItem`) on a target counter (e.g. `User.postCount += 1`). Present
|
|
3333
|
-
* (non-empty) only when a fragment's lifecycle declares `derive`; absent otherwise
|
|
3334
|
-
* (no regression). Each contributes an `UpdateItem` (`ADD`) to the method's atomic
|
|
3335
|
-
* `TransactWriteItems` and **promotes** the method to a transaction.
|
|
3336
|
-
*/
|
|
3337
|
-
readonly derivedUpdates?: readonly DerivedUpdate[];
|
|
3338
|
-
/**
|
|
3339
|
-
* The uniqueness guards derived from the mutation's `unique` effects (issue #86),
|
|
3340
|
-
* flattened across every fragment in declaration order. Present (non-empty) only
|
|
3341
|
-
* when a fragment's lifecycle declares `unique`; absent otherwise (no regression).
|
|
3342
|
-
* Each {@link DerivedUniqueGuard} contributes its marker-row `Put` / `Delete`
|
|
3343
|
-
* item(s) to the method's atomic `TransactWriteItems`, and their presence (like
|
|
3344
|
-
* {@link conditionChecks}) **promotes** an otherwise single-op method to a
|
|
3345
|
-
* transaction so a duplicate scoped value (the guard `Put`'s `attribute_not_exists`
|
|
3346
|
-
* failing) rolls the WHOLE write back — the entity is never created.
|
|
3347
|
-
*/
|
|
3348
|
-
readonly uniqueGuards?: readonly DerivedUniqueGuard[];
|
|
3349
|
-
/**
|
|
3350
|
-
* The outbox events derived from the mutation's `emits` effects (issue #87),
|
|
3351
|
-
* flattened across every fragment in declaration order. Present (non-empty) only
|
|
3352
|
-
* when a fragment's lifecycle declares `emits`; absent otherwise (no regression).
|
|
3353
|
-
* Each {@link DerivedOutboxEvent} contributes its transactional-outbox `Put` to the
|
|
3354
|
-
* method's atomic `TransactWriteItems` — recording the event ATOMICALLY with the
|
|
3355
|
-
* entity write (event and state cannot diverge) — and their presence (like
|
|
3356
|
-
* {@link conditionChecks}) **promotes** an otherwise single-op method to a
|
|
3357
|
-
* transaction. The outbox row is drainable via `src/cdc/`; delivery is out of scope.
|
|
3358
|
-
*/
|
|
3359
|
-
readonly outboxEvents?: readonly DerivedOutboxEvent[];
|
|
3360
|
-
/**
|
|
3361
|
-
* The client-token idempotency guard derived from the mutation's `idempotency`
|
|
3362
|
-
* effect (issue #87). Present only when a fragment's lifecycle declares
|
|
3363
|
-
* `idempotency`; absent otherwise (no regression). The {@link DerivedIdempotencyGuard}
|
|
3364
|
-
* contributes its `attribute_not_exists` guard `Put` to the method's atomic
|
|
3365
|
-
* `TransactWriteItems`, and its presence (like {@link conditionChecks}) **promotes**
|
|
3366
|
-
* an otherwise single-op method to a transaction so a same-token re-execution fails
|
|
3367
|
-
* the guard and rolls the WHOLE write back — no effect is double-applied.
|
|
3368
|
-
*/
|
|
3369
|
-
readonly idempotencyGuard?: DerivedIdempotencyGuard;
|
|
3370
|
-
/** @internal Phantom carrier retaining the formal `CommandMethod` type. */
|
|
3371
|
-
readonly __signature?: CommandMethod<TKey, TParams, TResult>;
|
|
3372
|
-
}
|
|
3373
|
-
/**
|
|
3374
|
-
* A resolved **QueryModel** — a keyed public read contract holding one or more
|
|
3375
|
-
* named query methods. The Key (`TKey`) is the access pattern; each entry of
|
|
3376
|
-
* `methods` is a use case. Carries the contract `kind` and the resolved per-method
|
|
3377
|
-
* IR; the source method closures are discarded.
|
|
3378
|
-
*
|
|
3379
|
-
* @typeParam TKey - The contract Key (the access pattern / join key).
|
|
3380
|
-
* @typeParam M - The method map (name → resolved {@link QueryMethodSpec}).
|
|
3381
|
-
*/
|
|
3382
|
-
interface QueryModelContract<TKey, M extends Record<string, QueryMethodSpec<TKey, unknown, unknown>>> {
|
|
3383
|
-
/** @internal Marks this object as a contract IR node. */
|
|
3384
|
-
readonly __isContract: true;
|
|
3385
|
-
/** Discriminant: a read contract. */
|
|
3386
|
-
readonly kind: 'query';
|
|
3387
|
-
/** The resolved, named query methods (use cases over the same Key). */
|
|
3388
|
-
readonly methods: M;
|
|
3389
|
-
/** @internal Phantom carrier retaining the contract Key type `TKey`. */
|
|
3390
|
-
readonly __keyType?: (value: TKey) => void;
|
|
3391
|
-
}
|
|
3392
|
-
/**
|
|
3393
|
-
* A resolved **CommandModel** — the write counterpart of {@link QueryModelContract},
|
|
3394
|
-
* completely symmetric: keyed, holding one or more named write-use-case methods.
|
|
3395
|
-
*
|
|
3396
|
-
* @typeParam TKey - The contract Key (the target identity).
|
|
3397
|
-
* @typeParam M - The method map (name → resolved {@link CommandMethodSpec}).
|
|
3398
|
-
*/
|
|
3399
|
-
interface CommandModelContract<TKey, M extends Record<string, CommandMethodSpec<TKey, any, any>>> {
|
|
3400
|
-
/** @internal Marks this object as a contract IR node. */
|
|
3401
|
-
readonly __isContract: true;
|
|
3402
|
-
/** Discriminant: a write contract. */
|
|
3403
|
-
readonly kind: 'command';
|
|
3404
|
-
/** The resolved, named command methods (write use cases over the same Key). */
|
|
3405
|
-
readonly methods: M;
|
|
3406
|
-
/** @internal Phantom carrier retaining the contract Key type `TKey`. */
|
|
3407
|
-
readonly __keyType?: (value: TKey) => void;
|
|
3408
|
-
}
|
|
3409
|
-
/** Runtime guard: is `value` a {@link QueryModelContract}? */
|
|
3410
|
-
declare function isQueryModelContract(value: unknown): value is QueryModelContract<unknown, Record<string, QueryMethodSpec<unknown, unknown, unknown>>>;
|
|
3411
|
-
/** Runtime guard: is `value` a {@link CommandModelContract}? */
|
|
3412
|
-
declare function isCommandModelContract(value: unknown): value is CommandModelContract<unknown, Record<string, CommandMethodSpec<unknown, unknown, unknown>>>;
|
|
3413
|
-
/**
|
|
3414
|
-
* Recovers the model-class constructor type `C` from a `ModelStatic<T, C>`
|
|
3415
|
-
* (Model-derived key form). Mirrors the helper used by the `define*` entry
|
|
3416
|
-
* points; the contract Key is then `PrimaryKeyOf<ClassOf<M>>`.
|
|
3417
|
-
*/
|
|
3418
|
-
type ClassOf$1<M> = M extends ModelStatic<infer _T, infer C> ? C : unknown;
|
|
3419
|
-
/**
|
|
3420
|
-
* The **authoring** signature of a contract method body. Deliberately distinct
|
|
3421
|
-
* from the formal, array-capable {@link QueryMethod} / {@link CommandMethod}: the
|
|
3422
|
-
* author writes a **single-key** body (`(keys: TKey, params) => …`) so that
|
|
3423
|
-
* `keys` (and `keys.<field>`) is assignable into the underlying model op's key
|
|
3424
|
-
* position, while the array capability (`get(keys[])`) is **derived** by the
|
|
3425
|
-
* contract and exposed on the resolved spec's `__signature`. This matches the
|
|
3426
|
-
* proposal: a contract method body "resolves to a declarative operation"; the
|
|
3427
|
-
* author does not hand-write the batched overload.
|
|
3428
|
-
*
|
|
3429
|
-
* @typeParam TKey - The contract Key (single key at authoring time).
|
|
3430
|
-
*/
|
|
3431
|
-
type AuthoredMethod<TKey> = (keys: TKey, params: never) => unknown;
|
|
3432
|
-
/**
|
|
3433
|
-
* The query-method-map type accepted by a {@link publicQueryModel} factory. Each
|
|
3434
|
-
* entry is an {@link AuthoredMethod} (a bare body) **or** an
|
|
3435
|
-
* {@link ArityTaggedQueryMethod} (a {@link point} / {@link range}-tagged body) whose
|
|
3436
|
-
* key type is fixed to the contract `TKey`; `TParams` / `TResult` (and, for a
|
|
3437
|
-
* tagged body, the literal arity `A`) are inferred per method.
|
|
3438
|
-
*/
|
|
3439
|
-
type QueryMethodMap<TKey> = Record<string, AuthoredMethod<TKey>>;
|
|
3440
|
-
/**
|
|
3441
|
-
* The command-method-map type accepted by a {@link publicCommandModel} factory.
|
|
3442
|
-
* Each entry is either a hand-written #64 {@link AuthoredMethod} closure
|
|
3443
|
-
* (`(keys, params) => Model.put|update|delete(...)`) **or** a #83
|
|
3444
|
-
* {@link PlannedCommandMethod} (`command({ input, select }).plan(mutation)`). The
|
|
3445
|
-
* two forms coexist in one contract — the brand routes each at build time.
|
|
3446
|
-
*/
|
|
3447
|
-
type CommandMethodMap<TKey> = Record<string, AuthoredMethod<TKey> | PlannedCommandMethod>;
|
|
3448
|
-
/**
|
|
3449
|
-
* The literal {@link InputArity} a (possibly {@link point} / {@link range}-tagged)
|
|
3450
|
-
* authored body declares, or the wide `InputArity` for a bare (untagged) body. An
|
|
3451
|
-
* `ArityTaggedQueryMethod` carries its literal `A` on the {@link ARITY_TAG} brand;
|
|
3452
|
-
* a bare body has none, so the resolved spec keeps the wide arity (build-checker +
|
|
3453
|
-
* runtime backstop only).
|
|
3454
|
-
*/
|
|
3455
|
-
type DeclaredArity<F> = F extends {
|
|
3456
|
-
readonly [ARITY_TAG]: infer A extends InputArity;
|
|
3457
|
-
} ? A : InputArity;
|
|
3458
|
-
/**
|
|
3459
|
-
* Maps a supplied query-method map `M` to its resolved {@link QueryMethodSpec}
|
|
3460
|
-
* map. The resolved spec carries the method's authored `TParams` / `TResult` and
|
|
3461
|
-
* its **literal** {@link InputArity} when the body was tagged ({@link point} /
|
|
3462
|
-
* {@link range}) — the literal threads on into the `executeQueryMethod` call
|
|
3463
|
-
* surface, where an array fed into a `'single'` method is a `tsc` error (issue
|
|
3464
|
-
* #71, N+1 rule (a)).
|
|
3465
|
-
*/
|
|
3466
|
-
type ResolvedQueryMethods<TKey, M extends QueryMethodMap<TKey>> = {
|
|
3467
|
-
readonly [K in keyof M]: M[K] extends (keys: TKey, params: infer P) => infer R ? QueryMethodSpec<TKey, P, R, DeclaredArity<M[K]>> : never;
|
|
3468
|
-
};
|
|
3469
|
-
/**
|
|
3470
|
-
* The TypeScript value type an input param shape `I` represents: each field
|
|
3471
|
-
* carries the value its placeholder stands in for (e.g. `param.string()` →
|
|
3472
|
-
* `string`). The public Command IF method's `TParams` for a #83 planned method.
|
|
3473
|
-
*/
|
|
3474
|
-
type InputParamsOf<I extends CommandInputShape> = {
|
|
3475
|
-
readonly [K in keyof I]: I[K] extends Param<infer V> ? V : never;
|
|
3476
|
-
};
|
|
3477
|
-
/**
|
|
3478
|
-
* The result type a #83 planned method returns: the projected read-back item when
|
|
3479
|
-
* a `select` is declared (a partial record over the projected fields), or `void`
|
|
3480
|
-
* for a fire-and-forget command (no `select`). The concrete field set is the
|
|
3481
|
-
* `true`-flagged keys of the declared `select`.
|
|
3482
|
-
*/
|
|
3483
|
-
type PlannedResultOf<S extends CommandSelectShape | undefined> = S extends CommandSelectShape ? Readonly<Record<Extract<keyof S, string>, unknown>> | null : void;
|
|
3484
|
-
/**
|
|
3485
|
-
* Maps a supplied command-method map to its resolved {@link CommandMethodSpec}
|
|
3486
|
-
* map. A #83 {@link PlannedCommandMethod} resolves to a spec whose `TParams` is
|
|
3487
|
-
* its input param shape and whose `TResult` is its read-back projection (or
|
|
3488
|
-
* `void`); a hand-written #64 closure resolves by its `(keys, params) => R`
|
|
3489
|
-
* signature, exactly as before.
|
|
3490
|
-
*/
|
|
3491
|
-
type ResolvedCommandMethods<TKey, M extends CommandMethodMap<TKey>> = {
|
|
3492
|
-
readonly [K in keyof M]: M[K] extends PlannedCommandMethod<infer I, infer S> ? CommandMethodSpec<TKey, InputParamsOf<I>, PlannedResultOf<S>> : M[K] extends (keys: TKey, params: infer P) => infer R ? CommandMethodSpec<TKey, P, R> : never;
|
|
3493
|
-
};
|
|
3494
|
-
/**
|
|
3495
|
-
* The explicit Key-field list a factory accepts (issue #71): the field names that
|
|
3496
|
-
* compose the contract Key, constrained to `keyof TKey`. Supplied to the explicit
|
|
3497
|
-
* form (`publicQueryModel<EmailKey>(['email'])`) so a **whole-`keys`** point
|
|
3498
|
-
* contract whose Key is a GSI (not the model's primary key) is resolved to the
|
|
3499
|
-
* right access pattern — without it, the type-erased whole-`keys` op carries no
|
|
3500
|
-
* field names and the serializer can only default to the primary key.
|
|
3501
|
-
*/
|
|
3502
|
-
type KeyFieldList<TKey> = readonly (keyof TKey & string)[];
|
|
3503
|
-
/**
|
|
3504
|
-
* Create a **QueryModel** factory bound to an explicit **Query key** (`TKey`) —
|
|
3505
|
-
* the form used for a GSI / alternate access pattern whose key is not the model's
|
|
3506
|
-
* primary key:
|
|
3507
|
-
*
|
|
3508
|
-
* ```ts
|
|
3509
|
-
* type CategoryKey = { categoryId: string };
|
|
3510
|
-
* const ArticleByCategory = publicQueryModel<CategoryKey>()({
|
|
3511
|
-
* list: (key, params) => Article.list({ categoryId: key.categoryId }, { select: { … } }),
|
|
3512
|
-
* });
|
|
3513
|
-
* ```
|
|
3514
|
-
*
|
|
3515
|
-
* **Explicit Key-field capture (issue #71).** A **whole-`keys`** point contract
|
|
3516
|
-
* whose Key is a **GSI** (e.g. `EmailKey = { email: string }` resolved through a
|
|
3517
|
-
* unique `email` index) must declare its Key fields so the build can resolve the
|
|
3518
|
-
* right access pattern — passing `keys` whole into `Model.query(keys, …)` erases
|
|
3519
|
-
* the field names (only the phantom `TKey` knows them). Supply the field list:
|
|
3520
|
-
*
|
|
3521
|
-
* ```ts
|
|
3522
|
-
* type EmailKey = { email: string };
|
|
3523
|
-
* const UserByEmail = publicQueryModel<EmailKey>(['email'])({
|
|
3524
|
-
* get: (keys, params) => User.query(keys, { userId: true, email: true }),
|
|
3525
|
-
* });
|
|
3526
|
-
* ```
|
|
3527
|
-
*
|
|
3528
|
-
* The captured fields are fed through {@link resolveKey}: a Key that resolves to a
|
|
3529
|
-
* **unique GSI** emits the GSI `indexName` + GSI key template and is a per-key
|
|
3530
|
-
* `Query` (`inputArity: 'single'` — not batch-coalescible), while a Key that
|
|
3531
|
-
* resolves to the **base-table primary key** keeps `inputArity: 'either'` (a key
|
|
3532
|
-
* array → one `BatchGetItem`). The list is **optional** for backward
|
|
3533
|
-
* compatibility: a whole-`keys` contract with no list defaults to the model's
|
|
3534
|
-
* primary key, exactly as before #71.
|
|
3535
|
-
*
|
|
3536
|
-
* The returned factory takes a record of named query methods (the use cases) and
|
|
3537
|
-
* resolves each body to its declarative internal op, deriving `resolution` /
|
|
3538
|
-
* `inputArity`. The method closures are discarded; only the resolved IR is kept.
|
|
3539
|
-
*
|
|
3540
|
-
* @typeParam TKey - The explicit contract Key (the access pattern / join key).
|
|
3541
|
-
* @param keyFields - Optional explicit Key-field names (issue #71); when present
|
|
3542
|
-
* they are captured into the IR and resolved to the access pattern.
|
|
3543
|
-
*/
|
|
3544
|
-
declare function publicQueryModel<TKey>(keyFields?: KeyFieldList<TKey>): <const M extends QueryMethodMap<TKey>>(methods: M) => QueryModelContract<TKey, ResolvedQueryMethods<TKey, M>>;
|
|
3545
|
-
/**
|
|
3546
|
-
* Create a **QueryModel** factory whose Key is **derived** from a Model's primary
|
|
3547
|
-
* key (no explicit key type needed):
|
|
3548
|
-
*
|
|
3549
|
-
* ```ts
|
|
3550
|
-
* const ArticleById = publicQueryModel(ArticleModel)({
|
|
3551
|
-
* get: (keys, params) => Article.query(keys, { articleId: true, title: true }, params),
|
|
3552
|
-
* });
|
|
3553
|
-
* ```
|
|
3554
|
-
*
|
|
3555
|
-
* An explicit Key-field list (issue #71) is **optional** here: the Key continues
|
|
3556
|
-
* to derive from the model's primary key when omitted.
|
|
3557
|
-
*
|
|
3558
|
-
* @param model - The model whose primary key becomes the contract Key.
|
|
3559
|
-
* @param keyFields - Optional explicit Key-field names; defaults to the model's
|
|
3560
|
-
* primary-key input fields.
|
|
3561
|
-
*/
|
|
3562
|
-
declare function publicQueryModel<M extends ModelStatic<DDBModel>>(model: M, keyFields?: KeyFieldList<PrimaryKeyOf<ClassOf$1<M>>>): <const Methods extends QueryMethodMap<PrimaryKeyOf<ClassOf$1<M>>>>(methods: Methods) => QueryModelContract<PrimaryKeyOf<ClassOf$1<M>>, ResolvedQueryMethods<PrimaryKeyOf<ClassOf$1<M>>, Methods>>;
|
|
3563
|
-
/**
|
|
3564
|
-
* Create a **CommandModel** factory bound to an explicit **Command key** (`TKey`).
|
|
3565
|
-
* Symmetric to {@link publicQueryModel}; each method resolves to a write op
|
|
3566
|
-
* (`put` / `update` / `delete`) and carries an explicit, contract-level result
|
|
3567
|
-
* type (`void` | a `Result` | the updated entity):
|
|
3568
|
-
*
|
|
3569
|
-
* ```ts
|
|
3570
|
-
* type UserKey = { userId: string };
|
|
3571
|
-
* const UserCommands = publicCommandModel<UserKey>()({
|
|
3572
|
-
* disable: (keys, params: { reason: string }): UpdatedUser =>
|
|
3573
|
-
* User.update(keys, { status: 'disabled', disableReason: params.reason }),
|
|
3574
|
-
* changeRole: (keys, params: { role: Role }): void =>
|
|
3575
|
-
* User.update(keys, { role: params.role }),
|
|
3576
|
-
* });
|
|
3577
|
-
* ```
|
|
3578
|
-
*
|
|
3579
|
-
* An explicit Key-field list (issue #71) is **optional**; symmetric to
|
|
3580
|
-
* {@link publicQueryModel}, it captures the whole-`keys` Key fields into the IR
|
|
3581
|
-
* so the serializer emits the contract Key explicitly. A command always writes to
|
|
3582
|
-
* the base table by its key, so the list does not change a command's
|
|
3583
|
-
* `inputArity` (always `'either'` — a single key or a batched write).
|
|
3584
|
-
*
|
|
3585
|
-
* @typeParam TKey - The explicit contract Key (the target identity).
|
|
3586
|
-
* @param keyFields - Optional explicit Key-field names (issue #71).
|
|
3587
|
-
*/
|
|
3588
|
-
declare function publicCommandModel<TKey>(keyFields?: KeyFieldList<TKey>): <const M extends CommandMethodMap<TKey>>(methods: M) => CommandModelContract<TKey, ResolvedCommandMethods<TKey, M>>;
|
|
3589
|
-
/**
|
|
3590
|
-
* Create a **CommandModel** factory whose Key is **derived** from a Model's
|
|
3591
|
-
* primary key.
|
|
3592
|
-
*
|
|
3593
|
-
* @param model - The model whose primary key becomes the contract Key.
|
|
3594
|
-
* @param keyFields - Optional explicit Key-field names; defaults to the model's
|
|
3595
|
-
* primary-key input fields.
|
|
3596
|
-
*/
|
|
3597
|
-
declare function publicCommandModel<M extends ModelStatic<DDBModel>>(model: M, keyFields?: KeyFieldList<PrimaryKeyOf<ClassOf$1<M>>>): <const Methods extends CommandMethodMap<PrimaryKeyOf<ClassOf$1<M>>>>(methods: Methods) => CommandModelContract<PrimaryKeyOf<ClassOf$1<M>>, ResolvedCommandMethods<PrimaryKeyOf<ClassOf$1<M>>, Methods>>;
|
|
3598
|
-
declare const PLANNED_COMMAND_BRAND: unique symbol;
|
|
3599
|
-
/**
|
|
3600
|
-
* The input param shape a {@link command} declares: a record of named
|
|
3601
|
-
* {@link Param} placeholders (`param.string()` / `param.number()` /
|
|
3602
|
-
* `param.literal(...)`). These are the **external** params of the public Command
|
|
3603
|
-
* IF — the method's caller-supplied input. The mutation fragment binds them by
|
|
3604
|
-
* name into the written entity (`$.<field>`), so each input field name must match
|
|
3605
|
-
* a fragment `$.field` reference / a model field the fragment writes.
|
|
3606
|
-
*/
|
|
3607
|
-
type CommandInputShape = Readonly<Record<string, Param<unknown>>>;
|
|
3608
|
-
/**
|
|
3609
|
-
* The return projection a {@link command} declares (proposal §3: "return = read
|
|
3610
|
-
* projection"): a JSON-safe boolean field map applied to the written entity as a
|
|
3611
|
-
* **consistent read-back** after the write commits. Omit for a fire-and-forget
|
|
3612
|
-
* command (no projected item is returned).
|
|
3613
|
-
*/
|
|
3614
|
-
type CommandSelectShape = Readonly<Record<string, boolean>>;
|
|
3615
|
-
/**
|
|
3616
|
-
* A **planned command method** (#83): the branded result of
|
|
3617
|
-
* `command({ input, select }).plan(mutation)`. It carries the captured fragment
|
|
3618
|
-
* IR (the {@link CommandPlan}), the public input param shape, and the return
|
|
3619
|
-
* `select`. {@link buildCommandContract} detects the brand and routes it to the
|
|
3620
|
-
* single-fragment mutation compiler (NOT through #64's closure path).
|
|
3621
|
-
*
|
|
3622
|
-
* The external surface is **params in / result out only** — the internal mutation
|
|
3623
|
-
* document is never exposed.
|
|
3624
|
-
*
|
|
3625
|
-
* @typeParam TInput - The input param shape (`{ field: Param<…> }`).
|
|
3626
|
-
* @typeParam TSelect - The return projection (`{ field: boolean }`), or `undefined`.
|
|
3627
|
-
*/
|
|
3628
|
-
interface PlannedCommandMethod<TInput extends CommandInputShape = CommandInputShape, TSelect extends CommandSelectShape | undefined = CommandSelectShape | undefined> {
|
|
3629
|
-
readonly [PLANNED_COMMAND_BRAND]: true;
|
|
3630
|
-
/** The captured mutation IR (the fragment list). */
|
|
3631
|
-
readonly plan: CommandPlan;
|
|
3632
|
-
/** The public input param shape. */
|
|
3633
|
-
readonly input: TInput;
|
|
3634
|
-
/** The return projection (consistent read-back), or `undefined`. */
|
|
3635
|
-
readonly select: TSelect;
|
|
3636
|
-
}
|
|
3637
|
-
/** Runtime guard: is `value` a {@link PlannedCommandMethod} (#83)? */
|
|
3638
|
-
declare function isPlannedCommandMethod(value: unknown): value is PlannedCommandMethod;
|
|
3639
|
-
/**
|
|
3640
|
-
* The builder returned by {@link command}: `.plan(mutation)` finalizes the public
|
|
3641
|
-
* Command IF method by attaching the internal {@link CommandPlan} that implements
|
|
3642
|
-
* it, yielding a branded {@link PlannedCommandMethod}.
|
|
3643
|
-
*/
|
|
3644
|
-
interface CommandBuilder<TInput extends CommandInputShape, TSelect extends CommandSelectShape | undefined> {
|
|
3645
|
-
/**
|
|
3646
|
-
* Attach the internal mutation that **implements** this Command IF method. The
|
|
3647
|
-
* mutation is a {@link CommandPlan} (a `mutation(...)` / `definePlan(...)`
|
|
3648
|
-
* result); the public surface stays params (the `input`) + result (the `select`
|
|
3649
|
-
* read-back projection) only.
|
|
3650
|
-
*
|
|
3651
|
-
* @param plan The internal {@link CommandPlan} that implements this method.
|
|
3652
|
-
*/
|
|
3653
|
-
plan(plan: CommandPlan): PlannedCommandMethod<TInput, TSelect>;
|
|
3654
|
-
}
|
|
3655
|
-
/**
|
|
3656
|
-
* Declare a public **Command IF** method (#83; proposal §3) — the external,
|
|
3657
|
-
* fixed surface other services depend on: **params in** (`input`) and **result
|
|
3658
|
-
* out** (`select`, the post-write read-back projection). The method is
|
|
3659
|
-
* *implemented by* an internal mutation, attached via {@link CommandBuilder.plan}:
|
|
3660
|
-
*
|
|
3661
|
-
* ```ts
|
|
3662
|
-
* export const PostCommands = publicCommandModel<PostKey>()({
|
|
3663
|
-
* create: command({
|
|
3664
|
-
* input: { userId: param.string(), title: param.string(), body: param.string() },
|
|
3665
|
-
* select: { postId: true, title: true }, // return = read-back projection
|
|
3666
|
-
* }).plan(CreatePost), // implemented by the internal mutation
|
|
3667
|
-
* });
|
|
3668
|
-
* ```
|
|
3669
|
-
*
|
|
3670
|
-
* The mutation document is **never** exposed; only the `input` params and the
|
|
3671
|
-
* `select` result cross the boundary.
|
|
3672
|
-
*
|
|
3673
|
-
* @param spec `{ input, select? }` — the public input param shape and the optional
|
|
3674
|
-
* return projection (consistent read-back).
|
|
3675
|
-
*/
|
|
3676
|
-
declare function command<const TInput extends CommandInputShape, const TSelect extends CommandSelectShape | undefined = undefined>(spec: {
|
|
3677
|
-
input: TInput;
|
|
3678
|
-
select?: TSelect;
|
|
3679
|
-
}): CommandBuilder<TInput, TSelect>;
|
|
3680
|
-
|
|
3681
|
-
/**
|
|
3682
|
-
* Single-service contract **Runtime** — execution of CQRS query-contract methods
|
|
3683
|
-
* (issue #62, Epic #57; spec `docs/cqrs-contract.md`, "Query Method
|
|
3684
|
-
* — Common Interface" + "Cardinality matrix"). READS only; command/write
|
|
3685
|
-
* execution is #64 and single-contract composition / External Query is #63 — both
|
|
3686
|
-
* out of scope here.
|
|
3687
|
-
*
|
|
3688
|
-
* The Runtime is the **execution engine**: given a resolved query contract (a
|
|
3689
|
-
* {@link QueryModelContract} from #58, carrying per-method `resolution` /
|
|
3690
|
-
* `inputArity` and a resolved {@link ContractMethodOp}) it executes one method
|
|
3691
|
-
* for a single key **or** an array of keys and returns the correct result shape
|
|
3692
|
-
* from the proposal's cardinality matrix:
|
|
3693
|
-
*
|
|
3694
|
-
* | Input | `resolution` | Result shape | Notation |
|
|
3695
|
-
* | -------------- | ------------ | ------------------------------------- | ---------------- |
|
|
3696
|
-
* | `key` (single) | `point` | `Item \| null` | `key1 : result1` |
|
|
3697
|
-
* | `key` (single) | `range` | `Connection` (`{ items, cursor }`) | `key1 : resultM` |
|
|
3698
|
-
* | `keys[]` (N) | `point` | keyed `Map<keyId, Item \| null>` | `keyN : resultN` |
|
|
3699
|
-
* | `keys[]` (N) | `range` | **unreachable for a single contract** | `keyN : resultN×M` |
|
|
3700
|
-
*
|
|
3701
|
-
* ## On `keyN : resultN×M` (range + array) — unreachable here, by design
|
|
3702
|
-
*
|
|
3703
|
-
* The fourth matrix cell — a keyed map of connections — is **not reachable for a
|
|
3704
|
-
* single contract** at the direct-call surface. The proposal's N+1 Safety rule
|
|
3705
|
-
* fixes a `range` method's `inputArity` to `'single'` ("A `range` method is
|
|
3706
|
-
* necessarily `'single'`"; "feeding an array of keys into a range method … is an
|
|
3707
|
-
* N+1 fan-out and is rejected at build time, with no opt-in escape hatch"). #58's
|
|
3708
|
-
* {@link inputArityOf} derives exactly that, and #60's N+1 checker rejects any
|
|
3709
|
-
* contract that violates it before it reaches the SSoT. So a range method only
|
|
3710
|
-
* ever accepts one key, and this runtime **rejects** an array fed into a `range`
|
|
3711
|
-
* method ({@link executeQueryMethod}) rather than fanning out. `keyN : resultN×M`
|
|
3712
|
-
* arises only under cross-contract composition (a `point` parent referencing a
|
|
3713
|
-
* child whose per-key result is a connection) — that is #63's External Query
|
|
3714
|
-
* territory, explicitly out of scope for #62. We therefore implement the **three
|
|
3715
|
-
* reachable shapes** and guard the fourth with a clear error, rather than
|
|
3716
|
-
* silently fanning out a forbidden N+1.
|
|
3717
|
-
*
|
|
3718
|
-
* ## Reuse, not reinvention
|
|
3719
|
-
*
|
|
3720
|
-
* Execution delegates to the proven machinery:
|
|
3721
|
-
*
|
|
3722
|
-
* - `point` single → {@link executeQuery} (GetItem / unique Query).
|
|
3723
|
-
* - `point` array → {@link executeKeyedBatchGet} below, which reuses the
|
|
3724
|
-
* `batch-retry` primitives (chunk ≤100, dedup, UnprocessedKeys retry) and
|
|
3725
|
-
* builds the **keyed** map (BatchGet neither preserves order nor returns absent
|
|
3726
|
-
* keys, so missing keys are filled with explicit `null`).
|
|
3727
|
-
* - `range` single → {@link executeListInternal} for a `{ items, cursor }`
|
|
3728
|
-
* connection, with the cursor wrapped in a per-key envelope
|
|
3729
|
-
* ({@link encodePerKeyCursor}) so it carries the key it belongs to.
|
|
3730
|
-
*
|
|
3731
|
-
* The result-shape typing (single → bare, array → keyed) is expressed as call
|
|
3732
|
-
* overloads on {@link executeQueryMethod}.
|
|
3733
|
-
*/
|
|
3734
|
-
|
|
3735
|
-
/**
|
|
3736
|
-
* The **structural** shape of a resolved query contract the runtime executes.
|
|
3737
|
-
* It depends only on the facts the executor reads — the `query` discriminant and
|
|
3738
|
-
* the per-method `resolution` and internal `op` — and is deliberately
|
|
3739
|
-
* **independent of the phantom Key / params / result types** carried by
|
|
3740
|
-
* {@link QueryModelContract}. Those phantom types are invariant /
|
|
3741
|
-
* contravariant on the full interface, so a concrete `QueryModelContract<{…}>`
|
|
3742
|
-
* is not assignable to `QueryModelContract<unknown, …>`; this minimal structural
|
|
3743
|
-
* type sidesteps that variance while accepting every real contract (a
|
|
3744
|
-
* {@link QueryModelContract} is structurally assignable to it). The runtime never
|
|
3745
|
-
* needs the method param / result types — it returns the dynamic
|
|
3746
|
-
* cardinality-matrix shapes ({@link ContractItem} / {@link Connection} /
|
|
3747
|
-
* {@link KeyedResult}).
|
|
3748
|
-
*/
|
|
3749
|
-
interface ExecutableQueryContract {
|
|
3750
|
-
readonly kind: 'query';
|
|
3751
|
-
readonly methods: Readonly<Record<string, {
|
|
3752
|
-
readonly resolution: Resolution;
|
|
3753
|
-
/**
|
|
3754
|
-
* The method's decided input arity (issue #71): `'either'` for a
|
|
3755
|
-
* coalescible base-table `point` (a key array → one `BatchGetItem`),
|
|
3756
|
-
* `'single'` for a `range` (partition `Query`) **or** a unique-GSI `point`
|
|
3757
|
-
* (a per-key GSI `Query`, not coalescible). The executor honors it to
|
|
3758
|
-
* reject an array fed into any `'single'` method rather than fanning out.
|
|
3759
|
-
*/
|
|
3760
|
-
readonly inputArity: InputArity;
|
|
3761
|
-
readonly op: ContractMethodOp;
|
|
3762
|
-
}>>;
|
|
3763
|
-
}
|
|
3764
|
-
/** A plain contract key (the access pattern / join key), e.g. `{ articleId }`. */
|
|
3765
|
-
type ContractKeyInput = Record<string, unknown>;
|
|
3766
|
-
/**
|
|
3767
|
-
* The literal {@link InputArity} the contract `C` carries for method `K`. A
|
|
3768
|
-
* {@link QueryMethodSpec} carries the method's literal arity in its `inputArity`
|
|
3769
|
-
* type when the body was tagged with `point` / `range` (`src/define/contract.ts`);
|
|
3770
|
-
* an untagged method keeps the wide `InputArity`. Falls back to `InputArity` for a
|
|
3771
|
-
* structurally-typed contract (e.g. the bare {@link ExecutableQueryContract}),
|
|
3772
|
-
* which keeps the call surface array-capable (the runtime backstop still rejects an
|
|
3773
|
-
* array fed into a `'single'` method).
|
|
3774
|
-
*/
|
|
3775
|
-
type ArityOfMethod<C extends ExecutableQueryContract, K extends keyof C['methods']> = C['methods'][K] extends {
|
|
3776
|
-
readonly inputArity: infer A extends InputArity;
|
|
3777
|
-
} ? A : InputArity;
|
|
3778
|
-
/**
|
|
3779
|
-
* The **array** key-argument type accepted by `executeQueryMethod` for method `K`
|
|
3780
|
-
* of contract `C`. A method whose literal arity is `'single'` (a `range` read **or**
|
|
3781
|
-
* a unique-GSI `point` — both one request per key) accepts **no** array: the type
|
|
3782
|
-
* collapses to `never`, so passing an array is a `tsc` compile error at the
|
|
3783
|
-
* user-facing surface (issue #71, N+1 rule (a), the **primary** enforcement layer).
|
|
3784
|
-
* Any other arity (`'either'` / `'array'`, or the wide `InputArity` for an untagged
|
|
3785
|
-
* / structural contract) accepts an array of keys (it coalesces to one batched
|
|
3786
|
-
* call). This is the array-overload half of {@link ContractCallSignature}'s
|
|
3787
|
-
* narrowing, applied at the real execution surface.
|
|
3788
|
-
*/
|
|
3789
|
-
type ArrayKeyArg<C extends ExecutableQueryContract, K extends keyof C['methods']> = ArityOfMethod<C, K> extends 'single' ? never : readonly ContractKeyInput[];
|
|
3790
|
-
/** A single hydrated result item (only the method's projected fields). */
|
|
3791
|
-
type ContractItem = Record<string, unknown>;
|
|
3792
|
-
/**
|
|
3793
|
-
* A pagination connection: a page of items plus an opaque `cursor` for the next
|
|
3794
|
-
* page (`null` when the page is the last). For a `range` contract method the
|
|
3795
|
-
* cursor is a **per-key envelope** ({@link encodePerKeyCursor}) that identifies
|
|
3796
|
-
* the key it paginates.
|
|
3797
|
-
*/
|
|
3798
|
-
interface Connection<TItem = ContractItem> {
|
|
3799
|
-
readonly items: TItem[];
|
|
3800
|
-
readonly cursor: string | null;
|
|
3801
|
-
}
|
|
3802
|
-
/**
|
|
3803
|
-
* The keyed result of a batched contract read: input-key identity (the canonical
|
|
3804
|
-
* {@link serializeContractKey} string) → per-key result. Backed by a `Map` so
|
|
3805
|
-
* the association is explicit and order-independent; missing keys are present
|
|
3806
|
-
* with an explicit `null` value (`point`) rather than omitted.
|
|
3807
|
-
*
|
|
3808
|
-
* @typeParam TValue The per-key value (`Item | null` for `point`).
|
|
3809
|
-
*/
|
|
3810
|
-
type KeyedResult<TValue> = Map<string, TValue>;
|
|
3811
|
-
/**
|
|
3812
|
-
* Retrieval options accepted by a contract query method (the serializable subset
|
|
3813
|
-
* from the proposal). All optional; a `point` method ignores the pagination
|
|
3814
|
-
* fields, a `range` method honors them.
|
|
3815
|
-
*/
|
|
3816
|
-
interface ContractQueryParams {
|
|
3817
|
-
/** Strongly-consistent read (point reads on the base table only). */
|
|
3818
|
-
readonly consistentRead?: boolean;
|
|
3819
|
-
/** Page size for a `range` (`list`) method. */
|
|
3820
|
-
readonly limit?: number;
|
|
3821
|
-
/** Resume token — a per-key cursor envelope from a prior `range` page. */
|
|
3822
|
-
readonly after?: string;
|
|
3823
|
-
/** Sort direction for a `range` (`list`) method. */
|
|
3824
|
-
readonly order?: 'ASC' | 'DESC';
|
|
3825
|
-
}
|
|
3826
|
-
/**
|
|
3827
|
-
* Maximum number of in-flight per-key queries for a batched `range` fan-out.
|
|
3828
|
-
* Reuses the relation-traversal cap — the same rationale (bound in-flight
|
|
3829
|
-
* requests, ride out throttling via UnprocessedKeys retry beneath the cap). Note
|
|
3830
|
-
* that a single-contract `range` method is `inputArity: 'single'`, so the
|
|
3831
|
-
* fan-out path is only exercised by the (out-of-scope, #63) composition runtime;
|
|
3832
|
-
* the bound is defined here so the seam is ready and tested in isolation.
|
|
3833
|
-
*/
|
|
3834
|
-
declare const CONTRACT_RANGE_FANOUT_CONCURRENCY = 16;
|
|
3835
|
-
/**
|
|
3836
|
-
* Keyed BatchGet: resolve `keys` against `modelClass` in one batched call and
|
|
3837
|
-
* return a `Map<keyId, Item | null>` with every input key present (misses →
|
|
3838
|
-
* `null`). Reuses the `batch-retry` primitives (chunk ≤100, dedup, UnprocessedKeys
|
|
3839
|
-
* retry) — never reinvented.
|
|
3840
|
-
*
|
|
3841
|
-
* Implementation notes:
|
|
3842
|
-
* - **Dedup**: the same key supplied twice is fetched once; both map entries
|
|
3843
|
-
* point at the same resolved item (or `null`).
|
|
3844
|
-
* - **Order / absence**: BatchGet returns matched items unordered and omits
|
|
3845
|
-
* misses, so each returned raw item is indexed by its `PK::SK` and looked up
|
|
3846
|
-
* per input key; an unmatched key gets `null`.
|
|
3847
|
-
* - **Projection**: items are fetched whole (DynamoDB BatchGet has no reliable
|
|
3848
|
-
* server-side projection for mixed keys here) and hydrated against the
|
|
3849
|
-
* method's `select`, so the per-key item carries exactly the projected fields.
|
|
3850
|
-
*/
|
|
3851
|
-
declare function executeKeyedBatchGet(modelClass: Function, keys: readonly ContractKeyInput[], select: Record<string, unknown>): Promise<KeyedResult<ContractItem | null>>;
|
|
3852
|
-
/**
|
|
3853
|
-
* Execute a query-contract method for a **single** key.
|
|
3854
|
-
*
|
|
3855
|
-
* The result shape is determined by the method's derived `resolution`:
|
|
3856
|
-
* - `point` → `Item | null` (`key1 : result1`);
|
|
3857
|
-
* - `range` → `Connection` (`key1 : resultM`).
|
|
3858
|
-
*
|
|
3859
|
-
* A single key is accepted for **every** method regardless of arity.
|
|
3860
|
-
*
|
|
3861
|
-
* @param contract The resolved query contract (a {@link QueryModelContract}).
|
|
3862
|
-
* @param methodName The method to execute (a method name of `contract`).
|
|
3863
|
-
* @param key A single contract key.
|
|
3864
|
-
* @param params Retrieval options (pagination / consistency).
|
|
3865
|
-
*/
|
|
3866
|
-
declare function executeQueryMethod<C extends ExecutableQueryContract, K extends keyof C['methods'] & string>(contract: C, methodName: K, key: ContractKeyInput, params?: ContractQueryParams): Promise<ContractItem | null | Connection>;
|
|
3867
|
-
/**
|
|
3868
|
-
* Execute a query-contract method for an **array** of keys (a single batched
|
|
3869
|
-
* call). Valid **only** for a method whose literal arity is not `'single'` — a
|
|
3870
|
-
* coalescible base-table `point` (`inputArity: 'either'`, a key array → one
|
|
3871
|
-
* `BatchGetItem`). A `'single'` method (a `range` read **or** a unique-GSI `point`)
|
|
3872
|
-
* makes the array key argument `never`, so passing an array is a **`tsc` compile
|
|
3873
|
-
* error** by construction (issue #71, N+1 rule (a) — the primary enforcement
|
|
3874
|
-
* layer; the build-time checker and runtime backstop remain). The result is a keyed
|
|
3875
|
-
* `Map<keyId, Item | null>` (`keyN : resultN`).
|
|
3876
|
-
*
|
|
3877
|
-
* @param contract The resolved query contract.
|
|
3878
|
-
* @param methodName The method to execute (a method name of `contract`).
|
|
3879
|
-
* @param keys An array of contract keys (rejected for a `'single'` method).
|
|
3880
|
-
* @param params Retrieval options.
|
|
3881
|
-
*/
|
|
3882
|
-
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>>;
|
|
3883
|
-
/**
|
|
3884
|
-
* The runtime fan-out seam for a batched `range` (`keyN : resultN×M`). NOT
|
|
3885
|
-
* reachable for a single contract under #62 (a `range` method's `inputArity` is
|
|
3886
|
-
* `'single'`, so {@link executeQueryMethod} rejects an array). It is the
|
|
3887
|
-
* cross-contract composition runtime (#63) that drives a per-key range fan-out;
|
|
3888
|
-
* this function implements that fan-out — N bounded-parallel Queries grouped by
|
|
3889
|
-
* input key, each connection carrying its own per-key cursor — so the seam is
|
|
3890
|
-
* defined, reused (via {@link mapWithConcurrency}), and unit-tested in isolation
|
|
3891
|
-
* ahead of #63, rather than left as a stub. #62 does not call it on any contract
|
|
3892
|
-
* surface.
|
|
3893
|
-
*
|
|
3894
|
-
* @param op The `range` method op to run per key.
|
|
3895
|
-
* @param keys The parent keys to fan out over.
|
|
3896
|
-
* @param params Retrieval options applied to **every** key's connection.
|
|
3897
|
-
*/
|
|
3898
|
-
declare function executeRangeFanout(op: ContractMethodOp, keys: readonly ContractKeyInput[], params?: ContractQueryParams): Promise<KeyedResult<Connection>>;
|
|
3899
|
-
|
|
3900
|
-
/**
|
|
3901
|
-
* Single-service contract **Runtime** — execution of CQRS command-contract
|
|
3902
|
-
* methods (issue #64, Epic #57; spec `docs/cqrs-contract.md`,
|
|
3903
|
-
* "Command Contract" + "Command Definition"). WRITES only; query reads are #62
|
|
3904
|
-
* ({@link executeQueryMethod}) and cross-contract composition is #63 — both out
|
|
3905
|
-
* of scope here.
|
|
3906
|
-
*
|
|
3907
|
-
* Given a resolved command contract (a {@link CommandModelContract} from #58,
|
|
3908
|
-
* carrying per-method `result` and the declared {@link CommandBatchMode}, plus a
|
|
3909
|
-
* resolved write {@link ContractMethodOp}) it executes one method for a **single
|
|
3910
|
-
* key** or an **array of keys** and applies the write(s) to DynamoDB:
|
|
3911
|
-
*
|
|
3912
|
-
* | Input | resolution |
|
|
3913
|
-
* | -------------- | ----------------------------------------------------------- |
|
|
3914
|
-
* | `key` (single) | one write op (`put` / `update` / `delete`) |
|
|
3915
|
-
* | `keys[]` (N) | a **batched** write per the method's declared `batch` mode |
|
|
3916
|
-
*
|
|
3917
|
-
* The batched form is **declared by the contract** (proposal "Consistency with
|
|
3918
|
-
* the existing write surface"):
|
|
3919
|
-
*
|
|
3920
|
-
* - `'transact'` — one `TransactWriteItems`: the per-key write applied
|
|
3921
|
-
* **atomically** (all-or-nothing) and **condition-capable** ({@link ConditionInput},
|
|
3922
|
-
* `{ notExists }` / equality). DynamoDB caps `TransactWriteItems` at **25** and
|
|
3923
|
-
* the call is atomic, so an array of **>25 keys cannot be split** without
|
|
3924
|
-
* breaking atomicity — this runtime **rejects** it with a clear error rather
|
|
3925
|
-
* than silently truncating or chunking ({@link MAX_TRANSACT_ITEMS}).
|
|
3926
|
-
* - `'batchWrite'` — a `BatchWriteItem`: the per-key writes applied
|
|
3927
|
-
* **non-atomically**, **without conditions** (DynamoDB's `BatchWriteItem` has no
|
|
3928
|
-
* `ConditionExpression`). An array of any size is permitted; it is chunked per
|
|
3929
|
-
* the 25-items-per-request limit with `UnprocessedItems` retry, reusing the
|
|
3930
|
-
* existing batch machinery ({@link executeBatchWrite}).
|
|
3931
|
-
* - **undefined** — the method declared no batched form: an array is rejected (it
|
|
3932
|
-
* resolves only a single key).
|
|
3933
|
-
*
|
|
3934
|
-
* ## Reuse, not reinvention
|
|
3935
|
-
*
|
|
3936
|
-
* The recorded write op carries declarative sentinels ({@link ContractParamRef} /
|
|
3937
|
-
* {@link ContractKeyFieldRef}) at its value positions; {@link renderRecord} binds
|
|
3938
|
-
* them to the concrete `(key, params)` at execution time, producing the very same
|
|
3939
|
-
* `item` / `changes` / `key` / `condition` objects a hand-written `Model.put` /
|
|
3940
|
-
* `Model.update` / `Model.delete` would pass. Execution then delegates to the
|
|
3941
|
-
* proven write machinery:
|
|
3942
|
-
*
|
|
3943
|
-
* - single → {@link executePut} / {@link executeUpdate} / {@link executeDelete};
|
|
3944
|
-
* - transact → one `TransactWriteItems` built from `buildPutInput` /
|
|
3945
|
-
* `buildUpdateInput` / `buildDeleteInput` (the same inputs the imperative
|
|
3946
|
-
* {@link TransactionContext} uses), with the ≤25 limit enforced;
|
|
3947
|
-
* - batchWrite → {@link executeBatchWrite} (chunk ≤25, `UnprocessedItems` retry).
|
|
3948
|
-
*
|
|
3949
|
-
* The TS and Python runtimes produce identical effects for the same SSoT.
|
|
3950
|
-
*/
|
|
3951
|
-
|
|
3952
|
-
/**
|
|
3953
|
-
* The **structural** shape of a resolved command contract the runtime executes.
|
|
3954
|
-
* It depends only on the facts the executor reads — the `command` discriminant
|
|
3955
|
-
* and the per-method `op` / `batch` / `result` — and is deliberately
|
|
3956
|
-
* **independent of the phantom Key / params / result types** carried by
|
|
3957
|
-
* {@link CommandModelContract} (those are invariant on the full interface, so a
|
|
3958
|
-
* concrete `CommandModelContract<{…}>` is not assignable to
|
|
3959
|
-
* `CommandModelContract<unknown, …>`). Every real command contract is
|
|
3960
|
-
* structurally assignable to this minimal type.
|
|
3961
|
-
*/
|
|
3962
|
-
interface ExecutableCommandContract {
|
|
3963
|
-
readonly kind: 'command';
|
|
3964
|
-
readonly methods: Readonly<Record<string, {
|
|
3965
|
-
readonly op: ContractMethodOp;
|
|
3966
|
-
readonly result: CommandResultKind;
|
|
3967
|
-
readonly batch?: CommandBatchMode;
|
|
3968
|
-
/**
|
|
3969
|
-
* The composed write ops of a **multi-fragment** `mutation`-derived method
|
|
3970
|
-
* (issue #90). When present (N≥2), a **single-key** call executes ALL of
|
|
3971
|
-
* them as **one atomic `TransactWriteItems`** (not sequential writes), so a
|
|
3972
|
-
* condition failure on any fragment rolls back the whole transaction. Absent
|
|
3973
|
-
* for a single-fragment / #83 / hand-written #64 method (which executes its
|
|
3974
|
-
* one {@link op}). The read-back stays keyed on the primary op ({@link op}).
|
|
3975
|
-
*/
|
|
3976
|
-
readonly ops?: readonly ContractMethodOp[];
|
|
3977
|
-
/**
|
|
3978
|
-
* The referential-integrity assertions derived from the mutation's
|
|
3979
|
-
* `requires` effects (issue #84). When present (non-empty), a
|
|
3980
|
-
* **single-key** call composes the base write(s) **and** one read-only
|
|
3981
|
-
* `ConditionCheck` (`attribute_exists`) per assertion into **one atomic
|
|
3982
|
-
* `TransactWriteItems`** — so a single-op write is **promoted** to a
|
|
3983
|
-
* transaction. If any referenced entity is absent, its assertion fails and
|
|
3984
|
-
* the WHOLE transaction rolls back (the entity write never persists). Absent
|
|
3985
|
-
* on a mutation with no `requires` / a #64 hand-written method.
|
|
3986
|
-
*/
|
|
3987
|
-
readonly conditionChecks?: readonly DerivedConditionCheck[];
|
|
3988
|
-
/**
|
|
3989
|
-
* The adjacency edge writes derived from the mutation's `edges` effects
|
|
3990
|
-
* (issue #85). When present, a **single-key** call composes each edge's
|
|
3991
|
-
* adjacency `Put` / `Delete` item(s) into the **same atomic
|
|
3992
|
-
* `TransactWriteItems`** as the entity write (and any ConditionChecks /
|
|
3993
|
-
* derived updates), so the edge is created / removed / moved atomically with
|
|
3994
|
-
* the entity. Absent on a mutation with no `edges` / a #64 hand-written method.
|
|
3995
|
-
*/
|
|
3996
|
-
readonly edgeWrites?: readonly DerivedEdgeWrite[];
|
|
3997
|
-
/**
|
|
3998
|
-
* The derived counter updates from the mutation's `derive` effects (issue
|
|
3999
|
-
* #85): each an atomic `ADD` (`UpdateItem`) on a target counter (e.g.
|
|
4000
|
-
* `User.postCount += 1`). When present, a **single-key** call composes each
|
|
4001
|
-
* `ADD` into the same atomic `TransactWriteItems` as the entity write, so the
|
|
4002
|
-
* counter moves atomically (and never clobbers a concurrent increment — an
|
|
4003
|
-
* `ADD` needs no prior read). Absent on a mutation with no `derive`.
|
|
4004
|
-
*/
|
|
4005
|
-
readonly derivedUpdates?: readonly DerivedUpdate[];
|
|
4006
|
-
/**
|
|
4007
|
-
* The uniqueness guards derived from the mutation's `unique` effects (issue
|
|
4008
|
-
* #86): each a marker-row `Put` (`attribute_not_exists`) / `Delete` /
|
|
4009
|
-
* `Delete(old)+Put(new)` swap. When present, a **single-key** call composes
|
|
4010
|
-
* the guard item(s) into the same atomic `TransactWriteItems` as the entity
|
|
4011
|
-
* write, so a duplicate scoped value (the guard `Put`'s `attribute_not_exists`
|
|
4012
|
-
* failing) rolls the WHOLE transaction back — the entity is never created.
|
|
4013
|
-
* Absent on a mutation with no `unique` / a #64 hand-written method.
|
|
4014
|
-
*/
|
|
4015
|
-
readonly uniqueGuards?: readonly DerivedUniqueGuard[];
|
|
4016
|
-
/**
|
|
4017
|
-
* The outbox events derived from the mutation's `emits` effects (issue #87):
|
|
4018
|
-
* each a transactional-outbox `Put`. When present, a **single-key** call
|
|
4019
|
-
* composes each event's `Put` into the same atomic `TransactWriteItems` as the
|
|
4020
|
-
* entity write, so the event RECORD is written ATOMICALLY with the state it
|
|
4021
|
-
* announces (they can never diverge). The row is drainable from real
|
|
4022
|
-
* DynamoDB Streams (captured at the table layer regardless of code path);
|
|
4023
|
-
* delivery / handlers are OUT of scope. Since #94 the derived-command
|
|
4024
|
-
* transaction also feeds every committed item (including this outbox `Put`)
|
|
4025
|
-
* into the in-process `ChangeCaptureRegistry` seam, so the dev/test
|
|
4026
|
-
* `src/cdc/` emulator now observes this path too — production Streams and the
|
|
4027
|
-
* emulator agree. Absent on a mutation with no `emits`.
|
|
4028
|
-
*/
|
|
4029
|
-
readonly outboxEvents?: readonly DerivedOutboxEvent[];
|
|
4030
|
-
/**
|
|
4031
|
-
* The client-token idempotency guard derived from the mutation's `idempotency`
|
|
4032
|
-
* effect (issue #87): a marker-row `Put` (`attribute_not_exists`). When present,
|
|
4033
|
-
* a **single-key** call composes the guard `Put` into the same atomic
|
|
4034
|
-
* `TransactWriteItems` as the entity write, so a same-token re-execution fails
|
|
4035
|
-
* the guard and rolls the WHOLE transaction back — no effect is double-applied.
|
|
4036
|
-
* Absent on a mutation with no `idempotency` / a #64 hand-written method.
|
|
4037
|
-
*/
|
|
4038
|
-
readonly idempotencyGuard?: DerivedIdempotencyGuard;
|
|
4039
|
-
/**
|
|
4040
|
-
* The return projection of a `mutation`-derived method (issue #83). When
|
|
4041
|
-
* present, a **single-key** call performs the write then issues a
|
|
4042
|
-
* **consistent read-back** (`GetItem` with `ConsistentRead`) of the
|
|
4043
|
-
* written entity's primary key and returns the projected item via the
|
|
4044
|
-
* existing read-projection machinery. Absent on a #64 hand-written method.
|
|
4045
|
-
*/
|
|
4046
|
-
readonly returnSelection?: Readonly<Record<string, boolean>>;
|
|
4047
|
-
}>>;
|
|
4048
|
-
}
|
|
4049
|
-
/** The params a command method consumes (the body's `params` argument). */
|
|
4050
|
-
type ContractCommandParams = Record<string, unknown>;
|
|
4051
|
-
/**
|
|
4052
|
-
* The projected read-back item a `mutation`-derived command method returns, or
|
|
4053
|
-
* `null` when the written row was absent at read-back (e.g. a `remove`). A plain
|
|
4054
|
-
* record of the projected fields.
|
|
4055
|
-
*/
|
|
4056
|
-
type CommandReturn = Record<string, unknown> | null;
|
|
4057
|
-
/**
|
|
4058
|
-
* Execute a command-contract method for a **single** key — one write op
|
|
4059
|
-
* (`put` / `update` / `delete`).
|
|
4060
|
-
*
|
|
4061
|
-
* @param contract The resolved command contract (a {@link CommandModelContract}).
|
|
4062
|
-
* @param methodName The method to execute.
|
|
4063
|
-
* @param key A single contract key (the mutation target).
|
|
4064
|
-
* @param params The mutation params (the body's `params` argument).
|
|
4065
|
-
*/
|
|
4066
|
-
declare function executeCommandMethod(contract: ExecutableCommandContract, methodName: string, key: ContractKeyInput, params?: ContractCommandParams): Promise<void | CommandReturn>;
|
|
4067
|
-
/**
|
|
4068
|
-
* Execute a command-contract method for an **array** of keys — a batched write
|
|
4069
|
-
* per the method's declared {@link CommandBatchMode}:
|
|
4070
|
-
*
|
|
4071
|
-
* - `'transact'` → one atomic `TransactWriteItems` (≤25, condition-capable); a
|
|
4072
|
-
* >25-key array is **rejected** (an atomic transaction cannot be split).
|
|
4073
|
-
* - `'batchWrite'` → a `BatchWriteItem` (no conditions; chunked ≤25 with
|
|
4074
|
-
* `UnprocessedItems` retry).
|
|
4075
|
-
*
|
|
4076
|
-
* A method that declared no batched form rejects an array with a clear error.
|
|
4077
|
-
*
|
|
4078
|
-
* @param contract The resolved command contract.
|
|
4079
|
-
* @param methodName The method to execute.
|
|
4080
|
-
* @param keys An array of contract keys.
|
|
4081
|
-
* @param params The mutation params shared across every key.
|
|
4082
|
-
*/
|
|
4083
|
-
declare function executeCommandMethod(contract: ExecutableCommandContract, methodName: string, keys: readonly ContractKeyInput[], params?: ContractCommandParams): Promise<void>;
|
|
4084
|
-
|
|
4085
826
|
/**
|
|
4086
827
|
* Per-key cursor envelope for batched `range` (`list`) contract methods (issue
|
|
4087
828
|
* #62, CQRS single-service runtime; spec `docs/cqrs-contract.md`,
|
|
@@ -4549,7 +1290,7 @@ type ParamLeavesOf<X> = X extends Param<infer V> ? V : X extends object ? {
|
|
|
4549
1290
|
* These accept {@link Param} placeholders at scalar leaves while keeping field
|
|
4550
1291
|
* names / value types / the key boundary / strict-select checked at definition
|
|
4551
1292
|
* time (via {@link Parameterize} layered on the model's real key & select
|
|
4552
|
-
* types). `Model.query` / `Model.
|
|
1293
|
+
* types). `Model.query` / `Model.putItem` / … are **untouched**.
|
|
4553
1294
|
*
|
|
4554
1295
|
* `defineQueries({...})` / `defineCommands({...})` take a record of these
|
|
4555
1296
|
* definition nodes and return the same record (typed), forming the IR the
|
|
@@ -5306,4 +2047,4 @@ declare function assertBundleSerializable(bundle: BridgeBundle): void;
|
|
|
5306
2047
|
*/
|
|
5307
2048
|
declare function buildBridgeBundle(queries?: DefinitionMap, commands?: DefinitionMap, registry?: typeof MetadataRegistry, transactions?: Record<string, TransactionDefinition>, contractInputs?: ContractInputs): BridgeBundle;
|
|
5308
2049
|
|
|
5309
|
-
export { type AnyModelContract,
|
|
2050
|
+
export { type AnyModelContract, AnyOperationDefinition, BATCH_GET_MAX_KEYS, BATCH_WRITE_MAX_ITEMS, BatchGetExecInput, BatchWriteExecItem, BridgeBundle, type BuiltContracts, CdcEmulator, CdcEmulatorOptions, ChangeEvent, ChangeHandler, ClientManager, type CollectParams, CommandMethodSpec, CommandModelContract, CommandSpec, ConcurrentRecomputeRef, type ConditionExpressionResult, ConditionInput, ConditionSpec, type ContextOwnership, type ContextOwnershipMap, ContextSpec, type ContractBoundaryViolation, type ContractInputs, type ContractMap, type ContractN1Violation, ContractSpec, DDBModel, DefinitionMap, DeleteInput, DynamoDBOperation, DynamoExecutor, type DynamoType, EDGE_WRITES_MARKER, type EdgeLifecycle, type EdgeWriteDeclaration, type EdgeWriteRecorder, type EdgeWritesDefinition, type EmbeddedMetadata, type EntityMetadata, EntityRef, EventLog, ExecutionPlan, Executor, ExecutorResult, type ExplainInput, FaultSpec, type FieldMetadata, type FieldOptions, type FilterExpressionResult, type GsiDefinition, type KeyDefinition, type LintResult, type LintRule, Linter, type ListInput, type ListOptions, MAX_TRANSACT_ITEMS, Manifest, MetadataRegistry, type ModelOptions, ModelStatic, OLD_VALUE_NAMESPACE, OperationDefinition, OperationsDocument, Param, ParamDescriptor, type Parameterize, PartialQueryKeyOf, type PerKeyCursorEnvelope, type PlanInput, PrimaryKeyOf, type ProjectionResult, PutInput, QueryMethodSpec, QueryModelContract, type QueryOptions$1 as QueryOptions, QuerySpec, type RelationLimitOptions, type RelationMetadata, type RelationOptions, type RelationTraversalOptions, ReplayOptions, ResolvedKey, SegmentedKey, SelectableOf, ShardId, TableMapping, TransactWriteExecItem, type TransactionDefinition, TransactionItemSpec, type TransactionParamShape, type TransactionRef, TransactionSpec, type TxConditionCheckOptions, type TxForEachInstruction, type TxForEachOptions, type TxInstruction, type TxRecorder, type TxWriteInstruction, type TxWriteOptions, UniqueQueryKeyOf, type UpdateExpressionResult, UpdateInput, type WhenComparison, WriteDefinitionOptions, WriteExecOptions, WriteResult, assertBundleSerializable, assertContractBoundaries, assertContractN1Safe, assertJsonSerializable, assertSupportedCondition, belongsTo, binary, boolean, buildBridgeBundle, buildConditionExpression, buildContexts, buildContracts, buildManifest, buildOperations, buildProjection, buildQuerySpec, buildTransactionSpec, buildTransactions, buildUpdateExpression, collectContractBoundaryViolations, collectContractN1Violations, compileFilterExpression, createCdcEmulator, createDefaultLinter, datetime, decodeCursor, decodePerKeyCursor, defineCommands, defineDelete, defineList, definePut, defineQueries, defineQuery, defineTransaction, defineTransactions, defineUpdate, deriveEdgeWriteItems, deriveEdgeWriteItemsFor, deriveModelEdgeWriteItems, derivePrefix, detectRelationFields, edgeWrites, embedded, encodeCursor, encodePerKeyCursor, evaluateFilter, execute, executeDeclarativeTransaction, executeExplain, executeList, executeQuery, expandTransaction, field, getEdgeWrites, getImplicitKeyFields, gsiAmbiguityRule, hasMany, hasOne, hydrate, isEdgeWritesDefinition, isSelectBuilder, isTransactionRef, list, literal, map, missingGsiRule, model, noScanRule, number, numberSet, plan, queryBoundaryRule, relationDepthRule, requireLimitRule, resolveKey, resolveRelations, serializeContractKey, serializeFieldValue, string, stringSet, validateDepth, validateGsiAmbiguity, when };
|