graphddb 0.4.2 → 0.5.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 +145 -476
- package/dist/{chunk-BROCT574.js → chunk-7NPG5R7O.js} +1 -1
- package/dist/{chunk-CPTV3H2U.js → chunk-FMJIWFIS.js} +156 -30
- package/dist/{chunk-G5RWWBAL.js → chunk-J5A665UW.js} +23 -3
- package/dist/cli.js +781 -6
- package/dist/index.d.ts +65 -3
- package/dist/index.js +20 -4
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types-BWOrWcbd.d.ts → types-B-F7jw9f.d.ts} +42 -0
- package/docs/cdc-emulator.md +203 -0
- package/docs/class-hydration.md +409 -0
- package/docs/cloudformation.md +294 -0
- package/docs/cqrs-contract.md +526 -0
- package/docs/design-patterns.md +521 -0
- package/docs/middleware.md +189 -0
- package/docs/mutation-command-derivation.md +356 -0
- package/docs/python-bridge.md +611 -0
- package/docs/spec.md +1626 -0
- package/docs/testing.md +265 -0
- package/package.json +6 -3
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 EntityMetadata, K as KeyDefinition, G as GsiDefinition, q as ModelKind, F as FieldOptions, r as DynamoType, s as ProjectionTransform, t as MaintainEvent, u as MembershipPredicate, v as MaintainConsistency, w as MaintainUpdateMode, x as MembershipPredicateOp, y as RelationOptions, A as AggregateOptions, z as AggregateValue, H as SelectBuilderSpec, J as RawCondition, L as TransactionSpec, N as Manifest, O as RetryOverride, Q as ExecutionPlan, V as FieldMetadata, X 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, Y as CdcEmulatorOptions, Z as ChangeHandler, _ as Unsubscribe, $ as FaultSpec, a0 as ConcurrentRecomputeRef, C as ChangeEvent, a1 as EventLog, a2 as ReplayOptions, a3 as ShardId, a4 as ViewDefinition, a5 as RelationMetadata, a6 as TransactionItemSpec, a7 as MaintainEffect, M as ModelStatic, f as DDBModel, a8 as Param, a9 as ParamDescriptor, aa as DefinitionMap, ab as OperationDefinition, ac as WriteDefinitionOptions, ad as PartialQueryKeyOf, ae as StrictSelectSpec, af as ReadDefinitionOptions, ag as EntityInput, ah as UniqueQueryKeyOf, ai as EntityRef, aj as ConditionInput, ak as QueryModelContract, al as QueryMethodSpec, am as CommandModelContract, an as CommandMethodSpec, ao as ContractSpec, ap as QuerySpec, aq as CommandSpec, ar as ContextSpec, as as OperationsDocument, at as AnyOperationDefinition, au as BridgeBundle, av as ConditionSpec } from './types-
|
|
2
|
-
export { aw as AggregateMetadata, ax as BatchDeleteRequest, ay as BatchGetOptions, az as BatchGetRequest, aA as BatchGetResult, aB as BatchPutRequest, aC as BatchResult, aD as BatchWriteRequest, aE as CONTRACT_RANGE_FANOUT_CONCURRENCY, aF as CdcMode, aG as Change, aH as ChangeBatch, aI as ChangeEventName, aJ as ClockMode, aK as CollectionEffect, aL as CollectionOptions, aM as Column, aN as ColumnMap, aO as CommandContractMethodSpec, aP as CommandInputShape, aQ as CommandMethod, aR as CommandPlan, aS as CommandResolutionTarget, aT as CommandResultKind, aU as CommandSelectShape, aV as CompiledFragment, aW as ComposeSpec, aX as CondSlot, aY as ConditionCheckInput, aZ as Connection, a_ as ContractCallSignature, a$ as ContractCardinality, b0 as ContractCommandParams, b1 as ContractCommandResult, b2 as ContractComposeNode, b3 as ContractFromRef, b4 as ContractInputArity, b5 as ContractItem, b6 as ContractKeyFieldRef, b7 as ContractKeyInput, b8 as ContractKeyRef, b9 as ContractKeySpec, ba as ContractKind, bb as ContractMethodOp, bc as ContractParamRef, bd as ContractQueryParams, be as ContractResolution, bf as CounterAggregate, bg as CounterEffect, bh as CtxBase, bi as DEFAULT_MAX_ATTEMPTS, bj as DEFAULT_RETRY_POLICY, bk as DeleteOptions, bl as DeriveEffect, bm as DerivedEdgeWrite, bn as DerivedUpdate, bo as DescriptorBinding, bp as ENTITY_WRITES_MARKER, bq as EdgeEffect, br as EffectPath, bs as EmbeddedMetadata, bt as EmitEffect, bu as EntityWritesDefinition, bv as EntityWritesShape, bw as ExecutableCommandContract, bx as ExecutableQueryContract, by as FilterInput, bz as FilterSpec, bA as FragmentInput, bB as GsiDefinitionMarker, bC as GsiOptions, bD as IdempotencyEffect, bE as InProcessWriteDescriptor, bF as InputArity, bG as KeyDefinitionMarker, bH as KeySegment, bI as KeySlot, bJ as KeyStructure, bK as KeyedResult, bL as LIFECYCLE_CONTRACT_MARKER, bM as LifecycleContract, bN as LifecycleEffects, bO as LiteralParam, bP as MaintainItem, bQ as MaintainTrigger, bR as MaintenanceGraph, bS as ManifestEntity, bT as ManifestField, bU as ManifestFieldType, bV as ManifestGsi, bW as ManifestKey, bX as ManifestRelation, bY as ManifestTable, bZ as MembershipEffect, b_ as ModelRef, b$ as MutateMode, c0 as MutateOptions, c1 as MutateParallelResult, c2 as MutateTransactionResult, c3 as MutationBody, c4 as MutationDescriptorMap, c5 as MutationFragment, c6 as MutationInputProxy, c7 as MutationInputRef, c8 as MutationIntent, c9 as NumberParam, ca as OperationKind, cb as OperationSpec, cc as ParallelOpResult, cd as ParamKind, ce as ParamSpec, cf as ParamStructure, cg as PersistCtx, ch as PersistOrigin, ci as PlannedCommandMethod, cj as ProjectionMap, ck as ProjectionTransformOp, cl as PutOptions, cm as QueryContractMethodSpec, cn as QueryEnvelopeResult, co as QueryKeyOf, cp as QueryMethod, cq as QueryResult, cr as RangeConditionSpec, cs as ReadEnvelope, ct as ReadOpCtx, cu as ReadOpKind, cv as ReadOperationType, cw as ReadRouteDescriptor, cx as ReadRouteOptions, cy as ReadRouteResult, cz as RecordedCompose, cA as RelationBuilder, cB as RelationConsistency, cC as RelationLimitOptions, cD as RelationPattern, cE as RelationProjection, cF as RelationReadOptions, cG as RelationSelect, cH as RelationSpec, cI as RelationUpdateMode, cJ as RelationWriteOptions, cK as RequiresEffect, cL as Resolution, cM as RetryInfo, cN as RetryOperationKind, cO as SPEC_VERSION, cP as SegmentSpec, cQ as SegmentedKey, cR as SelectBuilder, cS as SelectOf, cT as SnapshotEffect, cU as StartingPosition, cV as StreamViewType, cW as StringParam, cX as TransactionContext, cY as TransactionItemType, cZ as UniqueEffect, c_ as Updatable, c$ as UpdateOptions, d0 as ViewSourceSlice, d1 as WhenSpec, d2 as WriteCtx, d3 as WriteDescriptor, d4 as WriteEnvelope, d5 as WriteInput, d6 as WriteKind, d7 as WriteLifecyclePhase, d8 as WriteMiddleware, d9 as WriteOperationType, da as WriteRecorder, db as WriteResultProjection, dc as attachModelClass, dd as buildDeleteInput, de as buildMaintenanceGraph, df as buildPutInput, dg as buildUpdateInput, dh as collectViewDefinitions, di as compileFragment, dj as compileMutationPlan, dk as compileSingleFragmentPlan, dl as cond, dm as contractOfMethodSpec, dn as definePlan, dp as entityWrites, dq as executeBatchGet, dr as executeBatchWrite, ds as executeCommandMethod, dt as executeDelete, du as executeKeyedBatchGet, dv as executePut, dw as executeQueryMethod, dx as executeRangeFanout, dy as executeTransaction, dz as executeUpdate, dA as from, dB as getEntityWrites, dC as gsi, dD as identity, dE as isColumn, dF as isCommandModelContract, dG as isCommandPlan, dH as isContractComposeNode, dI as isContractFromRef, dJ as isContractKeyFieldRef, dK as isContractKeyRef, dL as isContractParamRef, dM as isEntityWritesDefinition, dN as isKeySegment, dO as isLifecycleContract, dP as isMaintainTrigger, dQ as isMutationFragment, dR as isMutationInputRef, dS as isParam, dT as isPlannedCommandMethod, dU as isQueryModelContract, dV as isRetryableError, dW as isRetryableTransactionCancellation, dX as k, dY as key, dZ as lifecyclePhaseForIntent, d_ as maintainTrigger, d$ as mintContractKeyFieldRef, e0 as mintContractParamRef, e1 as mutation, e2 as param, e3 as preview, e4 as publicCommandModel, e5 as publicQueryModel, e6 as query, e7 as resolveLifecycle, e8 as wholeKeysSentinel } from './types-
|
|
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, q as ModelKind, F as FieldOptions, r as DynamoType, s as ProjectionTransform, t as MaintainEvent, u as MembershipPredicate, v as MaintainConsistency, w as MaintainUpdateMode, x as MembershipPredicateOp, y as RelationOptions, A as AggregateOptions, z as AggregateValue, H as SelectBuilderSpec, J as RawCondition, L as TransactionSpec, N as Manifest, O as RetryOverride, Q as ExecutionPlan, V as FieldMetadata, X 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, Y as CdcEmulatorOptions, Z as ChangeHandler, _ as Unsubscribe, $ as FaultSpec, a0 as ConcurrentRecomputeRef, C as ChangeEvent, a1 as EventLog, a2 as ReplayOptions, a3 as ShardId, a4 as ViewDefinition, a5 as RelationMetadata, a6 as TransactionItemSpec, a7 as MaintainEffect, M as ModelStatic, f as DDBModel, a8 as Param, a9 as ParamDescriptor, aa as DefinitionMap, ab as OperationDefinition, ac as WriteDefinitionOptions, ad as PartialQueryKeyOf, ae as StrictSelectSpec, af as ReadDefinitionOptions, ag as EntityInput, ah as UniqueQueryKeyOf, ai as EntityRef, aj as ConditionInput, ak as QueryModelContract, al as QueryMethodSpec, am as CommandModelContract, an as CommandMethodSpec, ao as ContractSpec, ap as QuerySpec, aq as CommandSpec, ar as ContextSpec, as as OperationsDocument, at as AnyOperationDefinition, au as BridgeBundle, av as ConditionSpec } from './types-B-F7jw9f.js';
|
|
2
|
+
export { aw as AggregateMetadata, ax as BatchDeleteRequest, ay as BatchGetOptions, az as BatchGetRequest, aA as BatchGetResult, aB as BatchPutRequest, aC as BatchResult, aD as BatchWriteRequest, aE as CONTRACT_RANGE_FANOUT_CONCURRENCY, aF as CdcMode, aG as Change, aH as ChangeBatch, aI as ChangeEventName, aJ as ClockMode, aK as CollectionEffect, aL as CollectionOptions, aM as Column, aN as ColumnMap, aO as CommandContractMethodSpec, aP as CommandInputShape, aQ as CommandMethod, aR as CommandPlan, aS as CommandResolutionTarget, aT as CommandResultKind, aU as CommandSelectShape, aV as CompiledFragment, aW as ComposeSpec, aX as CondSlot, aY as ConditionCheckInput, aZ as Connection, a_ as ContractCallSignature, a$ as ContractCardinality, b0 as ContractCommandParams, b1 as ContractCommandResult, b2 as ContractComposeNode, b3 as ContractFromRef, b4 as ContractInputArity, b5 as ContractItem, b6 as ContractKeyFieldRef, b7 as ContractKeyInput, b8 as ContractKeyRef, b9 as ContractKeySpec, ba as ContractKind, bb as ContractMethodOp, bc as ContractParamRef, bd as ContractQueryParams, be as ContractResolution, bf as CounterAggregate, bg as CounterEffect, bh as CtxBase, bi as DEFAULT_MAX_ATTEMPTS, bj as DEFAULT_RETRY_POLICY, bk as DeleteOptions, bl as DeriveEffect, bm as DerivedEdgeWrite, bn as DerivedUpdate, bo as DescriptorBinding, bp as ENTITY_WRITES_MARKER, bq as EdgeEffect, br as EffectPath, bs as EmbeddedMetadata, bt as EmitEffect, bu as EntityWritesDefinition, bv as EntityWritesShape, bw as ExecutableCommandContract, bx as ExecutableQueryContract, by as FilterInput, bz as FilterSpec, bA as FragmentInput, bB as GsiDefinitionMarker, bC as GsiOptions, bD as IdempotencyEffect, bE as InProcessWriteDescriptor, bF as InputArity, bG as KeyDefinitionMarker, bH as KeySegment, bI as KeySlot, bJ as KeyStructure, bK as KeyedResult, bL as LIFECYCLE_CONTRACT_MARKER, bM as LifecycleContract, bN as LifecycleEffects, bO as LiteralParam, bP as MaintainItem, bQ as MaintainTrigger, bR as MaintenanceGraph, bS as ManifestEntity, bT as ManifestField, bU as ManifestFieldType, bV as ManifestGsi, bW as ManifestKey, bX as ManifestRelation, bY as ManifestTable, bZ as MembershipEffect, b_ as ModelRef, b$ as MutateMode, c0 as MutateOptions, c1 as MutateParallelResult, c2 as MutateTransactionResult, c3 as MutationBody, c4 as MutationDescriptorMap, c5 as MutationFragment, c6 as MutationInputProxy, c7 as MutationInputRef, c8 as MutationIntent, c9 as NumberParam, ca as OperationKind, cb as OperationSpec, cc as ParallelOpResult, cd as ParamKind, ce as ParamSpec, cf as ParamStructure, cg as PersistCtx, ch as PersistOrigin, ci as PlannedCommandMethod, cj as ProjectionMap, ck as ProjectionTransformOp, cl as PutOptions, cm as QueryContractMethodSpec, cn as QueryEnvelopeResult, co as QueryKeyOf, cp as QueryMethod, cq as QueryResult, cr as RangeConditionSpec, cs as ReadEnvelope, ct as ReadOpCtx, cu as ReadOpKind, cv as ReadOperationType, cw as ReadRouteDescriptor, cx as ReadRouteOptions, cy as ReadRouteResult, cz as RecordedCompose, cA as RelationBuilder, cB as RelationConsistency, cC as RelationLimitOptions, cD as RelationPattern, cE as RelationProjection, cF as RelationReadOptions, cG as RelationSelect, cH as RelationSpec, cI as RelationUpdateMode, cJ as RelationWriteOptions, cK as RequiresEffect, cL as Resolution, cM as RetryInfo, cN as RetryOperationKind, cO as SPEC_VERSION, cP as SegmentSpec, cQ as SegmentedKey, cR as SelectBuilder, cS as SelectOf, cT as SnapshotEffect, cU as StartingPosition, cV as StreamViewType, cW as StringParam, cX as TransactionContext, cY as TransactionItemType, cZ as UniqueEffect, c_ as Updatable, c$ as UpdateOptions, d0 as ViewSourceSlice, d1 as WhenSpec, d2 as WriteCtx, d3 as WriteDescriptor, d4 as WriteEnvelope, d5 as WriteInput, d6 as WriteKind, d7 as WriteLifecyclePhase, d8 as WriteMiddleware, d9 as WriteOperationType, da as WriteRecorder, db as WriteResultProjection, dc as attachModelClass, dd as buildDeleteInput, de as buildMaintenanceGraph, df as buildPutInput, dg as buildUpdateInput, dh as collectViewDefinitions, di as compileFragment, dj as compileMutationPlan, dk as compileSingleFragmentPlan, dl as cond, dm as contractOfMethodSpec, dn as definePlan, dp as entityWrites, dq as executeBatchGet, dr as executeBatchWrite, ds as executeCommandMethod, dt as executeDelete, du as executeKeyedBatchGet, dv as executePut, dw as executeQueryMethod, dx as executeRangeFanout, dy as executeTransaction, dz as executeUpdate, dA as from, dB as getEntityWrites, dC as gsi, dD as identity, dE as isColumn, dF as isCommandModelContract, dG as isCommandPlan, dH as isContractComposeNode, dI as isContractFromRef, dJ as isContractKeyFieldRef, dK as isContractKeyRef, dL as isContractParamRef, dM as isEntityWritesDefinition, dN as isKeySegment, dO as isLifecycleContract, dP as isMaintainTrigger, dQ as isMutationFragment, dR as isMutationInputRef, dS as isParam, dT as isPlannedCommandMethod, dU as isQueryModelContract, dV as isRetryableError, dW as isRetryableTransactionCancellation, dX as k, dY as key, dZ as lifecyclePhaseForIntent, d_ as maintainTrigger, d$ as mintContractKeyFieldRef, e0 as mintContractParamRef, e1 as mutation, e2 as param, e3 as preview, e4 as publicCommandModel, e5 as publicQueryModel, e6 as query, e7 as resolveLifecycle, e8 as wholeKeysSentinel } from './types-B-F7jw9f.js';
|
|
3
3
|
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
4
4
|
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
5
5
|
|
|
@@ -356,6 +356,19 @@ declare class MetadataRegistry {
|
|
|
356
356
|
static get(target: Function): EntityMetadata;
|
|
357
357
|
static has(target: Function): boolean;
|
|
358
358
|
static getAll(): Map<Function, EntityMetadata>;
|
|
359
|
+
/**
|
|
360
|
+
* Snapshot of the entities that are **already finalized**, WITHOUT triggering
|
|
361
|
+
* finalize on any pending entity (unlike {@link getAll}). This is the safe view
|
|
362
|
+
* for a cross-entity lint rule that runs *inside* `finalize`: eagerly
|
|
363
|
+
* finalizing the rest of the registry from within a finalize pass would
|
|
364
|
+
* re-enter (and duplicate) the in-progress entity's finalize. A whole-registry
|
|
365
|
+
* rule instead reasons over the already-finalized peers plus the entity
|
|
366
|
+
* currently being checked; any conflicting pair is caught when the later of the
|
|
367
|
+
* two finalizes (both are present by then).
|
|
368
|
+
*
|
|
369
|
+
* @internal — for cross-entity linter rules.
|
|
370
|
+
*/
|
|
371
|
+
static getFinalized(): Map<Function, EntityMetadata>;
|
|
359
372
|
static get linter(): Linter;
|
|
360
373
|
static set linter(linter: Linter);
|
|
361
374
|
/** Resets linter to the default built-in rule set. */
|
|
@@ -484,6 +497,48 @@ interface FieldDecorator<T> extends FieldDecoratorFn<T> {
|
|
|
484
497
|
* `literals` set.
|
|
485
498
|
*/
|
|
486
499
|
declare function literal<const L extends string>(...values: readonly [L, ...L[]]): FieldDecoratorFn<L>;
|
|
500
|
+
/**
|
|
501
|
+
* `@ttl` — the **DynamoDB Time-To-Live** field decorator (issue #172, Epic #167 —
|
|
502
|
+
* CFn generator C4). It marks the model's TTL attribute: the Number field holding
|
|
503
|
+
* the **epoch-seconds** expiry timestamp DynamoDB reads to auto-delete the item.
|
|
504
|
+
*
|
|
505
|
+
* ## Standalone, number-pinned (not composed with `@number`)
|
|
506
|
+
*
|
|
507
|
+
* `@ttl` is a **standalone** semantic decorator — it records the field's storage
|
|
508
|
+
* type (`dynamoType: 'N'`) itself, exactly as `@datetime` records `'S'` + a format
|
|
509
|
+
* and `@literal` records `'S'` + a literal set. It is NOT composed with `@number`
|
|
510
|
+
* (a `@number @ttl` stack). Two reasons:
|
|
511
|
+
*
|
|
512
|
+
* 1. **A valid TTL attribute is always a Number** (epoch seconds) — there is no
|
|
513
|
+
* string/boolean TTL — so the storage type is not a free choice the author
|
|
514
|
+
* should have to (re)state. Folding `'N'` into `@ttl` removes the redundant
|
|
515
|
+
* second decorator and the ordering/consistency question a two-decorator stack
|
|
516
|
+
* would raise (which wins if they disagreed?).
|
|
517
|
+
* 2. It matches the module idiom: every semantic decorator here is self-contained
|
|
518
|
+
* and pins exactly one storage type; none composes with another.
|
|
519
|
+
*
|
|
520
|
+
* ## Loud-reject (the most-feared silent-drop class in this repo)
|
|
521
|
+
*
|
|
522
|
+
* - **Non-number field → compile error.** The decorator is pinned to `number`
|
|
523
|
+
* (its `context` is `ClassFieldDecoratorContext<unknown, number | undefined>`),
|
|
524
|
+
* so `@ttl expiresAt!: string` fails to type-check — the SAME cross-type
|
|
525
|
+
* mechanism that makes `@number age!: string` an error. A TTL on a non-Number
|
|
526
|
+
* attribute can never be declared (DynamoDB would silently never expire it).
|
|
527
|
+
* - **More than one `@ttl` per (physical) table → throws.** DynamoDB allows
|
|
528
|
+
* exactly one TTL attribute per table. A second `@ttl` on the same model throws
|
|
529
|
+
* at `@model` registration; two `@ttl` fields on DIFFERENT entities sharing one
|
|
530
|
+
* physical table throw at manifest build (where all table siblings are known).
|
|
531
|
+
*
|
|
532
|
+
* ## Runtime behaviour
|
|
533
|
+
*
|
|
534
|
+
* A `@ttl` field is a plain stored Number at runtime — no special serialization,
|
|
535
|
+
* hydration, or key handling (it is not a key attribute). The decorator only
|
|
536
|
+
* records metadata (`dynamoType: 'N'`, `ttl: true`); the TTL *effect* is a table
|
|
537
|
+
* property DynamoDB applies, emitted into the generated CloudFormation
|
|
538
|
+
* `TimeToLiveSpecification` (never into `AttributeDefinitions`, which holds key
|
|
539
|
+
* attributes only). So it needs no runtime-serialization conformance change.
|
|
540
|
+
*/
|
|
541
|
+
declare const ttl: FieldDecoratorFn<number>;
|
|
487
542
|
declare const string: FieldDecorator<string>;
|
|
488
543
|
declare const number: FieldDecorator<number>;
|
|
489
544
|
declare const boolean: FieldDecorator<boolean>;
|
|
@@ -1608,6 +1663,13 @@ declare function decodePerKeyCursor(cursor: string, expectedKey: Record<string,
|
|
|
1608
1663
|
* unaffected. query-boundary is excluded because it flags non-unique GSIs which
|
|
1609
1664
|
* are valid for list() operations; boundary enforcement is handled at runtime by
|
|
1610
1665
|
* the planner's boundary-check.
|
|
1666
|
+
*
|
|
1667
|
+
* Also includes cfn-schema-consistency (issue #169): a whole-table validator
|
|
1668
|
+
* that rejects entities sharing a physical table with an ambiguous base
|
|
1669
|
+
* KeySchema, inconsistently-shared GSI index names, or more than 20 unioned
|
|
1670
|
+
* GSIs — the CFn deploy-consistency user-error class, caught statically at
|
|
1671
|
+
* finalize. It is additive: a single entity on its own table with ≤ 20 GSIs is
|
|
1672
|
+
* never affected.
|
|
1611
1673
|
*/
|
|
1612
1674
|
declare function createDefaultLinter(): Linter;
|
|
1613
1675
|
|
|
@@ -3003,4 +3065,4 @@ declare function assertBundleSerializable(bundle: BridgeBundle): void;
|
|
|
3003
3065
|
*/
|
|
3004
3066
|
declare function buildBridgeBundle(queries?: DefinitionMap, commands?: DefinitionMap, registry?: typeof MetadataRegistry, transactions?: Record<string, TransactionDefinition>, contractInputs?: ContractInputs): BridgeBundle;
|
|
3005
3067
|
|
|
3006
|
-
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, type HasManyVersionedCallback, type HasManyVersionedOptions, type HasOneVersionedCallback, type HasOneVersionedOptions, Item, KeyDefinition, type LintResult, type LintRule, Linter, type ListInput, type ListOptions, MAINT_OUTBOX_PK_PREFIX, MAX_TRANSACT_ITEMS, MaintainConsistency, MaintainEffect, MaintainEvent, MaintainUpdateMode, type MaintainedFromCallback, type MaintainedFromOptions, MaintenanceDrain, type MaintenanceDrainOptions, type MaintenanceRebuildOptions, MaintenanceRebuilder, Manifest, MembershipPredicate, MembershipPredicateOp, MetadataRegistry, Middleware, type ModelOptions, ModelStatic, OLD_VALUE_NAMESPACE, OperationDefinition, OperationsDocument, Param, ParamDescriptor, type Parameterize, PartialQueryKeyOf, type PerKeyCursorEnvelope, type PlanInput, PrimaryKeyOf, type ProjectionResult, ProjectionTransform, type ProjectionValue, 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, type SelfProxy, type SelfRef, ShardId, type SourceProxy, type SourceRef, 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 VersionedHistoryOptions, type VersionedLatestOptions, 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, 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, maintainedFrom, map, max, missingGsiRule, model, noScanRule, number, numberSet, plan, queryBoundaryRule, relationDepthRule, requireLimitRule, resolveKey, resolveRelations, serializeContractKey, serializeFieldValue, string, stringSet, validateDepth, validateGsiAmbiguity, when, whenMember };
|
|
3068
|
+
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, type HasManyVersionedCallback, type HasManyVersionedOptions, type HasOneVersionedCallback, type HasOneVersionedOptions, Item, KeyDefinition, type LintResult, type LintRule, Linter, type ListInput, type ListOptions, MAINT_OUTBOX_PK_PREFIX, MAX_TRANSACT_ITEMS, MaintainConsistency, MaintainEffect, MaintainEvent, MaintainUpdateMode, type MaintainedFromCallback, type MaintainedFromOptions, MaintenanceDrain, type MaintenanceDrainOptions, type MaintenanceRebuildOptions, MaintenanceRebuilder, Manifest, MembershipPredicate, MembershipPredicateOp, MetadataRegistry, Middleware, type ModelOptions, ModelStatic, OLD_VALUE_NAMESPACE, OperationDefinition, OperationsDocument, Param, ParamDescriptor, type Parameterize, PartialQueryKeyOf, type PerKeyCursorEnvelope, type PlanInput, PrimaryKeyOf, type ProjectionResult, ProjectionTransform, type ProjectionValue, 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, type SelfProxy, type SelfRef, ShardId, type SourceProxy, type SourceRef, 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 VersionedHistoryOptions, type VersionedLatestOptions, 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, 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, maintainedFrom, map, max, missingGsiRule, model, noScanRule, number, numberSet, plan, queryBoundaryRule, relationDepthRule, requireLimitRule, resolveKey, resolveRelations, serializeContractKey, serializeFieldValue, string, stringSet, ttl, validateDepth, validateGsiAmbiguity, when, whenMember };
|
package/dist/index.js
CHANGED
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
validateDepth,
|
|
80
80
|
when,
|
|
81
81
|
wholeKeysSentinel
|
|
82
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-J5A665UW.js";
|
|
83
83
|
import {
|
|
84
84
|
CdcEmulator,
|
|
85
85
|
MAINT_OUTBOX_PK_PREFIX,
|
|
@@ -91,7 +91,7 @@ import {
|
|
|
91
91
|
orderAndTrimCollection,
|
|
92
92
|
pathField,
|
|
93
93
|
projectFrom
|
|
94
|
-
} from "./chunk-
|
|
94
|
+
} from "./chunk-7NPG5R7O.js";
|
|
95
95
|
import {
|
|
96
96
|
BATCH_GET_MAX_KEYS,
|
|
97
97
|
BATCH_WRITE_MAX_ITEMS,
|
|
@@ -154,7 +154,7 @@ import {
|
|
|
154
154
|
resolveModelClass,
|
|
155
155
|
segmentFieldNames,
|
|
156
156
|
serializeFieldValue
|
|
157
|
-
} from "./chunk-
|
|
157
|
+
} from "./chunk-FMJIWFIS.js";
|
|
158
158
|
|
|
159
159
|
// src/metadata/prefix.ts
|
|
160
160
|
function derivePrefix(customPrefix, className) {
|
|
@@ -267,6 +267,13 @@ function model(options) {
|
|
|
267
267
|
`@model on '${String(context.name)}': \`kind: '${kind}'\` declares a maintained view but no \`@maintainedFrom(...)\` source is stacked. A view with no source is maintained by nothing \u2014 add at least one \`@maintainedFrom\` decorator.`
|
|
268
268
|
);
|
|
269
269
|
}
|
|
270
|
+
const ttlFields = fields.filter((f) => f.ttl === true);
|
|
271
|
+
if (ttlFields.length > 1) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`@model on '${String(context.name)}': ${ttlFields.length} \`@ttl\` fields (${ttlFields.map((f) => `'${f.propertyName}'`).join(", ")}) are declared, but DynamoDB allows exactly one TTL attribute per table. Mark a single field \`@ttl\`.`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const ttlAttribute = ttlFields.length === 1 ? ttlFields[0].propertyName : void 0;
|
|
270
277
|
MetadataRegistry.register(target, {
|
|
271
278
|
tableName: options.table,
|
|
272
279
|
prefix,
|
|
@@ -278,7 +285,8 @@ function model(options) {
|
|
|
278
285
|
embeddedFields,
|
|
279
286
|
kind,
|
|
280
287
|
...options.description !== void 0 ? { description: options.description } : {},
|
|
281
|
-
maintainedFrom: maintainedFrom2
|
|
288
|
+
maintainedFrom: maintainedFrom2,
|
|
289
|
+
...ttlAttribute !== void 0 ? { ttlAttribute } : {}
|
|
282
290
|
});
|
|
283
291
|
};
|
|
284
292
|
}
|
|
@@ -328,6 +336,13 @@ function literal(...values) {
|
|
|
328
336
|
});
|
|
329
337
|
};
|
|
330
338
|
}
|
|
339
|
+
var ttl = (_value, context) => {
|
|
340
|
+
collectField({
|
|
341
|
+
propertyName: String(context.name),
|
|
342
|
+
dynamoType: "N",
|
|
343
|
+
ttl: true
|
|
344
|
+
});
|
|
345
|
+
};
|
|
331
346
|
var string = createFieldDecorator("S");
|
|
332
347
|
var number = createFieldDecorator("N");
|
|
333
348
|
var boolean = createFieldDecorator("BOOL");
|
|
@@ -1973,6 +1988,7 @@ export {
|
|
|
1973
1988
|
serializeFieldValue,
|
|
1974
1989
|
string,
|
|
1975
1990
|
stringSet,
|
|
1991
|
+
ttl,
|
|
1976
1992
|
validateDepth,
|
|
1977
1993
|
validateGsiAmbiguity,
|
|
1978
1994
|
when,
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { E as Executor, D as DynamoDBOperation, 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, M as ModelStatic, f as DDBModel, C as ChangeEvent } from '../types-
|
|
1
|
+
import { E as Executor, D as DynamoDBOperation, 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, M as ModelStatic, f as DDBModel, C as ChangeEvent } from '../types-B-F7jw9f.js';
|
|
2
2
|
import '@aws-sdk/client-dynamodb';
|
|
3
3
|
|
|
4
4
|
/**
|
package/dist/testing/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createCdcEmulator
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-7NPG5R7O.js";
|
|
4
4
|
import {
|
|
5
5
|
ClientManager,
|
|
6
6
|
MetadataRegistry,
|
|
7
7
|
TableMapping,
|
|
8
8
|
resolveModelClass
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-FMJIWFIS.js";
|
|
10
10
|
|
|
11
11
|
// src/memory/memory-store.ts
|
|
12
12
|
function deepClone(value) {
|
|
@@ -3126,6 +3126,21 @@ interface ManifestEntity {
|
|
|
3126
3126
|
* manifest. Surfaces as the generated Python class docstring.
|
|
3127
3127
|
*/
|
|
3128
3128
|
readonly description?: string;
|
|
3129
|
+
/**
|
|
3130
|
+
* The physical attribute name of the model's DynamoDB **Time-To-Live** field,
|
|
3131
|
+
* when one is declared via `@ttl` (issue #172, Epic #167 — CFn generator C4).
|
|
3132
|
+
* Unlike the maintenance/stream signals (a maintenance-IR concern kept OFF the
|
|
3133
|
+
* manifest), TTL is a **physical schema** fact, and the CloudFormation emitter
|
|
3134
|
+
* consumes the manifest — so the TTL attribute name flows here and the emitter
|
|
3135
|
+
* renders `TimeToLiveSpecification { AttributeName: <this>, Enabled: true }`. The
|
|
3136
|
+
* attribute is deliberately NOT added to the table's key `AttributeDefinitions`
|
|
3137
|
+
* (that list holds ONLY key/index attributes; `TimeToLiveSpecification` legitimately
|
|
3138
|
+
* names a non-key attribute). **Absent** when the model declares no `@ttl`, so a
|
|
3139
|
+
* TTL-free model serializes byte-identically to the pre-#172 manifest (backward
|
|
3140
|
+
* compatible). DynamoDB permits exactly one TTL attribute per physical table; the
|
|
3141
|
+
* single-per-table rule is enforced at manifest build.
|
|
3142
|
+
*/
|
|
3143
|
+
readonly ttlAttribute?: string;
|
|
3129
3144
|
}
|
|
3130
3145
|
interface ManifestTable {
|
|
3131
3146
|
readonly physicalName: string;
|
|
@@ -6230,6 +6245,20 @@ interface FieldMetadata {
|
|
|
6230
6245
|
* `@number`, …), so plain fields are byte-for-byte unchanged (backward compat).
|
|
6231
6246
|
*/
|
|
6232
6247
|
literals?: readonly string[];
|
|
6248
|
+
/**
|
|
6249
|
+
* For an `@ttl` field (issue #172, Epic #167 — CFn generator C4) — marks this
|
|
6250
|
+
* field as the model's **DynamoDB Time-To-Live** attribute. `@ttl` is a
|
|
6251
|
+
* standalone semantic decorator pinned to `number` (it records `dynamoType: 'N'`
|
|
6252
|
+
* itself, so `@ttl expiresAt!: string` is a compile error), because DynamoDB TTL
|
|
6253
|
+
* requires a Number attribute holding **epoch seconds**. It is a **physical
|
|
6254
|
+
* schema** fact (unlike the maintenance signals kept off the manifest): the flag
|
|
6255
|
+
* flows through {@link EntityMetadata} into `manifest.json`
|
|
6256
|
+
* ({@link ManifestEntity.ttlAttribute}), which the CloudFormation emitter consumes
|
|
6257
|
+
* to render `TimeToLiveSpecification`. DynamoDB allows exactly one TTL attribute
|
|
6258
|
+
* per table, enforced loudly at model registration (`@model`). `undefined` for
|
|
6259
|
+
* every other field decorator, so non-TTL fields are byte-for-byte unchanged.
|
|
6260
|
+
*/
|
|
6261
|
+
ttl?: boolean;
|
|
6233
6262
|
}
|
|
6234
6263
|
|
|
6235
6264
|
/**
|
|
@@ -6530,6 +6559,19 @@ interface EntityMetadata {
|
|
|
6530
6559
|
* Absent / empty for a plain entity.
|
|
6531
6560
|
*/
|
|
6532
6561
|
maintainedFrom?: MaintainedFromMetadata[];
|
|
6562
|
+
/**
|
|
6563
|
+
* The physical attribute name of the model's `@ttl` field, when one is declared
|
|
6564
|
+
* (issue #172, Epic #167 — CFn generator C4). DynamoDB TTL requires a single
|
|
6565
|
+
* Number attribute holding epoch seconds; `@model` derives this from the (unique)
|
|
6566
|
+
* field carrying {@link FieldMetadata.ttl} and throws if more than one field on the
|
|
6567
|
+
* SAME model is `@ttl` (the cross-entity, same-physical-table single-TTL rule is
|
|
6568
|
+
* enforced downstream, at manifest build, where all entities on a table are known).
|
|
6569
|
+
* A non-key field's physical attribute name is its property name, so this is the
|
|
6570
|
+
* `@ttl` field's property name. Propagated to the manifest
|
|
6571
|
+
* ({@link ManifestEntity.ttlAttribute}) — the SSoT the CloudFormation emitter reads
|
|
6572
|
+
* to render `TimeToLiveSpecification`. Absent when the model declares no `@ttl`.
|
|
6573
|
+
*/
|
|
6574
|
+
ttlAttribute?: string;
|
|
6533
6575
|
}
|
|
6534
6576
|
|
|
6535
6577
|
/**
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# CDC Emulator
|
|
2
|
+
|
|
3
|
+
The CDC (Change Data Capture) emulator produces DynamoDB-Streams-equivalent change events from GraphDDB writes and delivers them to a consumer under a deterministic, fault-injectable delivery contract. It exists to drive and verify stream-based logic — incremental aggregation in particular (see the [Aggregate Tree Pattern](../examples/aggregate-tree-pattern/README.md)) — locally and in CI.
|
|
4
|
+
|
|
5
|
+
The emulator is **dev/test only**. DynamoDB Local does not support Streams, so there is no standard way to exercise stream consumers off-cloud. The emulator fills that gap. Its `ChangeEvent` type and delivery contract mirror real DynamoDB Streams (`NEW_AND_OLD_IMAGES`), so a consumer written against the emulator runs unchanged against real Streams in production.
|
|
6
|
+
|
|
7
|
+
The emulator is responsible only for **producing, delivering, faulting, and replaying change events**. It performs no aggregation; aggregate logic lives entirely in the consumer.
|
|
8
|
+
|
|
9
|
+
## Scope
|
|
10
|
+
|
|
11
|
+
| In scope | Out of scope |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Capturing writes and producing `ChangeEvent`s | Running aggregations (consumer's responsibility) |
|
|
14
|
+
| OldImage / NewImage attachment | Faithful reproduction of real Streams internals (shard splits, iterator age) |
|
|
15
|
+
| Per-shard ordering and batched delivery | Cross-region / GSI streams, TTL-delete events |
|
|
16
|
+
| Fault injection (duplicate, reorder, delay, drop, partial-batch failure) | A user-facing public write hook |
|
|
17
|
+
| Record / replay | Production use |
|
|
18
|
+
|
|
19
|
+
## The `ChangeEvent` type
|
|
20
|
+
|
|
21
|
+
`ChangeEvent` is owned by the `cdc` module; `core` never references it. The shape mirrors a DynamoDB Streams record under the `NEW_AND_OLD_IMAGES` view, which is the only view supported — incremental aggregation requires both images.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
type StreamViewType = 'NEW_AND_OLD_IMAGES';
|
|
25
|
+
type ChangeEventName = 'INSERT' | 'MODIFY' | 'REMOVE';
|
|
26
|
+
|
|
27
|
+
interface ChangeEvent<T = Record<string, unknown>> {
|
|
28
|
+
eventName: ChangeEventName;
|
|
29
|
+
table: string;
|
|
30
|
+
model?: string; // resolved model name when known
|
|
31
|
+
keys: { pk: string; sk: string };
|
|
32
|
+
oldImage?: T; // present for MODIFY / REMOVE
|
|
33
|
+
newImage?: T; // present for INSERT / MODIFY
|
|
34
|
+
approximateCreationTime: string; // ISO 8601, derived from the clock
|
|
35
|
+
sequenceNumber: string; // monotonic within a shard, zero-padded
|
|
36
|
+
shardId: string; // partition = hash(pk)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ChangeBatch<T = Record<string, unknown>> {
|
|
40
|
+
records: ChangeEvent<T>[]; // same-shard records in sequenceNumber order
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface BatchResult {
|
|
44
|
+
batchItemFailures?: string[]; // sequenceNumbers the consumer failed to process
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ChangeHandler = (batch: ChangeBatch) => Promise<BatchResult | void>;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`eventName` is derived from the write: a `delete` yields `REMOVE`; a `put`/`update` yields `MODIFY` when a prior image existed, otherwise `INSERT`. `sequenceNumber` is the per-shard monotonic counter rendered as a 20-digit zero-padded string so lexicographic order equals numeric order. `approximateCreationTime` is derived from the active clock (virtual or real).
|
|
51
|
+
|
|
52
|
+
A consumer derives a delta from `oldImage`/`newImage` and may use `approximateCreationTime` as the source change time.
|
|
53
|
+
|
|
54
|
+
## Capture seam
|
|
55
|
+
|
|
56
|
+
GraphDDB captures writes through an internal, non-exported seam in `core`. The write paths (`executePut`/`Update`/`Delete`/`batch`/`transaction`) call `captureWrite(record)` **after a successful write**. The seam carries only a minimal `RawWriteRecord` (`table`, `model?`, `op`, `keys`, `newItem?`, `oldItem?`); `core` knows nothing of images, sequence numbers, or shards.
|
|
57
|
+
|
|
58
|
+
- The seam is `@internal` and is **not** re-exported from `src/index.ts`. It is the private internal mechanism, distinct from any user-facing hook (which is out of scope). The dependency direction is always `core ← cdc`; `core` does not import the cdc module, so there is no cycle.
|
|
59
|
+
- `ChangeCaptureRegistry` (core-owned, `@internal`) supports multiple subscribers via `register(subscriber): Unsubscribe`. With zero subscribers the seam is a no-op, so there is zero overhead when nothing is listening.
|
|
60
|
+
- A pre-write image (`ReturnValues: ALL_OLD`) is requested only when the model is stream-enabled **and** a subscriber is live, so the OldImage cost is never paid when nobody is listening.
|
|
61
|
+
- Writes that did not actually land (e.g. `ConditionalCheckFailedException`) are not captured.
|
|
62
|
+
- Capture failures are isolated: a throwing subscriber neither fails the originating write nor blocks other subscribers (errors are swallowed at the seam).
|
|
63
|
+
|
|
64
|
+
The cdc module registers as one subscriber and maps each `RawWriteRecord` to a `ChangeEvent`, assigning `shardId`, `sequenceNumber`, and `approximateCreationTime`. Stream enablement is opt-in per model: each class in `opts.models` is stream-enabled on construction (`ChangeCaptureRegistry.enableStream`) and disabled on `close()`. When `models` is provided, records whose resolved model name is not in the set are dropped; a record carrying no model name is emitted.
|
|
65
|
+
|
|
66
|
+
## Delivery contract
|
|
67
|
+
|
|
68
|
+
- **Sharding.** `shardId = hash(pk) % shardCount` (FNV-1a, default `shardCount` 8). All events for one partition key land in the same shard.
|
|
69
|
+
- **Per-shard ordering.** Within a shard, records are delivered in ascending `sequenceNumber`. Order across shards is independent.
|
|
70
|
+
- **Batching.** Records are delivered in batches of up to `batchSize` (default 100) per shard.
|
|
71
|
+
- **At-least-once.** Duplicate delivery is permitted and is reproducible via fault injection.
|
|
72
|
+
- **Checkpoint / resume.** Each shard tracks the last successfully checkpointed sequence number, available via `checkpoints()`. The checkpoint advances only over the **contiguous prefix** of acked sequences (`ReportBatchItemFailures` semantics): it never moves past an un-acked record awaiting redelivery, so a resume cannot skip a record.
|
|
73
|
+
- **Partial-batch failure.** The consumer returns the `sequenceNumber`s it failed to process in `BatchResult.batchItemFailures`. Successful records are checkpointed; failed records are redelivered, re-sorted into ascending order so per-shard ordering is preserved.
|
|
74
|
+
- **Dead-letter queue.** A record that fails more than `maxRetries` times (default 3) is moved to the dead-letter buffer, available via `deadLetters()`.
|
|
75
|
+
|
|
76
|
+
## Modes
|
|
77
|
+
|
|
78
|
+
The `mode` option selects when events are delivered.
|
|
79
|
+
|
|
80
|
+
| Mode | Use | Delivery timing |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `inline` | Unit tests | Delivered automatically after each capture. The capture seam is synchronous; the async delivery is chained onto an internal promise that callers await via `flush()`. |
|
|
83
|
+
| `queued` | Time-dependent behavior (throttle, sweep period, staleness) | Buffered. Delivery is driven by `pump()` or by advancing the virtual clock with `advanceClock()`. |
|
|
84
|
+
| `record` | Capture a log without delivering | Events are recorded but not delivered; obtain the log with `record()`. |
|
|
85
|
+
| `replay` | Replay-equivalence verification | Delivery is driven by `replay(log, opts?)`. |
|
|
86
|
+
|
|
87
|
+
Every mode records events into an internal log, so `record()` and `replay()` are available regardless of mode.
|
|
88
|
+
|
|
89
|
+
## Virtual clock
|
|
90
|
+
|
|
91
|
+
The `clock` option is `'real'` or `'virtual'`; the default is `'virtual'` for `queued` mode and `'real'` otherwise. Under the virtual clock, time advances only through `advanceClock(ms)`, which moves the clock forward and delivers every record whose due time has arrived. `now()` returns the current virtual time in milliseconds since the emulator epoch (`2020-01-01T00:00:00Z`), and `approximateCreationTime` is derived from it. This makes throttle period, sweep period, and staleness behavior deterministic without depending on wall-clock time. `advanceClock()` throws if the clock is not virtual.
|
|
92
|
+
|
|
93
|
+
## Fault injection
|
|
94
|
+
|
|
95
|
+
Faults are configured via `fault(spec)` (the spec is merged into the current one). All randomness routes through a seeded PRNG (`seed`, default 1), so a given seed reproduces the exact same delivery — including duplicates, reorderings, delays, and failures.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
interface FaultSpec {
|
|
99
|
+
duplicate?: number; // probability a delivered record is delivered again
|
|
100
|
+
reorder?: boolean; // shuffle within-shard order on delivery
|
|
101
|
+
delay?: { min: number; max: number }; // per-record virtual-time delivery delay (ms); requires queued mode
|
|
102
|
+
dropThenRedeliver?: number; // probability a record is dropped on first delivery, then redelivered
|
|
103
|
+
partialBatchFailure?: number; // probability a record is reported as a partial-batch failure
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`reorder` deliberately violates the within-shard ordering guarantee to exercise a consumer's invariant detection. `delay` withholds a record until the virtual clock reaches its due time.
|
|
108
|
+
|
|
109
|
+
### Concurrent-recompute injection
|
|
110
|
+
|
|
111
|
+
To stage a mark-versus-recompute race (the false-clean race), the harness arms two hooks:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
emulator.injectConcurrentRecompute({ ref: 'Parent#p1' }); // which ref to race
|
|
115
|
+
emulator.onConcurrentRecompute((ref, event) => { /* run the recompute */ });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Once both are set, the emulator invokes the registered recompute callback for each record immediately before that record reaches the consumer, deterministically interleaving the recompute with the consumer's mark. `injectConcurrentRecompute` alone records the ref; `onConcurrentRecompute` supplies the actual recompute and arms the race.
|
|
119
|
+
|
|
120
|
+
## API
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
createCdcEmulator(opts?: CdcEmulatorOptions): CdcEmulator
|
|
124
|
+
|
|
125
|
+
class CdcEmulator {
|
|
126
|
+
subscribe(handler: ChangeHandler): Unsubscribe;
|
|
127
|
+
flush(): Promise<void>; // await inline-mode deliveries
|
|
128
|
+
pump(maxBatches?: number): Promise<void>;
|
|
129
|
+
advanceClock(ms: number): Promise<void>;
|
|
130
|
+
now(): number;
|
|
131
|
+
fault(spec: FaultSpec): void;
|
|
132
|
+
injectConcurrentRecompute(ref: ConcurrentRecomputeRef): void;
|
|
133
|
+
onConcurrentRecompute(hook: (ref, event) => void | Promise<void>): void;
|
|
134
|
+
record(): EventLog;
|
|
135
|
+
replay(log: EventLog, opts?: ReplayOptions): Promise<void>;
|
|
136
|
+
checkpoints(): Record<ShardId, string>;
|
|
137
|
+
deadLetters(): ChangeEvent[];
|
|
138
|
+
reset(): void; // reset delivery state and re-seed (keeps subscription + opt-in)
|
|
139
|
+
close(): void; // detach from the seam, disable model streaming
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`subscribe` registers the single consumer; when `startingPosition` is `'LATEST'`, records buffered before the subscription are dropped. `record()` returns `{ seed, events }`. `replay(log, opts)` re-shards the log by `hash(pk)`, optionally shuffles and duplicates it (`ReplayOptions = { shuffle?: boolean; duplicate?: number }`), then re-sorts each shard into ascending `sequenceNumber` order before delivering — so cross-shard reordering and at-least-once chaos are modelled while the within-shard ordering guarantee that aggregation depends on is preserved.
|
|
144
|
+
|
|
145
|
+
## Options
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
interface CdcEmulatorOptions {
|
|
149
|
+
models?: Function[]; // stream-enabled model classes (opt-in; default none)
|
|
150
|
+
mode?: CdcMode; // default 'inline'
|
|
151
|
+
clock?: ClockMode; // default 'virtual' for queued, 'real' otherwise
|
|
152
|
+
batchSize?: number; // default 100
|
|
153
|
+
maxRetries?: number; // default 3
|
|
154
|
+
startingPosition?: 'TRIM_HORIZON' | 'LATEST'; // default 'TRIM_HORIZON'
|
|
155
|
+
seed?: number; // fault / shard determinism; default 1
|
|
156
|
+
shardCount?: number; // default 8
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A `ModelStatic` (from `DDBModel.asModel()`) or a raw decorated class may be passed in `models`; a `ModelStatic` is unwrapped to its inner class so the stream opt-in and model-name filter align with the class the write paths key on.
|
|
161
|
+
|
|
162
|
+
## Connecting to a consumer
|
|
163
|
+
|
|
164
|
+
The emulator emits generic `ChangeEvent`s; wiring them to an aggregate is the consumer's job. A consumer subscribes, derives the work to do from each event, and reports per-record outcomes. Below, a consumer rebuilds a leaf-sum aggregate idempotently from `newImage`:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
const cdc = createCdcEmulator({ models: [CounterModel], mode: 'queued', seed: 1 });
|
|
168
|
+
|
|
169
|
+
const latestByLeaf = new Map<string, number>();
|
|
170
|
+
cdc.subscribe(async (batch) => {
|
|
171
|
+
for (const e of batch.records) {
|
|
172
|
+
const key = `${e.keys.pk}|${e.keys.sk}`;
|
|
173
|
+
if (e.eventName === 'REMOVE') latestByLeaf.delete(key);
|
|
174
|
+
else latestByLeaf.set(key, Number((e.newImage as { value: number }).value));
|
|
175
|
+
}
|
|
176
|
+
// return { batchItemFailures: [...] } to redeliver specific records
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await Counter.putItem({ siteId: 's1', leafId: 'a', value: 10 });
|
|
180
|
+
await cdc.advanceClock(1); // drive delivery under the virtual clock
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Sweep scheduling lives outside the emulator: the emulator handles only change production and delivery. In production, swap `createCdcEmulator` for a real Streams consumer; the consumer code is unchanged because the `ChangeEvent` type and delivery contract match.
|
|
184
|
+
|
|
185
|
+
### Recording and replaying
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const recorder = createCdcEmulator({ models: [CounterModel], mode: 'record', seed: 4 });
|
|
189
|
+
await Counter.putItem({ siteId: 's1', leafId: 'a', value: 5 });
|
|
190
|
+
await Counter.updateItem({ siteId: 's1', leafId: 'a' }, { value: 9 });
|
|
191
|
+
const log = recorder.record();
|
|
192
|
+
recorder.close();
|
|
193
|
+
|
|
194
|
+
const replayer = createCdcEmulator({ mode: 'replay', seed: 4 });
|
|
195
|
+
replayer.subscribe(/* idempotent, key-addressed consumer */);
|
|
196
|
+
await replayer.replay(log, { shuffle: true, duplicate: 0.5 });
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Because the consumer is idempotent and the replay preserves per-shard ordering, the final aggregate equals a full recompute from source regardless of the global delivery order or duplicates (replay equivalence).
|
|
200
|
+
|
|
201
|
+
## Related
|
|
202
|
+
|
|
203
|
+
- Pattern: [Aggregate Tree Pattern](../examples/aggregate-tree-pattern/README.md)
|