graphddb 0.2.5 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/chunk-PNIZS37E.js +916 -0
- package/dist/{chunk-SGYBE2OV.js → chunk-VST3WOK3.js} +597 -113
- package/dist/{chunk-4TYK3AV6.js → chunk-YIXXTGZ6.js} +1369 -15
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +587 -75
- package/dist/index.js +535 -16
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types--tdHK3vi.d.ts → types-DuJ08bgc.d.ts} +1542 -63
- package/package.json +1 -1
- package/dist/chunk-ITHQ2EDH.js +0 -461
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as SelectableOf, g as PrimaryKeyOf, h as RequestContext, i as Middleware, j as ReadRequestKind, k as CtxModel, l as ReadParams, m as ReadRequestCtx, D as DynamoDBOperation, I as Item, E as Executor, n as RetryPolicy, o as ExecutionPlanSpec, p as
|
|
2
|
-
export {
|
|
1
|
+
import { S as SelectableOf, g as PrimaryKeyOf, h as RequestContext, i as Middleware, j as ReadRequestKind, k as CtxModel, l as ReadParams, m as ReadRequestCtx, D as DynamoDBOperation, I as Item, E as Executor, n as RetryPolicy, o as ExecutionPlanSpec, p as EntityMetadata, K as KeyDefinition, G as GsiDefinition, F as FieldOptions, q as DynamoType, r as RelationOptions, A as AggregateOptions, s as AggregateValue, t as SelectBuilderSpec, u as RawCondition, v as TransactionSpec, w as Manifest, x as RetryOverride, y as ExecutionPlan, z as FieldMetadata, H as ResolvedKey, R as ReadExecOptions, a as ExecutorResult, B as BatchGetExecInput, P as PutInput, W as WriteExecOptions, b as WriteResult, U as UpdateInput, c as DeleteInput, d as BatchWriteExecItem, e as BatchExecOptions, T as TransactWriteExecItem, J as CdcEmulatorOptions, L as ChangeHandler, N as Unsubscribe, O as FaultSpec, Q as ConcurrentRecomputeRef, C as ChangeEvent, V as EventLog, X as ReplayOptions, Y as ShardId, Z as ViewDefinition, _ as ProjectionDefinition, $ as RelationMetadata, a0 as TransactionItemSpec, a1 as MaintainEffect, M as ModelStatic, f as DDBModel, a2 as Param, a3 as ParamDescriptor, a4 as DefinitionMap, a5 as OperationDefinition, a6 as WriteDefinitionOptions, a7 as PartialQueryKeyOf, a8 as StrictSelectSpec, a9 as EntityInput, aa as UniqueQueryKeyOf, ab as EntityRef, ac as ConditionInput, ad as QueryModelContract, ae as QueryMethodSpec, af as CommandModelContract, ag as CommandMethodSpec, ah as ContractSpec, ai as QuerySpec, aj as CommandSpec, ak as ContextSpec, al as OperationsDocument, am as AnyOperationDefinition, an as BridgeBundle, ao as ConditionSpec } from './types-DuJ08bgc.js';
|
|
2
|
+
export { ap as AggregateMetadata, aq as BatchDeleteRequest, ar as BatchGetOptions, as as BatchGetRequest, at as BatchGetResult, au as BatchPutRequest, av as BatchResult, aw as BatchWriteRequest, ax as CONTRACT_RANGE_FANOUT_CONCURRENCY, ay as CdcMode, az as Change, aA as ChangeBatch, aB as ChangeEventName, aC as ClockMode, aD as CollectionEffect, aE as CollectionOptions, aF as Column, aG as ColumnMap, aH as CommandContractMethodSpec, aI as CommandInputShape, aJ as CommandMethod, aK as CommandPlan, aL as CommandResolutionTarget, aM as CommandResultKind, aN as CommandSelectShape, aO as CompiledFragment, aP as ComposeSpec, aQ as CondSlot, aR as ConditionCheckInput, aS as Connection, aT as ContractCallSignature, aU as ContractCardinality, aV as ContractCommandParams, aW as ContractCommandResult, aX as ContractComposeNode, aY as ContractFromRef, aZ as ContractInputArity, a_ as ContractItem, a$ as ContractKeyFieldRef, b0 as ContractKeyInput, b1 as ContractKeyRef, b2 as ContractKeySpec, b3 as ContractKind, b4 as ContractMethodOp, b5 as ContractParamRef, b6 as ContractQueryParams, b7 as ContractResolution, b8 as CounterAggregate, b9 as CounterEffect, ba as CtxBase, bb as DEFAULT_MAX_ATTEMPTS, bc as DEFAULT_RETRY_POLICY, bd as DeleteOptions, be as DeriveEffect, bf as DerivedEdgeWrite, bg as DerivedUpdate, bh as DescriptorBinding, bi as ENTITY_WRITES_MARKER, bj as EdgeEffect, bk as EffectPath, bl as EmbeddedMetadata, bm as EmitEffect, bn as EntityWritesDefinition, bo as EntityWritesShape, bp as ExecutableCommandContract, bq as ExecutableQueryContract, br as FilterInput, bs as FilterSpec, bt as FragmentInput, bu as GsiDefinitionMarker, bv as GsiOptions, bw as IdempotencyEffect, bx as InProcessWriteDescriptor, by as InputArity, bz as KeyDefinitionMarker, bA as KeySegment, bB as KeySlot, bC as KeyStructure, bD as KeyedResult, bE as LIFECYCLE_CONTRACT_MARKER, bF as LifecycleContract, bG as LifecycleEffects, bH as LiteralParam, bI as MaintainConsistency, bJ as MaintainEvent, bK as MaintainItem, bL as MaintainTrigger, bM as MaintainUpdateMode, bN as MaintenanceGraph, bO as ManifestEntity, bP as ManifestField, bQ as ManifestFieldType, bR as ManifestGsi, bS as ManifestKey, bT as ManifestRelation, bU as ManifestTable, bV as MembershipEffect, bW as MembershipPredicate, bX as MembershipPredicateOp, bY as ModelRef, bZ as MutateMode, b_ as MutateOptions, b$ as MutateParallelResult, c0 as MutateTransactionResult, c1 as MutationBody, c2 as MutationDescriptorMap, c3 as MutationFragment, c4 as MutationInputProxy, c5 as MutationInputRef, c6 as MutationIntent, c7 as NumberParam, c8 as OperationKind, c9 as OperationSpec, ca as ParallelOpResult, cb as ParamKind, cc as ParamSpec, cd as ParamStructure, ce as PersistCtx, cf as PersistOrigin, cg as PlannedCommandMethod, ch as ProjectionDefinitionInput, ci as ProjectionDefinitionMap, cj as ProjectionMap, ck as ProjectionTransform, cl as ProjectionTransformOp, cm as ProjectionVia, cn as PutOptions, co as QueryContractMethodSpec, cp as QueryEnvelopeResult, cq as QueryKeyOf, cr as QueryMethod, cs as QueryResult, ct as RangeConditionSpec, cu as ReadEnvelope, cv as ReadOpCtx, cw as ReadOpKind, cx as ReadOperationType, cy as ReadRouteDescriptor, cz as ReadRouteOptions, cA as ReadRouteResult, cB as RecordedCompose, cC as RelationBuilder, cD as RelationConsistency, cE as RelationLimitOptions, cF as RelationPattern, cG as RelationProjection, cH as RelationReadOptions, cI as RelationSelect, cJ as RelationSpec, cK as RelationUpdateMode, cL as RelationWriteOptions, cM as RequiresEffect, cN as Resolution, cO as RetryInfo, cP as RetryOperationKind, cQ as SPEC_VERSION, cR as SegmentSpec, cS as SegmentedKey, cT as SelectBuilder, cU as SelectOf, cV as SnapshotEffect, cW as StartingPosition, cX as StreamViewType, cY as StringParam, cZ as TransactionContext, c_ as TransactionItemType, c$ as UniqueEffect, d0 as Updatable, d1 as UpdateOptions, d2 as VersionedDefinition, d3 as VersionedDefinitionInput, d4 as ViewDefinitionInput, d5 as ViewDefinitionMap, d6 as ViewRegistry, d7 as ViewSource, d8 as ViewSourceSlice, d9 as WhenSpec, da as WriteCtx, db as WriteDescriptor, dc as WriteEnvelope, dd as WriteInput, de as WriteKind, df as WriteLifecyclePhase, dg as WriteMiddleware, dh as WriteOperationType, di as WriteRecorder, dj as WriteResultProjection, dk as attachModelClass, dl as buildDeleteInput, dm as buildMaintenanceGraph, dn as buildPutInput, dp as buildUpdateInput, dq as compileFragment, dr as compileMutationPlan, ds as compileSingleFragmentPlan, dt as cond, du as contractOfMethodSpec, dv as definePlan, dw as defineProjection, dx as defineProjections, dy as defineVersioned, dz as defineView, dA as defineViews, dB as entityWrites, dC as executeBatchGet, dD as executeBatchWrite, dE as executeCommandMethod, dF as executeDelete, dG as executeKeyedBatchGet, dH as executePut, dI as executeQueryMethod, dJ as executeRangeFanout, dK as executeTransaction, dL as executeUpdate, dM as from, dN as getEntityWrites, dO as gsi, dP as identity, dQ as isColumn, dR as isCommandModelContract, dS as isCommandPlan, dT as isContractComposeNode, dU as isContractFromRef, dV as isContractKeyFieldRef, dW as isContractKeyRef, dX as isContractParamRef, dY as isEntityWritesDefinition, dZ as isKeySegment, d_ as isLifecycleContract, d$ as isMaintainTrigger, e0 as isMutationFragment, e1 as isMutationInputRef, e2 as isParam, e3 as isPlannedCommandMethod, e4 as isProjectionDefinition, e5 as isQueryModelContract, e6 as isRetryableError, e7 as isRetryableTransactionCancellation, e8 as isViewDefinition, e9 as k, ea as key, eb as lifecyclePhaseForIntent, ec as maintainTrigger, ed as mintContractKeyFieldRef, ee as mintContractParamRef, ef as mutation, eg as param, eh as preview, ei as publicCommandModel, ej as publicQueryModel, ek as query, el as resolveLifecycle, em as whenMember, en as wholeKeysSentinel } from './types-DuJ08bgc.js';
|
|
3
3
|
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
4
4
|
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
5
5
|
|
|
@@ -34,7 +34,7 @@ type ListOptions<T, S extends SelectableOf<T> = SelectableOf<T>> = {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Read-middleware runtime (issue #138). Drives the registered {@link Middleware}
|
|
37
|
-
* list through the read hook points R1–R5 with the ordering
|
|
37
|
+
* list through the read hook points R1–R5 with the ordering docs/middleware.md
|
|
38
38
|
* specifies:
|
|
39
39
|
*
|
|
40
40
|
* - `before*` (R1 / R2) run **first-registered-first** (FIFO);
|
|
@@ -52,7 +52,7 @@ type ListOptions<T, S extends SelectableOf<T> = SelectableOf<T>> = {
|
|
|
52
52
|
* set stable even if middleware is (un)registered mid-flight, and is
|
|
53
53
|
* concurrency-safe under parallel fan-out (#31).
|
|
54
54
|
*
|
|
55
|
-
* @see docs/
|
|
55
|
+
* @see docs/middleware.md
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
58
|
/**
|
|
@@ -313,71 +313,6 @@ interface RelationExecutionPlan {
|
|
|
313
313
|
readonly resultPaths: readonly string[];
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
type DynamoType = 'S' | 'N' | 'BOOL' | 'B' | 'SS' | 'NS' | 'L' | 'M';
|
|
317
|
-
interface FieldOptions {
|
|
318
|
-
format?: 'datetime' | 'date';
|
|
319
|
-
readonly?: boolean;
|
|
320
|
-
serialize?: (value: unknown) => unknown;
|
|
321
|
-
deserialize?: (value: unknown) => unknown;
|
|
322
|
-
}
|
|
323
|
-
interface FieldMetadata {
|
|
324
|
-
propertyName: string;
|
|
325
|
-
dynamoType: DynamoType;
|
|
326
|
-
options?: FieldOptions;
|
|
327
|
-
/**
|
|
328
|
-
* For a `@literal(...)` field — a `string`-stored field whose value is one of a
|
|
329
|
-
* known, finite set — the allowed literal values, in declaration order (issue
|
|
330
|
-
* #75). Recorded purely as model metadata so a Contract param bound into the
|
|
331
|
-
* field can recover the precise `literal` param-kind from its bind position
|
|
332
|
-
* (`src/spec/contracts.ts`), serializing as `{ type: 'literal', literals: [...] }`
|
|
333
|
-
* instead of the imprecise `'string'`. JSON-safe by construction (a non-empty
|
|
334
|
-
* array of strings). `undefined` for every other field decorator (`@string`,
|
|
335
|
-
* `@number`, …), so plain fields are byte-for-byte unchanged (backward compat).
|
|
336
|
-
*/
|
|
337
|
-
literals?: readonly string[];
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
interface KeyDefinition {
|
|
341
|
-
/** The canonical structured key (PK/SK segment lists). */
|
|
342
|
-
segmented: SegmentedKey;
|
|
343
|
-
inputFieldNames: string[];
|
|
344
|
-
}
|
|
345
|
-
interface GsiDefinition {
|
|
346
|
-
indexName: string;
|
|
347
|
-
/** The canonical structured key (PK/SK segment lists). */
|
|
348
|
-
segmented: SegmentedKey;
|
|
349
|
-
inputFieldNames: string[];
|
|
350
|
-
unique: boolean;
|
|
351
|
-
}
|
|
352
|
-
interface RelationLimitOptions {
|
|
353
|
-
default: number;
|
|
354
|
-
max: number;
|
|
355
|
-
}
|
|
356
|
-
interface RelationOptions {
|
|
357
|
-
limit?: RelationLimitOptions;
|
|
358
|
-
order?: 'ASC' | 'DESC';
|
|
359
|
-
}
|
|
360
|
-
interface RelationMetadata {
|
|
361
|
-
type: 'hasMany' | 'hasOne' | 'belongsTo';
|
|
362
|
-
propertyName: string;
|
|
363
|
-
targetFactory: () => new (...args: unknown[]) => unknown;
|
|
364
|
-
keyBinding: Record<string, string>;
|
|
365
|
-
options?: RelationOptions;
|
|
366
|
-
}
|
|
367
|
-
interface EmbeddedMetadata {
|
|
368
|
-
propertyName: string;
|
|
369
|
-
modelFactory: () => new (...args: unknown[]) => unknown;
|
|
370
|
-
}
|
|
371
|
-
interface EntityMetadata {
|
|
372
|
-
tableName: string;
|
|
373
|
-
prefix: string;
|
|
374
|
-
fields: FieldMetadata[];
|
|
375
|
-
primaryKey: KeyDefinition | null;
|
|
376
|
-
gsiDefinitions: GsiDefinition[];
|
|
377
|
-
relations: RelationMetadata[];
|
|
378
|
-
embeddedFields: EmbeddedMetadata[];
|
|
379
|
-
}
|
|
380
|
-
|
|
381
316
|
interface LintRule {
|
|
382
317
|
id: string;
|
|
383
318
|
severity: 'error' | 'warning';
|
|
@@ -401,6 +336,22 @@ declare class Linter {
|
|
|
401
336
|
declare class MetadataRegistry {
|
|
402
337
|
private static store;
|
|
403
338
|
private static _linter;
|
|
339
|
+
/**
|
|
340
|
+
* A monotonically-increasing counter bumped on every change to the set of
|
|
341
|
+
* registered models ({@link register} / {@link clear}). It lets a consumer that
|
|
342
|
+
* caches a structure derived from the whole registry (e.g. the maintenance graph
|
|
343
|
+
* in `src/spec/mutation-command.ts`) detect that the registry has changed since
|
|
344
|
+
* the cache was built and rebuild — closing the silent-drop window where a model
|
|
345
|
+
* registered after a cache build would be invisible to it. Only the membership
|
|
346
|
+
* of {@link store} changes here; per-model lazy finalize ({@link finalize}) does
|
|
347
|
+
* not bump it (it adds no relations/triggers, so it cannot change the graph).
|
|
348
|
+
*/
|
|
349
|
+
private static _generation;
|
|
350
|
+
/**
|
|
351
|
+
* The current registry generation (see {@link _generation}). A consumer caches
|
|
352
|
+
* the value it built against and rebuilds when it no longer matches.
|
|
353
|
+
*/
|
|
354
|
+
static get generation(): number;
|
|
404
355
|
static register(target: Function, metadata: EntityMetadata): void;
|
|
405
356
|
static get(target: Function): EntityMetadata;
|
|
406
357
|
static has(target: Function): boolean;
|
|
@@ -531,9 +482,92 @@ declare const map: FieldDecorator<Record<string, unknown>>;
|
|
|
531
482
|
declare function embedded(modelFactory: () => new (...args: unknown[]) => unknown): (_value: undefined, context: ClassFieldDecoratorContext) => void;
|
|
532
483
|
|
|
533
484
|
declare function hasMany(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>, options?: RelationOptions): (_value: undefined, context: ClassFieldDecoratorContext) => void;
|
|
534
|
-
declare function belongsTo(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string
|
|
485
|
+
declare function belongsTo(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>, options?: RelationOptions): (_value: undefined, context: ClassFieldDecoratorContext) => void;
|
|
535
486
|
declare function hasOne(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>): (_value: undefined, context: ClassFieldDecoratorContext) => void;
|
|
536
487
|
|
|
488
|
+
/**
|
|
489
|
+
* `@aggregate` — a **scalar-aggregate** field decorator (Epic #118 §5.2 counter /
|
|
490
|
+
* latest preset; issue #122).
|
|
491
|
+
*
|
|
492
|
+
* Where `@hasMany` / `@belongsTo` declare a *navigation* (a relation yielding
|
|
493
|
+
* model instances) and `@string` / `@number` declare a *stored* attribute,
|
|
494
|
+
* `@aggregate` declares a **scalar value derived by aggregating a source
|
|
495
|
+
* entity's rows**. It is a field (the decorated property holds a `number` /
|
|
496
|
+
* `string`), but — like a relation — it carries a source `targetFactory` +
|
|
497
|
+
* `keyBinding` selecting the rows it aggregates. The declaration surface mirrors
|
|
498
|
+
* the RFC §5.2 example:
|
|
499
|
+
*
|
|
500
|
+
* ```ts
|
|
501
|
+
* @model({ table: 'AppTable', prefix: 'THREAD' })
|
|
502
|
+
* class ThreadSummaryModel extends DDBModel {
|
|
503
|
+
* @string threadId!: string;
|
|
504
|
+
*
|
|
505
|
+
* @aggregate(() => PostModel, { threadId: 'threadId' }, {
|
|
506
|
+
* pattern: 'counter', value: count(),
|
|
507
|
+
* write: { maintainedOn: ['Post.created', 'Post.removed'] },
|
|
508
|
+
* })
|
|
509
|
+
* postCount!: number;
|
|
510
|
+
*
|
|
511
|
+
* @aggregate(() => PostModel, { threadId: 'threadId' }, {
|
|
512
|
+
* pattern: 'counter', value: max('createdAt'),
|
|
513
|
+
* write: { maintainedOn: ['Post.created'] },
|
|
514
|
+
* })
|
|
515
|
+
* lastPostAt!: string;
|
|
516
|
+
* }
|
|
517
|
+
* ```
|
|
518
|
+
*
|
|
519
|
+
* ## Scope (issue #122)
|
|
520
|
+
*
|
|
521
|
+
* This is the **declaration surface only** — the decorator records an
|
|
522
|
+
* {@link AggregateMetadata} into its own collector array (drained by `@model`
|
|
523
|
+
* into `EntityMetadata.aggregates`), exactly as the relation decorators push to
|
|
524
|
+
* the relation collector. The aggregation *effect* (maintenance graph #124,
|
|
525
|
+
* compile injection #125, runtime #127) is out of scope and consumes this
|
|
526
|
+
* metadata later.
|
|
527
|
+
*
|
|
528
|
+
* ## Vocabulary reuse (no double definition)
|
|
529
|
+
*
|
|
530
|
+
* `options.pattern` is the shared {@link RelationPattern} (typically `'counter'`)
|
|
531
|
+
* and `options.write` is the shared {@link RelationWriteOptions} — `maintainedOn`
|
|
532
|
+
* triggers on the `created|updated|removed` event vocabulary plus
|
|
533
|
+
* `consistency` (`transactional|eventual`) / `updateMode` (`mutation|stream`).
|
|
534
|
+
* Both were defined by B (#121) in `src/metadata/types.ts` and are **reused**
|
|
535
|
+
* here, never re-defined.
|
|
536
|
+
*
|
|
537
|
+
* @param targetFactory - Resolves the source entity whose rows are aggregated
|
|
538
|
+
* (lazy, mirroring the relation decorators, to tolerate circular imports).
|
|
539
|
+
* @param keyBinding - The key binding selecting the source rows
|
|
540
|
+
* (target key field → source field).
|
|
541
|
+
* @param options - The aggregate preset / value / maintenance-write declaration.
|
|
542
|
+
*/
|
|
543
|
+
declare function aggregate(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>, options: AggregateOptions): (_value: undefined, context: ClassFieldDecoratorContext) => void;
|
|
544
|
+
/**
|
|
545
|
+
* Value helper for an `@aggregate` field: the **count** aggregation — the
|
|
546
|
+
* cardinality of source rows matched by the key binding (Epic #118 §5.2
|
|
547
|
+
* `counter` preset, `count()`). It takes no source field (it counts rows, not a
|
|
548
|
+
* value), so the decorated field is a `number` (e.g. `postCount!: number`).
|
|
549
|
+
*
|
|
550
|
+
* Produces the {@link AggregateValue} IR node `{ op: 'count' }`. This is the
|
|
551
|
+
* standalone, recorder-free authoring helper used directly in the decorator
|
|
552
|
+
* argument (mirroring the standalone `preview(...)` / `identity(...)` projection
|
|
553
|
+
* helpers A (#120) exposes for relation options).
|
|
554
|
+
*/
|
|
555
|
+
declare function count(): AggregateValue;
|
|
556
|
+
/**
|
|
557
|
+
* Value helper for an `@aggregate` field: the **max** aggregation — the maximum
|
|
558
|
+
* value of a named source `field` across the rows matched by the key binding
|
|
559
|
+
* (Epic #118 §5.2 `counter` / latest preset, `max('createdAt')`). The decorated
|
|
560
|
+
* field's type matches the source field's (e.g. `lastPostAt!: string` ←
|
|
561
|
+
* `max('createdAt')`).
|
|
562
|
+
*
|
|
563
|
+
* Produces the {@link AggregateValue} IR node `{ op: 'max', field }`.
|
|
564
|
+
*
|
|
565
|
+
* @param field - The source field whose maximum is tracked (e.g. `'createdAt'`).
|
|
566
|
+
* @throws if `field` is not a non-empty string (an aggregation over no source
|
|
567
|
+
* field is meaningless — a defensive runtime check mirroring `@literal`'s).
|
|
568
|
+
*/
|
|
569
|
+
declare function max(field: string): AggregateValue;
|
|
570
|
+
|
|
537
571
|
declare class TableMapping {
|
|
538
572
|
private static mapping;
|
|
539
573
|
static set(mapping: Record<string, string>): void;
|
|
@@ -1041,6 +1075,246 @@ declare class CdcEmulator {
|
|
|
1041
1075
|
/** Create a CDC emulator (spec §10). */
|
|
1042
1076
|
declare function createCdcEmulator(opts?: CdcEmulatorOptions): CdcEmulator;
|
|
1043
1077
|
|
|
1078
|
+
/**
|
|
1079
|
+
* CDC **maintenance drain** (issue #130) — the asynchronous lower for
|
|
1080
|
+
* `updateMode: 'stream'` maintainers (Epic #118 §5.1).
|
|
1081
|
+
*
|
|
1082
|
+
* A synchronous (`updateMode: 'mutation'`) maintainer composes its owner-row write
|
|
1083
|
+
* into the SAME atomic `TransactWriteItems` as the source write (#127). A
|
|
1084
|
+
* `updateMode: 'stream'` maintainer instead emits a **maintenance-outbox** marker row
|
|
1085
|
+
* (`OUTBOX#MAINT#…`) ATOMICALLY with the source write (the #130 compile lowering in
|
|
1086
|
+
* `src/spec/mutation-command.ts`), and THIS consumer — driven by the {@link
|
|
1087
|
+
* import('./emulator.js').CdcEmulator} (or, in production, real DynamoDB Streams +
|
|
1088
|
+
* a Lambda built around {@link createMaintenanceDrainHandler}) — applies the owner-row
|
|
1089
|
+
* write asynchronously.
|
|
1090
|
+
*
|
|
1091
|
+
* ## Why a separate async path exists (not just "do it later synchronously")
|
|
1092
|
+
*
|
|
1093
|
+
* The async path realizes the maintenance operations a single synchronous
|
|
1094
|
+
* `UpdateExpression` cannot express against an unread item:
|
|
1095
|
+
*
|
|
1096
|
+
* - **collection `maxItems` trim** — keeping a bounded, ordered list requires reading
|
|
1097
|
+
* the current list, appending, sorting, and trimming (a read-modify-write); a
|
|
1098
|
+
* synchronous `list_append` can only append (Phase 1 was append-only, #127).
|
|
1099
|
+
* - **`removed`-driven splice** — removing an entry from a maintained collection on a
|
|
1100
|
+
* source `removed` event likewise needs the current list.
|
|
1101
|
+
* - **running `max(field)`** — a conditional `SET` (`#a = :v` guarded by
|
|
1102
|
+
* `attribute_not_exists(#a) OR #a < :v`) whose failed guard, if run synchronously,
|
|
1103
|
+
* would roll back the legitimate source write.
|
|
1104
|
+
*
|
|
1105
|
+
* ## Delivery guarantees (reused from the CDC substrate)
|
|
1106
|
+
*
|
|
1107
|
+
* The maintenance-outbox row is recorded ATOMICALLY with the source write, so the
|
|
1108
|
+
* intent can never be lost ("wrote the row, lost the maintenance" is impossible). The
|
|
1109
|
+
* CDC substrate then delivers it **at-least-once**, **per-shard ordered** (a shard is
|
|
1110
|
+
* `hash(pk)`, and the outbox row's `pk` is keyed off the SOURCE row's key, so all
|
|
1111
|
+
* maintenance events for one source entity land on one shard in source-write order),
|
|
1112
|
+
* with a **DLQ** for poison events. This consumer is therefore written to be
|
|
1113
|
+
* **idempotent / commutative** wherever at-least-once redelivery could double-apply:
|
|
1114
|
+
*
|
|
1115
|
+
* - a `snapshot` `SET` is naturally idempotent (re-applying the same projection is a
|
|
1116
|
+
* no-op);
|
|
1117
|
+
* - a `collection` rebuild de-duplicates by the projected item's identity key, so a
|
|
1118
|
+
* redelivered append does not grow the list twice;
|
|
1119
|
+
* - a `max` conditional `SET` is idempotent (the guard rejects a non-greater value);
|
|
1120
|
+
* - a `count` `ADD` is the ONE non-idempotent op — but the source-row create guard
|
|
1121
|
+
* (`attribute_not_exists`) means a given source `created` event is emitted at most
|
|
1122
|
+
* once into the outbox, and the per-shard checkpoint advances only over acked events,
|
|
1123
|
+
* so a `count` is applied once per genuine source lifecycle. (A duplicate DELIVERY of
|
|
1124
|
+
* the same event is folded by the consumer's per-event de-dup set within a drain.)
|
|
1125
|
+
*/
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* The PK prefix a maintenance-outbox row keys on — the single source of truth shared
|
|
1129
|
+
* with the compiler ({@link import('../spec/mutation-command.js')}'s
|
|
1130
|
+
* `MAINT_OUTBOX_PK_PREFIX`). The drain selects its events by `keys.pk.startsWith` of
|
|
1131
|
+
* this. Kept as a local literal (rather than importing a non-exported compiler const)
|
|
1132
|
+
* so the cdc module does not depend on the spec module; the value is asserted equal by
|
|
1133
|
+
* the integration tests that round-trip a real stream maintainer through both.
|
|
1134
|
+
*/
|
|
1135
|
+
declare const MAINT_OUTBOX_PK_PREFIX = "OUTBOX#MAINT#";
|
|
1136
|
+
/** Options for {@link createMaintenanceDrain}. */
|
|
1137
|
+
interface MaintenanceDrainOptions {
|
|
1138
|
+
/**
|
|
1139
|
+
* The model classes (or `ModelStatic`s) whose stream maintainers this drain
|
|
1140
|
+
* applies. The drain builds a scoped {@link MaintenanceGraph} over them to resolve
|
|
1141
|
+
* each maintenance-outbox event back to its declared effect. Defaults to the GLOBAL
|
|
1142
|
+
* registry when omitted.
|
|
1143
|
+
*/
|
|
1144
|
+
readonly models?: readonly Function[];
|
|
1145
|
+
/**
|
|
1146
|
+
* The {@link ViewDefinition}s (`defineView`) whose maintainers this drain applies
|
|
1147
|
+
* (issue #132). A view's destination is its dedicated view model; its source
|
|
1148
|
+
* classes should also be in {@link models} so the drain can resolve their
|
|
1149
|
+
* maintenance-outbox events. Defaults to none.
|
|
1150
|
+
*/
|
|
1151
|
+
readonly views?: readonly ViewDefinition[];
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* A maintenance drain: a CDC consumer ({@link ChangeHandler}) that applies the
|
|
1155
|
+
* owner-row writes for `updateMode: 'stream'` maintainers. Construct via
|
|
1156
|
+
* {@link createMaintenanceDrain}, then `subscribe` its {@link handler} to a
|
|
1157
|
+
* {@link import('./emulator.js').CdcEmulator} (dev/test) or wire it into a real
|
|
1158
|
+
* Streams Lambda (production).
|
|
1159
|
+
*/
|
|
1160
|
+
declare class MaintenanceDrain {
|
|
1161
|
+
private readonly graph;
|
|
1162
|
+
/** Owner entity name → its model class (for the owner-row key derivation). */
|
|
1163
|
+
private readonly ownerByName;
|
|
1164
|
+
/** Per-event-id de-dup within a single handler invocation (at-least-once). */
|
|
1165
|
+
private appliedSeq;
|
|
1166
|
+
/** Count of owner-row writes applied (test/diagnostic). */
|
|
1167
|
+
private applied;
|
|
1168
|
+
constructor(opts?: MaintenanceDrainOptions);
|
|
1169
|
+
/** The CDC {@link ChangeHandler} to subscribe to an emulator / Streams source. */
|
|
1170
|
+
readonly handler: ChangeHandler;
|
|
1171
|
+
/** Resolve one maintenance-outbox event to its effect and apply the owner-row write. */
|
|
1172
|
+
private applyOne;
|
|
1173
|
+
/** Resolve a maintenance-outbox event back to the declared {@link MaintainItem}. */
|
|
1174
|
+
private resolve;
|
|
1175
|
+
/** A `snapshot` SET of each projected attribute onto the owner row (idempotent). */
|
|
1176
|
+
private applySnapshot;
|
|
1177
|
+
/**
|
|
1178
|
+
* A sparse-view **membership** write (#133): evaluate the membership predicate against
|
|
1179
|
+
* the source image; PUT the view row (its projection + key fields) when the predicate
|
|
1180
|
+
* holds, DELETE it when it flips false. A source `removed` event always deletes (the
|
|
1181
|
+
* source no longer exists, so its view row must disappear regardless of the predicate).
|
|
1182
|
+
*
|
|
1183
|
+
* Both ops are idempotent under at-least-once redelivery: a repeated PUT writes the same
|
|
1184
|
+
* row, a repeated DELETE is a no-op. The view row is keyed by the source identity
|
|
1185
|
+
* (`effect.keys`), so the PUT/DELETE always targets the SAME physical row the predicate
|
|
1186
|
+
* gates.
|
|
1187
|
+
*/
|
|
1188
|
+
private applyMembership;
|
|
1189
|
+
/**
|
|
1190
|
+
* A running `max`: read the owner row, and `SET` the attribute to the source value
|
|
1191
|
+
* only when it is greater (or absent). Implemented as a conditional update so a
|
|
1192
|
+
* redelivered / out-of-order older value never regresses the stored max — idempotent
|
|
1193
|
+
* and commutative under at-least-once delivery.
|
|
1194
|
+
*/
|
|
1195
|
+
private applyMax;
|
|
1196
|
+
/**
|
|
1197
|
+
* A bounded `collection`: read the current list, apply the event (append for a
|
|
1198
|
+
* `created`/`updated` source, splice for a `removed`), de-duplicate by the projected
|
|
1199
|
+
* identity key, order by `orderBy` (in `orderDir`, default DESC), trim to `maxItems`, and write the whole
|
|
1200
|
+
* list back. The read-modify-write is exactly what a single synchronous
|
|
1201
|
+
* `UpdateExpression` cannot do — the reason a bounded/ordered collection is a stream
|
|
1202
|
+
* maintainer (Phase 1 sync was append-only).
|
|
1203
|
+
*/
|
|
1204
|
+
private applyCollection;
|
|
1205
|
+
/** Owner-row writes applied so far (test/diagnostic). */
|
|
1206
|
+
appliedCount(): number;
|
|
1207
|
+
/** Clear the per-delivery de-dup set + counters (test reset). */
|
|
1208
|
+
reset(): void;
|
|
1209
|
+
}
|
|
1210
|
+
/** Create a {@link MaintenanceDrain} (issue #130). */
|
|
1211
|
+
declare function createMaintenanceDrain(opts?: MaintenanceDrainOptions): MaintenanceDrain;
|
|
1212
|
+
/**
|
|
1213
|
+
* Create just the CDC {@link ChangeHandler} for a maintenance drain — the form to
|
|
1214
|
+
* subscribe to an emulator or wire into a production Streams Lambda. A thin wrapper
|
|
1215
|
+
* over {@link createMaintenanceDrain} for callers that only need the handler.
|
|
1216
|
+
*/
|
|
1217
|
+
declare function createMaintenanceDrainHandler(opts?: MaintenanceDrainOptions): ChangeHandler;
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* External-projection **sink drain** (issue #132) — the asynchronous runtime for
|
|
1221
|
+
* `defineProjection` (`externalProjection`, Epic #118 §5.3 / RFC §4.A.8).
|
|
1222
|
+
*
|
|
1223
|
+
* A `defineProjection` declares a stream + idempotency projection of a source
|
|
1224
|
+
* entity into an EXTERNAL sink (BigQuery / S3 / OpenSearch / Neptune in
|
|
1225
|
+
* production). The transport is the CDC stream: the source entity's writes surface
|
|
1226
|
+
* as {@link ChangeEvent}s (real DynamoDB Streams in production, the
|
|
1227
|
+
* {@link import('./emulator.js').CdcEmulator} in dev/test), and THIS consumer
|
|
1228
|
+
* projects each event's source image and upserts it into the sink.
|
|
1229
|
+
*
|
|
1230
|
+
* ## At-least-once → exactly-once-effect via the idempotency key
|
|
1231
|
+
*
|
|
1232
|
+
* The CDC substrate delivers at-least-once (a single source event may be delivered
|
|
1233
|
+
* more than once). An external sink upsert is the ONE place a duplicate would
|
|
1234
|
+
* double-write, so the projection folds redelivery by the declared
|
|
1235
|
+
* `idempotencyKey` (a source attribute, e.g. `resultId`): the sink is an UPSERT
|
|
1236
|
+
* keyed by that value, so re-delivering the same source event overwrites the same
|
|
1237
|
+
* sink record rather than appending a second. This is the standard "make
|
|
1238
|
+
* at-least-once idempotent with a business key" pattern; the sink need only
|
|
1239
|
+
* support an idempotent put keyed by the idempotency key.
|
|
1240
|
+
*
|
|
1241
|
+
* ## The sink abstraction (dev/test vs production)
|
|
1242
|
+
*
|
|
1243
|
+
* {@link ProjectionSink} is the minimal contract a real sink adapter implements
|
|
1244
|
+
* (`upsert(key, record)` / `delete(key)`). Production wires a BigQuery / S3 /
|
|
1245
|
+
* OpenSearch adapter; dev/test uses {@link InMemoryProjectionSink}, an in-process
|
|
1246
|
+
* map that the integration tests assert convergence + idempotency against. The
|
|
1247
|
+
* drain itself is sink-agnostic — it resolves the projection, computes the key,
|
|
1248
|
+
* and calls the adapter.
|
|
1249
|
+
*/
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* The minimal contract a projection sink adapter implements. An upsert is keyed by
|
|
1253
|
+
* the idempotency key (so a redelivery overwrites the same record), a delete
|
|
1254
|
+
* removes it (for a source `removed` event projected to the sink).
|
|
1255
|
+
*/
|
|
1256
|
+
interface ProjectionSink {
|
|
1257
|
+
/** Upsert the projected record under the idempotency key (idempotent). */
|
|
1258
|
+
upsert(key: string, record: Record<string, unknown>): void | Promise<void>;
|
|
1259
|
+
/** Remove the record under the idempotency key (for a projected `removed`). */
|
|
1260
|
+
delete?(key: string): void | Promise<void>;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* An in-process {@link ProjectionSink} for dev/test — a map keyed by the
|
|
1264
|
+
* idempotency key. Mirrors what a real sink (BigQuery / S3) does logically: an
|
|
1265
|
+
* upsert keyed by a business key folds at-least-once redelivery to one record. The
|
|
1266
|
+
* integration tests assert convergence + idempotency against {@link records}.
|
|
1267
|
+
*/
|
|
1268
|
+
declare class InMemoryProjectionSink implements ProjectionSink {
|
|
1269
|
+
/** The current sink contents, keyed by the idempotency key. */
|
|
1270
|
+
readonly records: Map<string, Record<string, unknown>>;
|
|
1271
|
+
/** Total upsert CALLS (including redelivered ones) — distinguishes calls from records. */
|
|
1272
|
+
private upserts;
|
|
1273
|
+
upsert(key: string, record: Record<string, unknown>): void;
|
|
1274
|
+
delete(key: string): void;
|
|
1275
|
+
/** The number of distinct sink records (the converged projection size). */
|
|
1276
|
+
size(): number;
|
|
1277
|
+
/** The total upsert calls (≥ size when redelivery occurred). */
|
|
1278
|
+
upsertCount(): number;
|
|
1279
|
+
/** Reset the sink (test). */
|
|
1280
|
+
reset(): void;
|
|
1281
|
+
}
|
|
1282
|
+
/** Options for {@link createProjectionSinkDrain}. */
|
|
1283
|
+
interface ProjectionSinkDrainOptions {
|
|
1284
|
+
/** The {@link ProjectionDefinition}s (`defineProjection`) this drain applies. */
|
|
1285
|
+
readonly projections: readonly ProjectionDefinition[];
|
|
1286
|
+
/** The external sink adapter the projected records are upserted into. */
|
|
1287
|
+
readonly sink: ProjectionSink;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* A projection sink drain (issue #132): a CDC consumer ({@link ChangeHandler}) that
|
|
1291
|
+
* projects each source event into the external sink, folding at-least-once delivery
|
|
1292
|
+
* by the projection's idempotency key. Construct via
|
|
1293
|
+
* {@link createProjectionSinkDrain}, then subscribe its {@link handler} to a
|
|
1294
|
+
* {@link import('./emulator.js').CdcEmulator} (dev/test) or a real Streams Lambda.
|
|
1295
|
+
*/
|
|
1296
|
+
declare class ProjectionSinkDrain {
|
|
1297
|
+
private readonly sink;
|
|
1298
|
+
/** Source-entity CLASS name (the `event.model` the capture seam records) → projections. */
|
|
1299
|
+
private readonly bySource;
|
|
1300
|
+
/** Per-(shard,seq) de-dup within one handler invocation (at-least-once). */
|
|
1301
|
+
private applied;
|
|
1302
|
+
constructor(opts: ProjectionSinkDrainOptions);
|
|
1303
|
+
/** The CDC {@link ChangeHandler} to subscribe to an emulator / Streams source. */
|
|
1304
|
+
readonly handler: ChangeHandler;
|
|
1305
|
+
/** Project one source event into the sink for every matching projection. */
|
|
1306
|
+
private applyOne;
|
|
1307
|
+
/** Clear the per-delivery de-dup set (test reset). */
|
|
1308
|
+
reset(): void;
|
|
1309
|
+
}
|
|
1310
|
+
/** Create a {@link ProjectionSinkDrain} (issue #132). */
|
|
1311
|
+
declare function createProjectionSinkDrain(opts: ProjectionSinkDrainOptions): ProjectionSinkDrain;
|
|
1312
|
+
/**
|
|
1313
|
+
* Create just the CDC {@link ChangeHandler} for a projection sink drain — the form
|
|
1314
|
+
* to subscribe to an emulator or wire into a production Streams Lambda.
|
|
1315
|
+
*/
|
|
1316
|
+
declare function createProjectionSinkHandler(opts: ProjectionSinkDrainOptions): ChangeHandler;
|
|
1317
|
+
|
|
1044
1318
|
/**
|
|
1045
1319
|
* Per-key cursor envelope for batched `range` (`list`) contract methods (issue
|
|
1046
1320
|
* #62, CQRS single-service runtime; spec `docs/cqrs-contract.md`,
|
|
@@ -1125,10 +1399,14 @@ declare function decodePerKeyCursor(cursor: string, expectedKey: Record<string,
|
|
|
1125
1399
|
/**
|
|
1126
1400
|
* Creates a Linter pre-loaded with rules safe for Entity registration.
|
|
1127
1401
|
*
|
|
1128
|
-
* Rules included: no-scan, require-limit, gsi-ambiguity, missing-gsi,
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1402
|
+
* Rules included: no-scan, require-limit, gsi-ambiguity, missing-gsi,
|
|
1403
|
+
* relation-depth, same-partition-preset, plus the Epic #118 Phase 1 maintenance
|
|
1404
|
+
* validators (issue #126): missing-context, 400kb, hot-partition, fan-out,
|
|
1405
|
+
* multi-maintainer-same-row. The maintenance validators are no-ops for any
|
|
1406
|
+
* relation that does not declare `write.maintainedOn`, so unannotated models are
|
|
1407
|
+
* unaffected. query-boundary is excluded because it flags non-unique GSIs which
|
|
1408
|
+
* are valid for list() operations; boundary enforcement is handled at runtime by
|
|
1409
|
+
* the planner's boundary-check.
|
|
1132
1410
|
*/
|
|
1133
1411
|
declare function createDefaultLinter(): Linter;
|
|
1134
1412
|
|
|
@@ -1302,12 +1580,42 @@ type EdgeLifecycle = 'onCreate' | 'onDelete' | 'onUpdateKeyChange';
|
|
|
1302
1580
|
* lifecycle-agnostic — the *same* edge is created on create, deleted on delete,
|
|
1303
1581
|
* and moved on a foreign-key change; {@link deriveEdgeWriteItems} fans it out
|
|
1304
1582
|
* into the three lifecycle forms.
|
|
1583
|
+
*
|
|
1584
|
+
* ## `inverse` — dual-edge synchronization (Epic #118 pattern 9, issue #133)
|
|
1585
|
+
*
|
|
1586
|
+
* The plain edge above is **one physical row** that serves *both* read directions:
|
|
1587
|
+
* the base PK reads one direction and an inverse GSI (auto-maintained by DynamoDB)
|
|
1588
|
+
* reads the other, so a bidirectional relation costs a single write. The RFC §1
|
|
1589
|
+
* pattern-9 gap is the case where the inverse direction is NOT served by that GSI
|
|
1590
|
+
* but by a **second physical row** on its own partition — the genuine *dual edge*
|
|
1591
|
+
* (forward + inverse rows). {@link inverse}, when present, names a SECOND adjacency
|
|
1592
|
+
* model + relation property whose row is the inverse edge; {@link
|
|
1593
|
+
* deriveEdgeWriteItemsFor} then maintains BOTH rows from this single (one-direction)
|
|
1594
|
+
* declaration — the create `Put`s both, the delete `Delete`s both, a foreign-key
|
|
1595
|
+
* change `Delete`s+`Put`s both — so the two directions can never drift. Omit
|
|
1596
|
+
* `inverse` for the historical single-row-plus-GSI bidirectional edge.
|
|
1305
1597
|
*/
|
|
1306
1598
|
interface EdgeWriteDeclaration {
|
|
1307
1599
|
/** Resolves the target model whose relation this edge feeds (e.g. `GroupModel`). */
|
|
1308
1600
|
readonly targetFactory: () => new (...args: unknown[]) => unknown;
|
|
1309
1601
|
/** The relation property on the target that reads this edge (e.g. `members`). */
|
|
1310
1602
|
readonly relationProperty: string;
|
|
1603
|
+
/**
|
|
1604
|
+
* The INVERSE edge to keep in sync (Epic #118 pattern 9 / issue #133, dual-edge).
|
|
1605
|
+
* Names the second adjacency model whose own row materializes the reverse-direction
|
|
1606
|
+
* relation. When present, every lifecycle derivation maintains the inverse row
|
|
1607
|
+
* alongside the forward row in the SAME transaction, so a one-direction declaration
|
|
1608
|
+
* keeps a two-row bidirectional edge consistent. Absent = the historical single-row
|
|
1609
|
+
* (base + inverse GSI) bidirectional edge.
|
|
1610
|
+
*/
|
|
1611
|
+
readonly inverse?: {
|
|
1612
|
+
/** Resolves the inverse adjacency model whose row IS the inverse edge. */
|
|
1613
|
+
readonly adjacencyFactory: () => new (...args: unknown[]) => unknown;
|
|
1614
|
+
/** Resolves the target model whose relation the inverse row feeds. */
|
|
1615
|
+
readonly targetFactory: () => new (...args: unknown[]) => unknown;
|
|
1616
|
+
/** The relation property on the inverse target that reads the inverse row. */
|
|
1617
|
+
readonly relationProperty: string;
|
|
1618
|
+
};
|
|
1311
1619
|
}
|
|
1312
1620
|
/**
|
|
1313
1621
|
* Marker carried by a model's static `writes` member ({@link edgeWrites}), so it
|
|
@@ -1348,6 +1656,34 @@ interface EdgeWriteRecorder {
|
|
|
1348
1656
|
* `relationProperty` relation (→ adjacency `Delete`(old) + `Put`(new)).
|
|
1349
1657
|
*/
|
|
1350
1658
|
updateKeyChangeEdge(target: () => new (...args: unknown[]) => unknown, relationProperty: string): EdgeWriteDeclaration;
|
|
1659
|
+
/**
|
|
1660
|
+
* Declare a **dual edge** (Epic #118 pattern 9 / issue #133): a bidirectional edge
|
|
1661
|
+
* maintained as TWO physical rows — a forward row on THIS adjacency entity (read by
|
|
1662
|
+
* `forward.target.relationProperty`) and an inverse row on a SECOND adjacency model
|
|
1663
|
+
* (read by `inverse.target.relationProperty`). Both rows are written / deleted /
|
|
1664
|
+
* moved together from this single declaration, so the two directions stay in sync
|
|
1665
|
+
* without an inverse GSI. Use this when the reverse direction lives on its own
|
|
1666
|
+
* partition (a second row) rather than a GSI of the forward row.
|
|
1667
|
+
*
|
|
1668
|
+
* @example
|
|
1669
|
+
* ```ts
|
|
1670
|
+
* // On the forward adjacency entity (FollowEdge: FOLLOWER#a / FOLLOWING#b):
|
|
1671
|
+
* static readonly writes = edgeWrites((w) => [
|
|
1672
|
+
* w.dualEdge(
|
|
1673
|
+
* { target: () => UserModel, relationProperty: 'following' },
|
|
1674
|
+
* { adjacency: () => FollowedByEdgeModel, target: () => UserModel, relationProperty: 'followers' },
|
|
1675
|
+
* ),
|
|
1676
|
+
* ]);
|
|
1677
|
+
* ```
|
|
1678
|
+
*/
|
|
1679
|
+
dualEdge(forward: {
|
|
1680
|
+
target: () => new (...args: unknown[]) => unknown;
|
|
1681
|
+
relationProperty: string;
|
|
1682
|
+
}, inverse: {
|
|
1683
|
+
adjacency: () => new (...args: unknown[]) => unknown;
|
|
1684
|
+
target: () => new (...args: unknown[]) => unknown;
|
|
1685
|
+
relationProperty: string;
|
|
1686
|
+
}): EdgeWriteDeclaration;
|
|
1351
1687
|
}
|
|
1352
1688
|
/**
|
|
1353
1689
|
* Declare an adjacency entity's edge write side (issue #82), as the model's
|
|
@@ -1419,6 +1755,182 @@ declare function getEdgeWrites(modelClass: new (...args: unknown[]) => unknown):
|
|
|
1419
1755
|
*/
|
|
1420
1756
|
declare function deriveModelEdgeWriteItems(modelClass: new (...args: unknown[]) => unknown, lifecycle: EdgeLifecycle): TransactionItemSpec[];
|
|
1421
1757
|
|
|
1758
|
+
/**
|
|
1759
|
+
* Maintenance **repair / rebuild planner** (Epic #118 Phase 2, issue #131).
|
|
1760
|
+
*
|
|
1761
|
+
* ## What this is
|
|
1762
|
+
*
|
|
1763
|
+
* The async drain ({@link import('../cdc/maintenance-drain.js')}, #130) keeps a
|
|
1764
|
+
* maintained owner row (a snapshot / collection / counter) in sync INCREMENTALLY —
|
|
1765
|
+
* it applies one source event at a time. Over a long-lived table the materialized
|
|
1766
|
+
* value can nevertheless **drift** from the source of truth: a dropped / dead-lettered
|
|
1767
|
+
* stream event, a maintainer added AFTER source rows already existed (initial
|
|
1768
|
+
* un-built state), a bug in an earlier maintainer version, or an out-of-band write.
|
|
1769
|
+
*
|
|
1770
|
+
* The rebuild planner is the repair for that drift. For a given owner row × maintainer
|
|
1771
|
+
* it **re-derives the owner-row value from scratch by scanning every source row that
|
|
1772
|
+
* belongs to that owner**, using the SAME projection / aggregation / ordering rules the
|
|
1773
|
+
* drain uses (shared via `maintenance-projection.ts` and this module), then:
|
|
1774
|
+
*
|
|
1775
|
+
* - **{@link detectDrift}** — compares the re-derived value against the value currently
|
|
1776
|
+
* stored on the owner row and reports whether (and how) they differ; or
|
|
1777
|
+
* - **{@link rebuild}** — writes the re-derived ABSOLUTE value onto the owner row, so the
|
|
1778
|
+
* materialized data converges to exactly what the source says.
|
|
1779
|
+
*
|
|
1780
|
+
* After a rebuild the owner row equals what an incremental drain would have converged to
|
|
1781
|
+
* had it never missed an event — the AC: "rebuild で materialized が source と一致する".
|
|
1782
|
+
*
|
|
1783
|
+
* ## Granularity: per owner row × maintainer
|
|
1784
|
+
*
|
|
1785
|
+
* Drift is detected and repaired at the **owner-row × maintainer** grain — the natural
|
|
1786
|
+
* unit, because one owner row's materialized value is a pure function of the set of
|
|
1787
|
+
* source rows bound to it. The caller names the owner rows to repair (their key inputs);
|
|
1788
|
+
* the planner discovers the source rows for each by querying the source on the maintenance
|
|
1789
|
+
* key binding (the same binding the live `hasMany` / `@aggregate` read uses, inverted:
|
|
1790
|
+
* the owner row supplies the binding value, the source partition is scanned for matches).
|
|
1791
|
+
*
|
|
1792
|
+
* ## count is re-derived as an ABSOLUTE SET, not an ADD
|
|
1793
|
+
*
|
|
1794
|
+
* The maintained counter applies a signed `ADD` per event (#141 / #130). A rebuild
|
|
1795
|
+
* instead **counts the source rows and SETs the absolute value** — it does not trust the
|
|
1796
|
+
* accumulated counter, it recomputes it. This is the whole point of repair (an ADD that
|
|
1797
|
+
* has drifted cannot be corrected by another ADD). The `max(field)` counter is re-derived
|
|
1798
|
+
* as the maximum over the scanned source rows and SET unconditionally (the rebuild is the
|
|
1799
|
+
* authority, so it does not use the drain's advance-only conditional guard).
|
|
1800
|
+
*
|
|
1801
|
+
* ## Execution form (production positioning)
|
|
1802
|
+
*
|
|
1803
|
+
* Like the drain applier, this is a **TS-side planner/executor** intended for dev/test and
|
|
1804
|
+
* **operational repair tooling** — run on-demand (a "repair this thread's summary now"
|
|
1805
|
+
* admin action) or as a periodic batch (a scheduled reconciliation sweep over a set of
|
|
1806
|
+
* owner rows). It is NOT injected into the SSoT spec and emits no generated artifact: a
|
|
1807
|
+
* rebuild is a host-side reconciliation against live DynamoDB, not part of a model's
|
|
1808
|
+
* compiled write contract — so there is no Python mirror (the Python runtime executes the
|
|
1809
|
+
* generated contract; rebuild is not in it). It reuses the maintenance graph (#124) to
|
|
1810
|
+
* resolve which maintainers exist and the shared re-derivation rules so a rebuilt value is
|
|
1811
|
+
* byte-consistent with a drained one.
|
|
1812
|
+
*/
|
|
1813
|
+
|
|
1814
|
+
/** A model class (or `ModelStatic`) the rebuild planner operates over. */
|
|
1815
|
+
type ModelLike = Function | ModelStatic<DDBModel>;
|
|
1816
|
+
/** Options for {@link createMaintenanceRebuilder}. */
|
|
1817
|
+
interface MaintenanceRebuildOptions {
|
|
1818
|
+
/**
|
|
1819
|
+
* The model classes whose maintainers this rebuilder can repair. The rebuilder
|
|
1820
|
+
* builds a scoped {@link MaintenanceGraph} over them to resolve each
|
|
1821
|
+
* owner-row × maintainer back to its declared effect. Defaults to the GLOBAL
|
|
1822
|
+
* registry when omitted.
|
|
1823
|
+
*/
|
|
1824
|
+
readonly models?: readonly ModelLike[];
|
|
1825
|
+
/**
|
|
1826
|
+
* The {@link ViewDefinition}s (`defineView`) whose maintainers this rebuilder can
|
|
1827
|
+
* repair (issue #132). The view model classes are added to the scoped registry so a
|
|
1828
|
+
* view row can be re-derived from its source rows, exactly like a relation owner row.
|
|
1829
|
+
*/
|
|
1830
|
+
readonly views?: readonly ViewDefinition[];
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* The drift verdict for one owner row × maintainer: whether the currently-stored
|
|
1834
|
+
* materialized value differs from the value re-derived from the source rows, and both
|
|
1835
|
+
* values (for diagnostics / a repair preview). `expected` is the re-derived authority;
|
|
1836
|
+
* `actual` is what the owner row currently holds (`undefined` when never built).
|
|
1837
|
+
*/
|
|
1838
|
+
interface DriftReport {
|
|
1839
|
+
/** The relation / aggregate property the maintained value lives under. */
|
|
1840
|
+
readonly relationProperty: string;
|
|
1841
|
+
/** The owner entity (logical name) the row belongs to. */
|
|
1842
|
+
readonly ownerEntity: string;
|
|
1843
|
+
/** The owner row's resolved key (`{ PK, SK }`). */
|
|
1844
|
+
readonly ownerKey: Record<string, unknown>;
|
|
1845
|
+
/** The maintained attribute (collection field / snapshot attrs / counter attribute). */
|
|
1846
|
+
readonly kind: MaintainEffect['kind'];
|
|
1847
|
+
/** True when `actual` differs from `expected` (drift present). */
|
|
1848
|
+
readonly drifted: boolean;
|
|
1849
|
+
/** The value re-derived from the source rows (the authority). */
|
|
1850
|
+
readonly expected: unknown;
|
|
1851
|
+
/** The value currently stored on the owner row (`undefined` when unbuilt). */
|
|
1852
|
+
readonly actual: unknown;
|
|
1853
|
+
}
|
|
1854
|
+
/** The outcome of a {@link MaintenanceRebuilder.rebuild} over one owner row × maintainer. */
|
|
1855
|
+
interface RebuildResult extends DriftReport {
|
|
1856
|
+
/** True when the owner row was written (drift was present and repaired). */
|
|
1857
|
+
readonly repaired: boolean;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* A maintenance **repair / rebuild planner** (#131). Construct via
|
|
1861
|
+
* {@link createMaintenanceRebuilder}, then call {@link detectDrift} to compare or
|
|
1862
|
+
* {@link rebuild} to repair one owner row × maintainer (resolved by the owner model +
|
|
1863
|
+
* the relation/aggregate property + the owner key inputs).
|
|
1864
|
+
*/
|
|
1865
|
+
declare class MaintenanceRebuilder {
|
|
1866
|
+
private readonly graph;
|
|
1867
|
+
constructor(opts?: MaintenanceRebuildOptions);
|
|
1868
|
+
/**
|
|
1869
|
+
* Re-derive the materialized value for one owner row × maintainer from the source
|
|
1870
|
+
* rows and report whether it differs from what the owner row currently stores. Does
|
|
1871
|
+
* NOT write anything (a read-only drift probe).
|
|
1872
|
+
*
|
|
1873
|
+
* @param owner The owner model class declaring the maintainer.
|
|
1874
|
+
* @param relationProperty The relation / `@aggregate` property the value lives under.
|
|
1875
|
+
* @param ownerKeyInput The owner row's key input (e.g. `{ threadId: 't1' }`).
|
|
1876
|
+
*/
|
|
1877
|
+
detectDrift(owner: ModelLike, relationProperty: string, ownerKeyInput: Record<string, unknown>): Promise<DriftReport>;
|
|
1878
|
+
/**
|
|
1879
|
+
* Re-derive the materialized value for one owner row × maintainer from the source
|
|
1880
|
+
* rows and WRITE the absolute re-derived value onto the owner row when it drifts (or
|
|
1881
|
+
* always, when `force`). Returns the drift verdict plus whether a repair write ran.
|
|
1882
|
+
*
|
|
1883
|
+
* @param options.force Write even when no drift is detected (default: write only on drift).
|
|
1884
|
+
*/
|
|
1885
|
+
rebuild(owner: ModelLike, relationProperty: string, ownerKeyInput: Record<string, unknown>, options?: {
|
|
1886
|
+
force?: boolean;
|
|
1887
|
+
}): Promise<RebuildResult>;
|
|
1888
|
+
/** Resolve an owner model + property to its {@link MaintainItem} + owner-key builder. */
|
|
1889
|
+
private resolve;
|
|
1890
|
+
/**
|
|
1891
|
+
* Scan every source row bound to this owner row and re-derive the maintained value:
|
|
1892
|
+
*
|
|
1893
|
+
* - **collection** — project each source row, order + trim by the SAME read-side
|
|
1894
|
+
* rules (`orderBy` / `orderDir` / `maxItems`) the drain uses, return the list;
|
|
1895
|
+
* - **snapshot** — project the single bound source row (or `undefined` when none);
|
|
1896
|
+
* - **counter `count`** — the COUNT of source rows (absolute, not an ADD);
|
|
1897
|
+
* - **counter `max`** — the maximum source value of the tracked field.
|
|
1898
|
+
*/
|
|
1899
|
+
private recompute;
|
|
1900
|
+
/**
|
|
1901
|
+
* The source rows bound to one owner row. The maintenance key binding is
|
|
1902
|
+
* `{ ownerField: '$.entity.<sourceField>' }` (the destination owner-key field ← the
|
|
1903
|
+
* source row's field). INVERTED for a rebuild: the owner row supplies the value, and
|
|
1904
|
+
* the source partition is queried for rows whose `<sourceField>` matches — exactly the
|
|
1905
|
+
* set the live `hasMany` / `@aggregate` read aggregates. Queries the source via the
|
|
1906
|
+
* same {@link plan} the read planner uses (a partition Query), paginating fully.
|
|
1907
|
+
*/
|
|
1908
|
+
private scanSourceRows;
|
|
1909
|
+
/** Re-derive a bounded collection: project, then order + trim by the read-side rules. */
|
|
1910
|
+
private recomputeCollection;
|
|
1911
|
+
/**
|
|
1912
|
+
* Re-derive a single-row snapshot. A snapshot mirrors ONE source row (a `belongsTo` /
|
|
1913
|
+
* `hasOne`, normally 1:1). In the unusual case where MORE THAN ONE source row binds, the
|
|
1914
|
+
* live single-value read takes the FIRST row in the source's natural query order
|
|
1915
|
+
* (`ScanIndexForward`, default ASC = the smallest sort-key leaf), so re-derive the same
|
|
1916
|
+
* row — {@link pickFirst} — for a verdict consistent with the live read. Returns
|
|
1917
|
+
* `undefined` when no source row is bound (the snapshot should be absent).
|
|
1918
|
+
*/
|
|
1919
|
+
private recomputeSnapshot;
|
|
1920
|
+
/** The source entity metadata a maintain effect projects from (its relation target). */
|
|
1921
|
+
private sourceMetaOf;
|
|
1922
|
+
/** Re-derive a counter: the absolute COUNT of source rows, or the MAX of the tracked field. */
|
|
1923
|
+
private recomputeCounter;
|
|
1924
|
+
/** Read the owner row (consistent) for the drift comparison / actual value. */
|
|
1925
|
+
private readOwnerRow;
|
|
1926
|
+
/** The value currently stored on the owner row for this maintainer. */
|
|
1927
|
+
private actualValue;
|
|
1928
|
+
/** Write the re-derived ABSOLUTE value onto the owner row (the repair). */
|
|
1929
|
+
private writeValue;
|
|
1930
|
+
}
|
|
1931
|
+
/** Create a {@link MaintenanceRebuilder} (issue #131). */
|
|
1932
|
+
declare function createMaintenanceRebuilder(opts?: MaintenanceRebuildOptions): MaintenanceRebuilder;
|
|
1933
|
+
|
|
1422
1934
|
/**
|
|
1423
1935
|
* Type-level machinery for the parameterized definition DSL (issue #41).
|
|
1424
1936
|
*
|
|
@@ -2290,4 +2802,4 @@ declare function assertBundleSerializable(bundle: BridgeBundle): void;
|
|
|
2290
2802
|
*/
|
|
2291
2803
|
declare function buildBridgeBundle(queries?: DefinitionMap, commands?: DefinitionMap, registry?: typeof MetadataRegistry, transactions?: Record<string, TransactionDefinition>, contractInputs?: ContractInputs): BridgeBundle;
|
|
2292
2804
|
|
|
2293
|
-
export { type AnyModelContract, AnyOperationDefinition, BATCH_GET_MAX_KEYS, BATCH_WRITE_MAX_ITEMS, BatchExecOptions, 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, CtxModel, DDBModel, DefinitionMap, DeleteInput, DynamoDBOperation, DynamoExecutor,
|
|
2805
|
+
export { AggregateOptions, AggregateValue, type AnyModelContract, AnyOperationDefinition, BATCH_GET_MAX_KEYS, BATCH_WRITE_MAX_ITEMS, BatchExecOptions, 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, CtxModel, DDBModel, DefinitionMap, DeleteInput, type DriftReport, DynamoDBOperation, DynamoExecutor, DynamoType, EDGE_WRITES_MARKER, type EdgeLifecycle, type EdgeWriteDeclaration, type EdgeWriteRecorder, type EdgeWritesDefinition, EntityMetadata, EntityRef, EventLog, ExecutionPlan, Executor, ExecutorResult, type ExplainInput, FaultSpec, FieldMetadata, FieldOptions, type FilterExpressionResult, GsiDefinition, InMemoryProjectionSink, Item, KeyDefinition, type LintResult, type LintRule, Linter, type ListInput, type ListOptions, MAINT_OUTBOX_PK_PREFIX, MAX_TRANSACT_ITEMS, MaintainEffect, MaintenanceDrain, type MaintenanceDrainOptions, type MaintenanceRebuildOptions, MaintenanceRebuilder, Manifest, MetadataRegistry, Middleware, type ModelOptions, ModelStatic, OLD_VALUE_NAMESPACE, OperationDefinition, OperationsDocument, Param, ParamDescriptor, type Parameterize, PartialQueryKeyOf, type PerKeyCursorEnvelope, type PlanInput, PrimaryKeyOf, ProjectionDefinition, type ProjectionResult, type ProjectionSink, ProjectionSinkDrain, type ProjectionSinkDrainOptions, PutInput, QueryMethodSpec, QueryModelContract, type QueryOptions$1 as QueryOptions, QuerySpec, RawCondition, ReadExecOptions, ReadParams, ReadRequestCtx, ReadRequestKind, type RebuildResult, RelationMetadata, RelationOptions, type RelationTraversalOptions, ReplayOptions, RequestContext, ResolvedKey, RetryOverride, RetryPolicy, RetryingExecutor, 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, ViewDefinition, type WhenComparison, WriteDefinitionOptions, WriteExecOptions, WriteResult, aggregate, assertBundleSerializable, assertContractBoundaries, assertContractN1Safe, assertJsonSerializable, assertSupportedCondition, belongsTo, binary, boolean, buildBridgeBundle, buildConditionExpression, buildContexts, buildContracts, buildManifest, buildOperations, buildProjection, buildQuerySpec, buildTransactionSpec, buildTransactions, buildUpdateExpression, collectContractBoundaryViolations, collectContractN1Violations, compileFilterExpression, count, createCdcEmulator, createDefaultLinter, createMaintenanceDrain, createMaintenanceDrainHandler, createMaintenanceRebuilder, createProjectionSinkDrain, createProjectionSinkHandler, 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, max, missingGsiRule, model, noScanRule, number, numberSet, plan, queryBoundaryRule, relationDepthRule, requireLimitRule, resolveKey, resolveRelations, serializeContractKey, serializeFieldValue, string, stringSet, validateDepth, validateGsiAmbiguity, when };
|