graphddb 0.5.0 → 0.5.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.
@@ -5992,6 +5992,189 @@ type MutateParallelResult<E extends WriteEnvelope> = {
5992
5992
  readonly [K in keyof E]: BulkWrap<E[K], ParallelResultOf<E[K]>>;
5993
5993
  };
5994
5994
 
5995
+ /**
5996
+ * CDC emulator types (issue #72). These types are **cdc-module-owned**; core
5997
+ * never references them. The {@link ChangeEvent} shape mirrors DynamoDB Streams
5998
+ * (`NEW_AND_OLD_IMAGES`) so a consumer written against the emulator runs
5999
+ * unchanged against real Streams in production (spec §4).
6000
+ */
6001
+ /** Stream view type. Fixed to `NEW_AND_OLD_IMAGES` — incremental aggregation
6002
+ * needs both images (spec §4). */
6003
+ type StreamViewType = 'NEW_AND_OLD_IMAGES';
6004
+ /** Stream event kind, matching DynamoDB Streams. */
6005
+ type ChangeEventName = 'INSERT' | 'MODIFY' | 'REMOVE';
6006
+ /**
6007
+ * A single change event. Within one shard, `sequenceNumber` is monotonically
6008
+ * increasing; across shards order is independent (spec §6).
6009
+ */
6010
+ interface ChangeEvent<T = Record<string, unknown>> {
6011
+ eventName: ChangeEventName;
6012
+ table: string;
6013
+ /** Resolved model name when known. */
6014
+ model?: string;
6015
+ keys: {
6016
+ pk: string;
6017
+ sk: string;
6018
+ };
6019
+ /** Present for MODIFY / REMOVE. */
6020
+ oldImage?: T;
6021
+ /** Present for INSERT / MODIFY. */
6022
+ newImage?: T;
6023
+ /** Deterministic-clock-derived creation time (ISO 8601). */
6024
+ approximateCreationTime: string;
6025
+ /** Monotonic within a shard (zero-padded for lexicographic ordering). */
6026
+ sequenceNumber: string;
6027
+ /** Partition = hash(pk). */
6028
+ shardId: string;
6029
+ }
6030
+ /** A delivered batch. Records of the same shard are in `sequenceNumber` order. */
6031
+ interface ChangeBatch<T = Record<string, unknown>> {
6032
+ records: ChangeEvent<T>[];
6033
+ }
6034
+ /**
6035
+ * The consumer's response to a batch (spec §6, `ReportBatchItemFailures`
6036
+ * equivalent). Returning the `sequenceNumber`s of records that failed lets the
6037
+ * emulator advance the checkpoint past the successful prefix and redeliver only
6038
+ * the failures.
6039
+ */
6040
+ interface BatchResult {
6041
+ /** `sequenceNumber`s of records the consumer failed to process. */
6042
+ batchItemFailures?: string[];
6043
+ }
6044
+ /** The consumer handler. */
6045
+ type ChangeHandler = (batch: ChangeBatch) => Promise<BatchResult | void>;
6046
+ /** Unsubscribe handle. */
6047
+ type Unsubscribe = () => void;
6048
+ /** Where a fresh subscriber begins reading. */
6049
+ type StartingPosition = 'TRIM_HORIZON' | 'LATEST';
6050
+ /** Clock mode. `virtual` makes time deterministic via `advanceClock`. */
6051
+ type ClockMode = 'real' | 'virtual';
6052
+ /** Delivery mode (spec §7). */
6053
+ type CdcMode = 'inline' | 'queued' | 'record' | 'replay';
6054
+ /** Deterministic fault-injection spec (spec §8). All probabilities are seeded. */
6055
+ interface FaultSpec {
6056
+ /** Probability [0,1] a delivered record is also re-delivered (duplicate). */
6057
+ duplicate?: number;
6058
+ /** Shuffle within-shard order on delivery (invariant-violation testing). */
6059
+ reorder?: boolean;
6060
+ /** Per-record virtual-time delivery delay range (ms). Requires queued mode. */
6061
+ delay?: {
6062
+ min: number;
6063
+ max: number;
6064
+ };
6065
+ /** Probability [0,1] a record is dropped on first delivery then redelivered. */
6066
+ dropThenRedeliver?: number;
6067
+ /** Probability [0,1] a record is reported as a partial-batch failure. */
6068
+ partialBatchFailure?: number;
6069
+ }
6070
+ /** A reference whose recompute should be forced to race a mark (spec §8, §7). */
6071
+ interface ConcurrentRecomputeRef {
6072
+ /** Opaque node ref the harness recomputes concurrently with marking. */
6073
+ ref: string;
6074
+ }
6075
+ /** A persisted event log (spec §10, record/replay). */
6076
+ interface EventLog {
6077
+ seed: number;
6078
+ events: ChangeEvent[];
6079
+ }
6080
+ /** Replay options (spec §10). */
6081
+ interface ReplayOptions {
6082
+ /** Deliver events in a deterministic shuffled order (any-order replay). */
6083
+ shuffle?: boolean;
6084
+ /** Probability [0,1] each event is delivered twice during replay. */
6085
+ duplicate?: number;
6086
+ }
6087
+ /** Constructor options (spec §11). */
6088
+ interface CdcEmulatorOptions {
6089
+ /** Stream-enabled model classes (opt-in; default none). */
6090
+ models?: Function[];
6091
+ /** Delivery mode. Default `inline`. */
6092
+ mode?: CdcMode;
6093
+ /** Clock mode. Default `virtual` for queued, `real` otherwise. */
6094
+ clock?: ClockMode;
6095
+ /** Records per delivered batch. Default 100. */
6096
+ batchSize?: number;
6097
+ /** Max redelivery attempts before a record goes to the DLQ. Default 3. */
6098
+ maxRetries?: number;
6099
+ /** Where a subscriber starts. Default `TRIM_HORIZON`. */
6100
+ startingPosition?: StartingPosition;
6101
+ /** Seed for fault injection / shard assignment determinism. Default 1. */
6102
+ seed?: number;
6103
+ /** Number of shards events are partitioned across. Default 8. */
6104
+ shardCount?: number;
6105
+ }
6106
+ type ShardId = string;
6107
+
6108
+ /**
6109
+ * `subscribe` — the declarative, batch CDC consumer builder (issue #153).
6110
+ *
6111
+ * The third sibling of `query` / `mutate` (GraphQL's query / mutation /
6112
+ * **subscription**). Unlike them, `subscribe` DOES NOT subscribe and does not hit
6113
+ * DynamoDB: it *returns* a {@link ChangeHandler} the consumer mounts on their own
6114
+ * stream source (the CDC emulator locally, DynamoDB Streams → Lambda in
6115
+ * production). Delivery — the actual subscription, the sink write, the dedup /
6116
+ * idempotency store — lives OUTSIDE the boundary, in consumer code (see
6117
+ * `docs/cdc-projection.md`). `DDBModel.subscribe` delegates here.
6118
+ *
6119
+ * The returned handler:
6120
+ *
6121
+ * - loops the batch and routes each event to its owning model by `event.model` /
6122
+ * `keys.pk` (via {@link parseChange});
6123
+ * - typed-parses that event's `oldImage` / `newImage` into `[old, new]` and calls
6124
+ * the matching handler;
6125
+ * - if a handler throws, records the event's `sequenceNumber` into
6126
+ * `batchItemFailures` (the Streams / Lambda `ReportBatchItemFailures` redelivery
6127
+ * protocol) and CONTINUES the batch — a single failing record never aborts the
6128
+ * whole batch. This mirrors how `mutate({ mode: 'parallel' })` aggregates per-op
6129
+ * ok/error; it is the delivery *protocol* shape, not delivery *semantics*, so it
6130
+ * stays inside the boundary;
6131
+ * - NEVER writes a sink and NEVER dedups.
6132
+ */
6133
+
6134
+ /**
6135
+ * One handler in the {@link SubscribeHandlers} map: given the parsed
6136
+ * `[oldRecord, newRecord]` for an event routed to this model, project it (the body
6137
+ * is CONSUMER code — a sink write, a diff, a log). May be async; a thrown error is
6138
+ * caught by the batch loop and turned into a `batchItemFailures` entry. `old` / `new`
6139
+ * are `T | null` (one side is null per event kind — INSERT / MODIFY / REMOVE).
6140
+ */
6141
+ type SubscribeHandler<T> = (oldRecord: T | null, newRecord: T | null) => void | Promise<unknown>;
6142
+ /**
6143
+ * The envelope-style handler map `subscribe` accepts, keyed by model name. Typed
6144
+ * against the generated {@link import('../index.js').CdcModelRegistry} at the
6145
+ * `DDBModel.subscribe` surface so keys are constrained to `@cdcProjected` models
6146
+ * and each handler's `old` / `new` is inferred from the key. The runtime here is
6147
+ * name-string driven (the type map is a compile-time-only concern).
6148
+ */
6149
+ type SubscribeHandlers = Record<string, SubscribeHandler<never>>;
6150
+ /**
6151
+ * Build the batch {@link ChangeHandler} from an envelope-style handler map. The
6152
+ * runtime core behind `DDBModel.subscribe`.
6153
+ */
6154
+ declare function buildSubscribeHandler(handlers: SubscribeHandlers): ChangeHandler;
6155
+
6156
+ /**
6157
+ * The generated **model name → model type** map (issue #153). graphddb ships it
6158
+ * EMPTY; the codegen (`aaac generate`) emits a `declare module 'graphddb' { interface
6159
+ * CdcModelRegistry { OrderModel: OrderModel; … } }` augmentation listing exactly the
6160
+ * `@cdcProjected` models (see `docs/cdc-projection.md` §4). With the augmentation in
6161
+ * scope, {@link DDBModel.subscribe}'s handler keys are constrained to the registry
6162
+ * (typos / unknown or non-`@cdcProjected` model names are compile errors) and each
6163
+ * handler's `old` / `new` are inferred from the key. Without it (no codegen run), the
6164
+ * registry is empty and `subscribe` falls back to a permissive name→instance map.
6165
+ */
6166
+ interface CdcModelRegistry {
6167
+ }
6168
+ /**
6169
+ * The handler map `DDBModel.subscribe` accepts (issue #153). When the generated
6170
+ * {@link CdcModelRegistry} is populated, keys are constrained to its model names and
6171
+ * each handler's `(old, new)` is inferred from the mapped instance type. An empty
6172
+ * registry (no codegen run) degrades to a permissive `Record` so authoring is not
6173
+ * blocked before the augmentation is generated.
6174
+ */
6175
+ type CdcSubscribeHandlers = keyof CdcModelRegistry extends never ? Record<string, SubscribeHandler<object>> : {
6176
+ [K in keyof CdcModelRegistry]?: SubscribeHandler<CdcModelRegistry[K]>;
6177
+ };
5995
6178
  declare abstract class DDBModel {
5996
6179
  /** @internal Type brand — prevents primitives from structurally matching DDBModel. */
5997
6180
  private readonly __brand;
@@ -6071,6 +6254,45 @@ declare abstract class DDBModel {
6071
6254
  static mutate<const E extends WriteEnvelope>(envelope: E, options: {
6072
6255
  mode: 'parallel';
6073
6256
  }): Promise<MutateParallelResult<E>>;
6257
+ /**
6258
+ * Parse a single CDC {@link ChangeEvent} into the `[oldRecord, newRecord]` tuple
6259
+ * for THIS model (issue #153 — the pure `(event) => [old, new]` typed mapper).
6260
+ * It reuses the read path's `hydrate` on `oldImage` / `newImage` (ISO 8601 →
6261
+ * `Date` for `@datetime`, embedded reconstruction) and returns typed instances of
6262
+ * this model — **all fields**, no projection (narrowing does not reduce cost; the
6263
+ * image is already on the stream).
6264
+ *
6265
+ * Routing + parse in one call: an event for a DIFFERENT model returns
6266
+ * `[null, null]` (a valid event for this model always has ≥1 non-null side, so
6267
+ * `[null, null]` is an unambiguous "not for this class" signal — no separate
6268
+ * `owns()` guard). The present side per event kind mirrors DynamoDB Streams:
6269
+ * INSERT → `[null, new]`, MODIFY → `[old, new]`, REMOVE → `[old, null]`.
6270
+ *
6271
+ * Only callable on a `@cdcProjected` model — the generated {@link CdcModelRegistry}
6272
+ * types this at the `subscribe` surface, and at runtime a non-`@cdcProjected` model
6273
+ * throws (loudly, never silently returning `[null, null]`, which would masquerade
6274
+ * as a routing miss). Delivery / sink writes / dedup are the consumer's, outside
6275
+ * this boundary (see `docs/cdc-projection.md`).
6276
+ */
6277
+ static fromChange<C extends abstract new (...args: never[]) => DDBModel>(this: C, event: ChangeEvent): [InstanceType<C> | null, InstanceType<C> | null];
6278
+ /**
6279
+ * Build a batch CDC {@link ChangeHandler} from an envelope-style handler map keyed
6280
+ * by model name (issue #153 — the GraphQL query/mutation/**subscription** trio's
6281
+ * third sibling). Unlike `query` / `mutate`, `subscribe` DOES NOT subscribe and
6282
+ * does not hit DynamoDB: it *returns* a `(batch) => Promise<BatchResult>` the
6283
+ * consumer mounts on their own stream source (the CDC emulator locally, DynamoDB
6284
+ * Streams → Lambda in production) — delivery lives outside the boundary.
6285
+ *
6286
+ * The returned handler loops the batch, routes each event to its owning model by
6287
+ * `event.model` / `keys.pk`, typed-parses via that model's `fromChange`, calls the
6288
+ * matching handler, and — if a handler throws — records the event's
6289
+ * `sequenceNumber` into `batchItemFailures` (the `ReportBatchItemFailures`
6290
+ * redelivery protocol) and continues. It NEVER writes a sink and NEVER dedups
6291
+ * (both are consumer concerns). Handler keys are constrained to the generated
6292
+ * {@link CdcModelRegistry} (`@cdcProjected` models only); a key that is not a
6293
+ * registered `@cdcProjected` model throws at build time.
6294
+ */
6295
+ static subscribe(handlers: CdcSubscribeHandlers): ChangeHandler;
6074
6296
  /**
6075
6297
  * Multi-key, multi-model batch read. Each {@link BatchGetRequest} contributes its
6076
6298
  * keys to a chunked `BatchGetItem` per physical table; results are re-grouped by
@@ -6559,6 +6781,19 @@ interface EntityMetadata {
6559
6781
  * Absent / empty for a plain entity.
6560
6782
  */
6561
6783
  maintainedFrom?: MaintainedFromMetadata[];
6784
+ /**
6785
+ * Whether the model is flagged **CDC-parseable** by `@cdcProjected()` (issue
6786
+ * #153). It carries NO runtime side effect — it is purely a compile-/registration-
6787
+ * time capability marker: `Model.fromChange(event)` (and, transitively,
6788
+ * `DDBModel.subscribe` routing to this model) is gated on it, and it is the SSoT
6789
+ * the codegen reads to emit the `CdcModelRegistry` module augmentation that types
6790
+ * `subscribe`'s handlers by model name. `true` only on a model carrying the
6791
+ * decorator; absent (⇒ falsy) for every plain entity, so a non-`@cdcProjected`
6792
+ * model is byte-for-byte unchanged (backward compatible). Deliberately kept OFF
6793
+ * the manifest / bridge (it is a host-runtime + TS-typing concern, like the
6794
+ * maintenance signals — not a physical-schema fact like {@link ttlAttribute}).
6795
+ */
6796
+ cdcProjected?: boolean;
6562
6797
  /**
6563
6798
  * The physical attribute name of the model's `@ttl` field, when one is declared
6564
6799
  * (issue #172, Epic #167 — CFn generator C4). DynamoDB TTL requires a single
@@ -6574,117 +6809,4 @@ interface EntityMetadata {
6574
6809
  ttlAttribute?: string;
6575
6810
  }
6576
6811
 
6577
- /**
6578
- * CDC emulator types (issue #72). These types are **cdc-module-owned**; core
6579
- * never references them. The {@link ChangeEvent} shape mirrors DynamoDB Streams
6580
- * (`NEW_AND_OLD_IMAGES`) so a consumer written against the emulator runs
6581
- * unchanged against real Streams in production (spec §4).
6582
- */
6583
- /** Stream view type. Fixed to `NEW_AND_OLD_IMAGES` — incremental aggregation
6584
- * needs both images (spec §4). */
6585
- type StreamViewType = 'NEW_AND_OLD_IMAGES';
6586
- /** Stream event kind, matching DynamoDB Streams. */
6587
- type ChangeEventName = 'INSERT' | 'MODIFY' | 'REMOVE';
6588
- /**
6589
- * A single change event. Within one shard, `sequenceNumber` is monotonically
6590
- * increasing; across shards order is independent (spec §6).
6591
- */
6592
- interface ChangeEvent<T = Record<string, unknown>> {
6593
- eventName: ChangeEventName;
6594
- table: string;
6595
- /** Resolved model name when known. */
6596
- model?: string;
6597
- keys: {
6598
- pk: string;
6599
- sk: string;
6600
- };
6601
- /** Present for MODIFY / REMOVE. */
6602
- oldImage?: T;
6603
- /** Present for INSERT / MODIFY. */
6604
- newImage?: T;
6605
- /** Deterministic-clock-derived creation time (ISO 8601). */
6606
- approximateCreationTime: string;
6607
- /** Monotonic within a shard (zero-padded for lexicographic ordering). */
6608
- sequenceNumber: string;
6609
- /** Partition = hash(pk). */
6610
- shardId: string;
6611
- }
6612
- /** A delivered batch. Records of the same shard are in `sequenceNumber` order. */
6613
- interface ChangeBatch<T = Record<string, unknown>> {
6614
- records: ChangeEvent<T>[];
6615
- }
6616
- /**
6617
- * The consumer's response to a batch (spec §6, `ReportBatchItemFailures`
6618
- * equivalent). Returning the `sequenceNumber`s of records that failed lets the
6619
- * emulator advance the checkpoint past the successful prefix and redeliver only
6620
- * the failures.
6621
- */
6622
- interface BatchResult {
6623
- /** `sequenceNumber`s of records the consumer failed to process. */
6624
- batchItemFailures?: string[];
6625
- }
6626
- /** The consumer handler. */
6627
- type ChangeHandler = (batch: ChangeBatch) => Promise<BatchResult | void>;
6628
- /** Unsubscribe handle. */
6629
- type Unsubscribe = () => void;
6630
- /** Where a fresh subscriber begins reading. */
6631
- type StartingPosition = 'TRIM_HORIZON' | 'LATEST';
6632
- /** Clock mode. `virtual` makes time deterministic via `advanceClock`. */
6633
- type ClockMode = 'real' | 'virtual';
6634
- /** Delivery mode (spec §7). */
6635
- type CdcMode = 'inline' | 'queued' | 'record' | 'replay';
6636
- /** Deterministic fault-injection spec (spec §8). All probabilities are seeded. */
6637
- interface FaultSpec {
6638
- /** Probability [0,1] a delivered record is also re-delivered (duplicate). */
6639
- duplicate?: number;
6640
- /** Shuffle within-shard order on delivery (invariant-violation testing). */
6641
- reorder?: boolean;
6642
- /** Per-record virtual-time delivery delay range (ms). Requires queued mode. */
6643
- delay?: {
6644
- min: number;
6645
- max: number;
6646
- };
6647
- /** Probability [0,1] a record is dropped on first delivery then redelivered. */
6648
- dropThenRedeliver?: number;
6649
- /** Probability [0,1] a record is reported as a partial-batch failure. */
6650
- partialBatchFailure?: number;
6651
- }
6652
- /** A reference whose recompute should be forced to race a mark (spec §8, §7). */
6653
- interface ConcurrentRecomputeRef {
6654
- /** Opaque node ref the harness recomputes concurrently with marking. */
6655
- ref: string;
6656
- }
6657
- /** A persisted event log (spec §10, record/replay). */
6658
- interface EventLog {
6659
- seed: number;
6660
- events: ChangeEvent[];
6661
- }
6662
- /** Replay options (spec §10). */
6663
- interface ReplayOptions {
6664
- /** Deliver events in a deterministic shuffled order (any-order replay). */
6665
- shuffle?: boolean;
6666
- /** Probability [0,1] each event is delivered twice during replay. */
6667
- duplicate?: number;
6668
- }
6669
- /** Constructor options (spec §11). */
6670
- interface CdcEmulatorOptions {
6671
- /** Stream-enabled model classes (opt-in; default none). */
6672
- models?: Function[];
6673
- /** Delivery mode. Default `inline`. */
6674
- mode?: CdcMode;
6675
- /** Clock mode. Default `virtual` for queued, `real` otherwise. */
6676
- clock?: ClockMode;
6677
- /** Records per delivered batch. Default 100. */
6678
- batchSize?: number;
6679
- /** Max redelivery attempts before a record goes to the DLQ. Default 3. */
6680
- maxRetries?: number;
6681
- /** Where a subscriber starts. Default `TRIM_HORIZON`. */
6682
- startingPosition?: StartingPosition;
6683
- /** Seed for fault injection / shard assignment determinism. Default 1. */
6684
- seed?: number;
6685
- /** Number of shards events are partitioned across. Default 8. */
6686
- shardCount?: number;
6687
- }
6688
- type ShardId = string;
6689
-
6690
- export { type FaultSpec as $, type AggregateOptions as A, type BatchGetExecInput as B, type ChangeEvent as C, type DynamoDBOperation as D, type Executor as E, type FieldOptions as F, type GsiDefinition as G, type SelectBuilderSpec as H, type Item as I, type RawCondition as J, type KeyDefinition as K, type TransactionSpec as L, type ModelStatic as M, type Manifest as N, type RetryOverride as O, type PutInput as P, type ExecutionPlan as Q, type ReadExecOptions as R, type SelectableOf as S, type TransactWriteExecItem as T, type UpdateInput as U, type FieldMetadata as V, type WriteExecOptions as W, type ResolvedKey as X, type CdcEmulatorOptions as Y, type ChangeHandler as Z, type Unsubscribe as _, type ExecutorResult as a, type ContractCardinality as a$, type ConcurrentRecomputeRef as a0, type EventLog as a1, type ReplayOptions as a2, type ShardId as a3, type ViewDefinition as a4, type RelationMetadata as a5, type TransactionItemSpec as a6, type MaintainEffect as a7, type Param as a8, type ParamDescriptor as a9, BatchGetResult as aA, type BatchPutRequest as aB, type BatchResult as aC, type BatchWriteRequest as aD, CONTRACT_RANGE_FANOUT_CONCURRENCY as aE, type CdcMode as aF, type Change as aG, type ChangeBatch as aH, type ChangeEventName as aI, type ClockMode as aJ, type CollectionEffect as aK, type CollectionOptions as aL, type Column as aM, type ColumnMap as aN, type CommandContractMethodSpec as aO, type CommandInputShape as aP, type CommandMethod as aQ, type CommandPlan as aR, type CommandResolutionTarget as aS, type CommandResultKind as aT, type CommandSelectShape as aU, type CompiledFragment as aV, type ComposeSpec as aW, type CondSlot as aX, type ConditionCheckInput as aY, type Connection as aZ, type ContractCallSignature as a_, type DefinitionMap as aa, type OperationDefinition as ab, type WriteDefinitionOptions as ac, type PartialQueryKeyOf as ad, type StrictSelectSpec as ae, type ReadDefinitionOptions as af, type EntityInput as ag, type UniqueQueryKeyOf as ah, type EntityRef as ai, type ConditionInput as aj, type QueryModelContract as ak, type QueryMethodSpec as al, type CommandModelContract as am, type CommandMethodSpec as an, type ContractSpec as ao, type QuerySpec as ap, type CommandSpec as aq, type ContextSpec as ar, type OperationsDocument as as, type AnyOperationDefinition as at, type BridgeBundle as au, type ConditionSpec as av, type AggregateMetadata as aw, type BatchDeleteRequest as ax, type BatchGetOptions as ay, type BatchGetRequest as az, type WriteResult as b, type MutateMode as b$, type ContractCommandParams as b0, type ContractCommandResult as b1, type ContractComposeNode as b2, type ContractFromRef as b3, type ContractInputArity as b4, type ContractItem as b5, type ContractKeyFieldRef as b6, type ContractKeyInput as b7, type ContractKeyRef as b8, type ContractKeySpec as b9, type FragmentInput as bA, type GsiDefinitionMarker as bB, type GsiOptions as bC, type IdempotencyEffect as bD, type InProcessWriteDescriptor as bE, type InputArity as bF, type KeyDefinitionMarker as bG, type KeySegment as bH, type KeySlot as bI, type KeyStructure as bJ, type KeyedResult as bK, LIFECYCLE_CONTRACT_MARKER as bL, type LifecycleContract as bM, type LifecycleEffects as bN, type LiteralParam as bO, type MaintainItem as bP, type MaintainTrigger as bQ, type MaintenanceGraph as bR, type ManifestEntity as bS, type ManifestField as bT, type ManifestFieldType as bU, type ManifestGsi as bV, type ManifestKey as bW, type ManifestRelation as bX, type ManifestTable as bY, type MembershipEffect as bZ, type ModelRef as b_, type ContractKind as ba, type ContractMethodOp as bb, type ContractParamRef as bc, type ContractQueryParams as bd, type ContractResolution as be, type CounterAggregate as bf, type CounterEffect as bg, type CtxBase as bh, DEFAULT_MAX_ATTEMPTS as bi, DEFAULT_RETRY_POLICY as bj, type DeleteOptions as bk, type DeriveEffect as bl, type DerivedEdgeWrite as bm, type DerivedUpdate as bn, type DescriptorBinding as bo, ENTITY_WRITES_MARKER as bp, type EdgeEffect as bq, type EffectPath as br, type EmbeddedMetadata as bs, type EmitEffect as bt, type EntityWritesDefinition as bu, type EntityWritesShape as bv, type ExecutableCommandContract as bw, type ExecutableQueryContract as bx, type FilterInput as by, type FilterSpec as bz, type DeleteInput as c, type UpdateOptions as c$, type MutateOptions as c0, type MutateParallelResult as c1, type MutateTransactionResult as c2, type MutationBody as c3, type MutationDescriptorMap as c4, type MutationFragment as c5, type MutationInputProxy as c6, type MutationInputRef as c7, type MutationIntent as c8, type NumberParam as c9, type RelationBuilder as cA, type RelationConsistency as cB, type RelationLimitOptions as cC, type RelationPattern as cD, type RelationProjection as cE, type RelationReadOptions as cF, type RelationSelect as cG, type RelationSpec as cH, type RelationUpdateMode as cI, type RelationWriteOptions as cJ, type RequiresEffect as cK, type Resolution as cL, type RetryInfo as cM, type RetryOperationKind as cN, SPEC_VERSION as cO, type SegmentSpec as cP, type SegmentedKey as cQ, type SelectBuilder as cR, type SelectOf as cS, type SnapshotEffect as cT, type StartingPosition as cU, type StreamViewType as cV, type StringParam as cW, TransactionContext as cX, type TransactionItemType as cY, type UniqueEffect as cZ, type Updatable as c_, type OperationKind as ca, type OperationSpec as cb, type ParallelOpResult as cc, type ParamKind as cd, type ParamSpec as ce, type ParamStructure as cf, type PersistCtx as cg, type PersistOrigin as ch, type PlannedCommandMethod as ci, type ProjectionMap as cj, type ProjectionTransformOp as ck, type PutOptions as cl, type QueryContractMethodSpec as cm, type QueryEnvelopeResult as cn, type QueryKeyOf as co, type QueryMethod as cp, type QueryResult as cq, type RangeConditionSpec as cr, type ReadEnvelope as cs, type ReadOpCtx as ct, type ReadOpKind as cu, type ReadOperationType as cv, type ReadRouteDescriptor as cw, type ReadRouteOptions as cx, type ReadRouteResult as cy, type RecordedCompose as cz, type BatchWriteExecItem as d, mintContractKeyFieldRef as d$, type ViewSourceSlice as d0, type WhenSpec as d1, type WriteCtx as d2, type WriteDescriptor as d3, type WriteEnvelope as d4, type WriteInput as d5, type WriteKind as d6, type WriteLifecyclePhase as d7, type WriteMiddleware as d8, type WriteOperationType as d9, from as dA, getEntityWrites as dB, gsi as dC, identity as dD, isColumn as dE, isCommandModelContract as dF, isCommandPlan as dG, isContractComposeNode as dH, isContractFromRef as dI, isContractKeyFieldRef as dJ, isContractKeyRef as dK, isContractParamRef as dL, isEntityWritesDefinition as dM, isKeySegment as dN, isLifecycleContract as dO, isMaintainTrigger as dP, isMutationFragment as dQ, isMutationInputRef as dR, isParam as dS, isPlannedCommandMethod as dT, isQueryModelContract as dU, isRetryableError as dV, isRetryableTransactionCancellation as dW, k as dX, key as dY, lifecyclePhaseForIntent as dZ, maintainTrigger as d_, type WriteRecorder as da, type WriteResultProjection as db, attachModelClass as dc, buildDeleteInput as dd, buildMaintenanceGraph as de, buildPutInput as df, buildUpdateInput as dg, collectViewDefinitions as dh, compileFragment as di, compileMutationPlan as dj, compileSingleFragmentPlan as dk, cond as dl, contractOfMethodSpec as dm, definePlan as dn, entityWrites as dp, executeBatchGet as dq, executeBatchWrite as dr, executeCommandMethod as ds, executeDelete as dt, executeKeyedBatchGet as du, executePut as dv, executeQueryMethod as dw, executeRangeFanout as dx, executeTransaction as dy, executeUpdate as dz, type BatchExecOptions as e, mintContractParamRef as e0, mutation as e1, param as e2, preview as e3, publicCommandModel as e4, publicQueryModel as e5, query as e6, resolveLifecycle as e7, wholeKeysSentinel as e8, DDBModel as f, type PrimaryKeyOf as g, type RequestContext as h, type Middleware as i, type ReadRequestKind as j, type CtxModel as k, type ReadParams as l, type ReadRequestCtx as m, type RetryPolicy as n, type ExecutionPlanSpec as o, type EntityMetadata as p, type ModelKind as q, type DynamoType as r, type ProjectionTransform as s, type MaintainEvent as t, type MembershipPredicate as u, type MaintainConsistency as v, type MaintainUpdateMode as w, type MembershipPredicateOp as x, type RelationOptions as y, type AggregateValue as z };
6812
+ export { type FaultSpec as $, type AggregateOptions as A, type BatchGetExecInput as B, type ChangeEvent as C, type DynamoDBOperation as D, type Executor as E, type FieldOptions as F, type GsiDefinition as G, type SelectBuilderSpec as H, type Item as I, type RawCondition as J, type KeyDefinition as K, type TransactionSpec as L, type ModelStatic as M, type Manifest as N, type RetryOverride as O, type PutInput as P, type ExecutionPlan as Q, type ReadExecOptions as R, type SelectableOf as S, type TransactWriteExecItem as T, type UpdateInput as U, type FieldMetadata as V, type WriteExecOptions as W, type ResolvedKey as X, type CdcEmulatorOptions as Y, type ChangeHandler as Z, type Unsubscribe as _, type ExecutorResult as a, type Connection as a$, type ConcurrentRecomputeRef as a0, type EventLog as a1, type ReplayOptions as a2, type ShardId as a3, type ViewDefinition as a4, type RelationMetadata as a5, type TransactionItemSpec as a6, type MaintainEffect as a7, type Param as a8, type ParamDescriptor as a9, BatchGetResult as aA, type BatchPutRequest as aB, type BatchResult as aC, type BatchWriteRequest as aD, CONTRACT_RANGE_FANOUT_CONCURRENCY as aE, type CdcMode as aF, type CdcModelRegistry as aG, type CdcSubscribeHandlers as aH, type Change as aI, type ChangeBatch as aJ, type ChangeEventName as aK, type ClockMode as aL, type CollectionEffect as aM, type CollectionOptions as aN, type Column as aO, type ColumnMap as aP, type CommandContractMethodSpec as aQ, type CommandInputShape as aR, type CommandMethod as aS, type CommandPlan as aT, type CommandResolutionTarget as aU, type CommandResultKind as aV, type CommandSelectShape as aW, type CompiledFragment as aX, type ComposeSpec as aY, type CondSlot as aZ, type ConditionCheckInput as a_, type DefinitionMap as aa, type OperationDefinition as ab, type WriteDefinitionOptions as ac, type PartialQueryKeyOf as ad, type StrictSelectSpec as ae, type ReadDefinitionOptions as af, type EntityInput as ag, type UniqueQueryKeyOf as ah, type EntityRef as ai, type ConditionInput as aj, type QueryModelContract as ak, type QueryMethodSpec as al, type CommandModelContract as am, type CommandMethodSpec as an, type ContractSpec as ao, type QuerySpec as ap, type CommandSpec as aq, type ContextSpec as ar, type OperationsDocument as as, type AnyOperationDefinition as at, type BridgeBundle as au, type ConditionSpec as av, type AggregateMetadata as aw, type BatchDeleteRequest as ax, type BatchGetOptions as ay, type BatchGetRequest as az, type WriteResult as b, type MembershipEffect as b$, type ContractCallSignature as b0, type ContractCardinality as b1, type ContractCommandParams as b2, type ContractCommandResult as b3, type ContractComposeNode as b4, type ContractFromRef as b5, type ContractInputArity as b6, type ContractItem as b7, type ContractKeyFieldRef as b8, type ContractKeyInput as b9, type FilterInput as bA, type FilterSpec as bB, type FragmentInput as bC, type GsiDefinitionMarker as bD, type GsiOptions as bE, type IdempotencyEffect as bF, type InProcessWriteDescriptor as bG, type InputArity as bH, type KeyDefinitionMarker as bI, type KeySegment as bJ, type KeySlot as bK, type KeyStructure as bL, type KeyedResult as bM, LIFECYCLE_CONTRACT_MARKER as bN, type LifecycleContract as bO, type LifecycleEffects as bP, type LiteralParam as bQ, type MaintainItem as bR, type MaintainTrigger as bS, type MaintenanceGraph as bT, type ManifestEntity as bU, type ManifestField as bV, type ManifestFieldType as bW, type ManifestGsi as bX, type ManifestKey as bY, type ManifestRelation as bZ, type ManifestTable as b_, type ContractKeyRef as ba, type ContractKeySpec as bb, type ContractKind as bc, type ContractMethodOp as bd, type ContractParamRef as be, type ContractQueryParams as bf, type ContractResolution as bg, type CounterAggregate as bh, type CounterEffect as bi, type CtxBase as bj, DEFAULT_MAX_ATTEMPTS as bk, DEFAULT_RETRY_POLICY as bl, type DeleteOptions as bm, type DeriveEffect as bn, type DerivedEdgeWrite as bo, type DerivedUpdate as bp, type DescriptorBinding as bq, ENTITY_WRITES_MARKER as br, type EdgeEffect as bs, type EffectPath as bt, type EmbeddedMetadata as bu, type EmitEffect as bv, type EntityWritesDefinition as bw, type EntityWritesShape as bx, type ExecutableCommandContract as by, type ExecutableQueryContract as bz, type DeleteInput as c, TransactionContext as c$, type ModelRef as c0, type MutateMode as c1, type MutateOptions as c2, type MutateParallelResult as c3, type MutateTransactionResult as c4, type MutationBody as c5, type MutationDescriptorMap as c6, type MutationFragment as c7, type MutationInputProxy as c8, type MutationInputRef as c9, type ReadRouteResult as cA, type RecordedCompose as cB, type RelationBuilder as cC, type RelationConsistency as cD, type RelationLimitOptions as cE, type RelationPattern as cF, type RelationProjection as cG, type RelationReadOptions as cH, type RelationSelect as cI, type RelationSpec as cJ, type RelationUpdateMode as cK, type RelationWriteOptions as cL, type RequiresEffect as cM, type Resolution as cN, type RetryInfo as cO, type RetryOperationKind as cP, SPEC_VERSION as cQ, type SegmentSpec as cR, type SegmentedKey as cS, type SelectBuilder as cT, type SelectOf as cU, type SnapshotEffect as cV, type StartingPosition as cW, type StreamViewType as cX, type StringParam as cY, type SubscribeHandler as cZ, type SubscribeHandlers as c_, type MutationIntent as ca, type NumberParam as cb, type OperationKind as cc, type OperationSpec as cd, type ParallelOpResult as ce, type ParamKind as cf, type ParamSpec as cg, type ParamStructure as ch, type PersistCtx as ci, type PersistOrigin as cj, type PlannedCommandMethod as ck, type ProjectionMap as cl, type ProjectionTransformOp as cm, type PutOptions as cn, type QueryContractMethodSpec as co, type QueryEnvelopeResult as cp, type QueryKeyOf as cq, type QueryMethod as cr, type QueryResult as cs, type RangeConditionSpec as ct, type ReadEnvelope as cu, type ReadOpCtx as cv, type ReadOpKind as cw, type ReadOperationType as cx, type ReadRouteDescriptor as cy, type ReadRouteOptions as cz, type BatchWriteExecItem as d, isRetryableTransactionCancellation as d$, type TransactionItemType as d0, type UniqueEffect as d1, type Updatable as d2, type UpdateOptions as d3, type ViewSourceSlice as d4, type WhenSpec as d5, type WriteCtx as d6, type WriteDescriptor as d7, type WriteEnvelope as d8, type WriteInput as d9, executePut as dA, executeQueryMethod as dB, executeRangeFanout as dC, executeTransaction as dD, executeUpdate as dE, from as dF, getEntityWrites as dG, gsi as dH, identity as dI, isColumn as dJ, isCommandModelContract as dK, isCommandPlan as dL, isContractComposeNode as dM, isContractFromRef as dN, isContractKeyFieldRef as dO, isContractKeyRef as dP, isContractParamRef as dQ, isEntityWritesDefinition as dR, isKeySegment as dS, isLifecycleContract as dT, isMaintainTrigger as dU, isMutationFragment as dV, isMutationInputRef as dW, isParam as dX, isPlannedCommandMethod as dY, isQueryModelContract as dZ, isRetryableError as d_, type WriteKind as da, type WriteLifecyclePhase as db, type WriteMiddleware as dc, type WriteOperationType as dd, type WriteRecorder as de, type WriteResultProjection as df, attachModelClass as dg, buildDeleteInput as dh, buildMaintenanceGraph as di, buildPutInput as dj, buildSubscribeHandler as dk, buildUpdateInput as dl, collectViewDefinitions as dm, compileFragment as dn, compileMutationPlan as dp, compileSingleFragmentPlan as dq, cond as dr, contractOfMethodSpec as ds, definePlan as dt, entityWrites as du, executeBatchGet as dv, executeBatchWrite as dw, executeCommandMethod as dx, executeDelete as dy, executeKeyedBatchGet as dz, type BatchExecOptions as e, k as e0, key as e1, lifecyclePhaseForIntent as e2, maintainTrigger as e3, mintContractKeyFieldRef as e4, mintContractParamRef as e5, mutation as e6, param as e7, preview as e8, publicCommandModel as e9, publicQueryModel as ea, query as eb, resolveLifecycle as ec, wholeKeysSentinel as ed, DDBModel as f, type PrimaryKeyOf as g, type RequestContext as h, type Middleware as i, type ReadRequestKind as j, type CtxModel as k, type ReadParams as l, type ReadRequestCtx as m, type RetryPolicy as n, type ExecutionPlanSpec as o, type EntityMetadata as p, type ModelKind as q, type DynamoType as r, type ProjectionTransform as s, type MaintainEvent as t, type MembershipPredicate as u, type MaintainConsistency as v, type MaintainUpdateMode as w, type MembershipPredicateOp as x, type RelationOptions as y, type AggregateValue as z };
@@ -0,0 +1,167 @@
1
+ # GraphDDB — CDC Projection (typed-consumer-IF)
2
+
3
+ CDC projection lets you turn a model's change stream into **typed records** you can
4
+ push to an **external sink** — BigQuery, OpenSearch, S3, Neptune — for analytics or
5
+ search, decoupled from the table. Delivery is asynchronous over CDC (DynamoDB Streams)
6
+ and must tolerate at-least-once semantics (a redelivered event must not double-write).
7
+
8
+ graphddb's job here is narrow and deliberate: **parse a change event into a typed
9
+ record**. It does *not* subscribe to the stream, write to the sink, or de-duplicate —
10
+ those belong to your infrastructure. This keeps the library out of your delivery
11
+ semantics (upsert, idempotency, dedup) while still giving you fully typed access to
12
+ what changed.
13
+
14
+ ## The boundary
15
+
16
+ graphddb owns the **parse → typed record** contract, and nothing downstream of it.
17
+
18
+ | graphddb (the contract) | you (the runtime) |
19
+ | --- | --- |
20
+ | Enable CDC parse on a source model (`@cdcProjected`) | Subscribe to the CDC stream (emulator / DynamoDB Streams) |
21
+ | Parse `oldImage` / `newImage` into typed instances | Write to the sink (upsert / delete) |
22
+ | Route an event to its owning model | Idempotency store (dedup / at-least-once) |
23
+ | Produce the `(event) => [old, new]` typed mapper | Actual delivery and retry decisions |
24
+
25
+ What graphddb hands you is a pure function (and a batch handler built on it) — never a
26
+ subscription, a sink write, or a dedup store.
27
+
28
+ ## API
29
+
30
+ ### 1. `@cdcProjected()` — mark a source model
31
+
32
+ Enables CDC parse on the model. `fromChange` and `subscribe` typing only apply to a
33
+ `@cdcProjected` model (calling `fromChange` on an un-annotated model is an error). The
34
+ decorator has no runtime effect — it only marks the model as CDC-parseable.
35
+
36
+ ```ts
37
+ import { model, cdcProjected, DDBModel, string, number, datetime, key, k } from 'graphddb';
38
+
39
+ @model({ table: 'Orders', prefix: 'ORDER' })
40
+ @cdcProjected()
41
+ class OrderModel extends DDBModel {
42
+ static readonly keys = key<{ orderId: string }>((c) => ({
43
+ pk: k`ORDER#${c.orderId}`, sk: k`META`,
44
+ }));
45
+
46
+ @string orderId!: string;
47
+ @string status!: string;
48
+ @number amountTotal!: number;
49
+ @datetime updatedAt!: Date;
50
+ }
51
+ ```
52
+
53
+ ### 2. `Model.fromChange(event)` — procedural, single event
54
+
55
+ Takes a `ChangeEvent` and returns the `[oldRecord, newRecord]` tuple, parsing
56
+ `oldImage` / `newImage` into typed model instances (the same hydration used for reads —
57
+ ISO strings become `Date`, embedded objects are reconstructed). It returns **all
58
+ fields** (there is no field projection — see [Design notes](#design-notes)).
59
+
60
+ ```ts
61
+ const [oldRecord, newRecord] = OrderModel.fromChange(event);
62
+ // ^ OrderModel | null ^ OrderModel | null
63
+
64
+ // One side is null depending on the event kind:
65
+ // INSERT → [null, newRecord]
66
+ // MODIFY → [oldRecord, newRecord]
67
+ // REMOVE → [oldRecord, null ]
68
+ // (event for a different model) → [null, null] ← self-routing
69
+
70
+ if (oldRecord || newRecord) {
71
+ if (!newRecord) {
72
+ // REMOVE — delete; derive the sink key from oldRecord
73
+ } else {
74
+ // INSERT | MODIFY — upsert; diff against oldRecord if you need change detection
75
+ }
76
+ }
77
+ ```
78
+
79
+ A valid event for this model always has at least one non-null side, so `[null, null]`
80
+ is an unambiguous "this event is not for this model" signal. You don't need a separate
81
+ guard — `fromChange` does routing **and** parse in one call.
82
+
83
+ ### 3. `DDBModel.subscribe(handlers)` — declarative, batch
84
+
85
+ The third sibling of `query` / `mutate` (GraphQL's query / mutation / **subscription**).
86
+ It takes a handler map keyed by model name. Unlike `query` / `mutate`, which hit
87
+ DynamoDB, **`subscribe` does not subscribe**: it *returns* a `ChangeHandler`
88
+ (`(batch) => Promise<BatchResult>`) that you mount on your own stream source.
89
+
90
+ ```ts
91
+ // graphddb side: build a batch handler that parses + routes + dispatches (no sink, no dedup)
92
+ const handler = DDBModel.subscribe({
93
+ OrderModel: (old, neu) => { // old, neu: OrderModel | null (typed via the generated registry)
94
+ if (!neu) return opensearch.delete({ id: old!.orderId });
95
+ return opensearch.index({ id: neu.orderId, body: { status: neu.status } });
96
+ },
97
+ UserModel: (old, neu) => { /* old, neu: UserModel | null */ },
98
+ });
99
+
100
+ // your side: the real subscription lives here (delivery is yours)
101
+ emulator.subscribe(handler); // local (CDC emulator)
102
+ // export const lambdaHandler = handler; // production (DynamoDB Streams → Lambda)
103
+ ```
104
+
105
+ The returned handler:
106
+
107
+ - loops the batch and routes each event to its owning model;
108
+ - typed-parses via that model's `fromChange`, then calls the matching handler;
109
+ - if a handler throws, records that record's `sequenceNumber` into `batchItemFailures`
110
+ (the Streams / Lambda partial-batch-failure protocol, so only the failed records are
111
+ redelivered);
112
+ - **never writes a sink and never dedups** — the `opensearch.*` calls above are your code.
113
+
114
+ Recording `batchItemFailures` is the delivery *protocol* shape (which records to
115
+ redeliver), not delivery *semantics* (upsert / dedup), so it stays inside the boundary.
116
+
117
+ ### 4. `CdcModelRegistry` — generated type registry
118
+
119
+ The `subscribe` handlers are keyed by model name (a string), so type inference needs a
120
+ type-level "model name → type" map. graphddb's code generation emits it for you as a
121
+ module augmentation:
122
+
123
+ ```ts
124
+ // generated by `graphddb generate` (you do not write this)
125
+ declare module 'graphddb' {
126
+ interface CdcModelRegistry {
127
+ OrderModel: OrderModel;
128
+ UserModel: UserModel;
129
+ }
130
+ }
131
+ ```
132
+
133
+ With it in your build, `subscribe`'s keys are constrained to your registered models
134
+ (typos and unknown model names are compile errors) and each handler's `old` / `neu` is
135
+ inferred from the key.
136
+
137
+ ## The GraphQL trio
138
+
139
+ | GraphQL | DDBModel | Executed by | Returns |
140
+ | --- | --- | --- | --- |
141
+ | query | `DDBModel.query(envelope)` | graphddb (hits DynamoDB) | result object |
142
+ | mutation | `DDBModel.mutate(envelope)` | graphddb (hits DynamoDB) | result object |
143
+ | subscription | `DDBModel.subscribe(handlers)` | **you** (mount it) | `ChangeHandler` |
144
+
145
+ Only subscription is "executed by you, returns a handler rather than a result" —
146
+ because CDC delivery lives outside the boundary.
147
+
148
+ ## Design notes
149
+
150
+ - **No field projection (`fromChange` returns all fields).** Narrowing the fields does
151
+ not reduce network cost (the image is already on the stream), so there is no `select`.
152
+ Shape the record however you like in plain TS inside your handler.
153
+ - **`[null, null]` for routing.** A valid event always has a non-null side, so both
154
+ being null is an unambiguous "not for this model" signal — `fromChange` handles
155
+ routing and parse without an extra guard.
156
+ - **`subscribe` returns a handler.** So that graphddb owns neither the subscription nor
157
+ delivery. The return type being a `ChangeHandler` (not a result) is the visible
158
+ expression of the boundary.
159
+ - **Naming.** `subscribe` completes the GraphQL query / mutation / subscription trio
160
+ (distinct from `emulator.subscribe(handler)`, which is the actual subscription).
161
+
162
+ ## See also
163
+
164
+ - [`spec.md` §7 — Relations & maintained access paths](./spec.md#7-relations)
165
+ - [`design-patterns.md` §10 — External Projection](./design-patterns.md#10-external-projection)
166
+ - [`cdc-emulator.md`](./cdc-emulator.md) — the CDC emulator that drives stream handlers locally
167
+ - [`class-hydration.md`](./class-hydration.md) — how raw items become typed instances
@@ -47,7 +47,7 @@ graph, the stream drain, and the rebuild planner consume every pattern uniformly
47
47
  | 7 | [Aggregate Counter](#7-aggregate-counter) | `@aggregate(..., { pattern: 'counter', value: count() })` | ✅ Phase 1 (`count()`) / stream (`max()`) |
48
48
  | 8 | [Versioned / Latest Pointer](#8-versioned--latest-pointer) | `@hasOne(... versionedLatest)` + `@hasMany(... versionedHistory)` | ✅ Phase 3 |
49
49
  | 9 | [Reverse Lookup](#9-reverse-lookup) | GSI (both directions) or `dualEdge` two-row sync | ✅ Phase 1 (GSI) / Phase 3 (`dualEdge`) |
50
- | 10 | [External Projection](#10-external-projection) | typed-consumer-IF contract (redesign see #153) | 🚧 redesign |
50
+ | 10 | [External Projection](#10-external-projection) | typed-consumer-IF contract (`@cdcProjected` + `fromChange` / `subscribe`) | |
51
51
 
52
52
  ---
53
53
 
@@ -494,23 +494,28 @@ OpenSearch, S3, Neptune — for analytics or search, decoupled from the table. T
494
494
  projection is asynchronous and must tolerate at-least-once delivery (a redelivered
495
495
  event must not double-write).
496
496
 
497
- **(b) How it is being redesigned (🚧 #153).** The previous `defineProjection` builder +
497
+ **(b) How to declare it in graphddb.** The previous `defineProjection` builder +
498
498
  `ProjectionSinkDrain` / `InMemoryProjectionSink` runtime baked the sink-delivery
499
499
  semantics (upsert, idempotency, dedup) into the library — a boundary overreach: the
500
500
  actual sink write happens inside the consumer's CDC infrastructure, outside graphddb.
501
- That runtime was **removed in 0.4.0** (issue #152). The redesign (issue #153) reframes
502
- external projection as a **typed-consumer-IF contract**: from a declared source +
503
- projection fields + idempotency-key extraction, graphddb emits a typed
504
- `(changeImage) => TypedProjectionRecord` mapper for the consumer to call inside its own
505
- sink delivery graphddb owns the *parse typed record* contract, not the delivery.
506
- External projection is neither a Model nor a relation, so it does not ride a decorator.
501
+ That runtime was **removed in 0.4.0** (issue #152). External projection is now a
502
+ **typed-consumer-IF contract** (issue #153): mark the source model with
503
+ `@cdcProjected()`, then parse a CDC change event into typed instances — `Model.fromChange(event)`
504
+ returns `[oldRecord, newRecord]`, and `DDBModel.subscribe(handlers)` (the GraphQL-style
505
+ sibling of `query` / `mutate`) returns a `ChangeHandler` the consumer mounts on its own
506
+ stream. graphddb owns the *parse typed record* contract; subscription, sink delivery,
507
+ and idempotency stay with the consumer. See [`cdc-projection.md`](./cdc-projection.md)
508
+ for the full design.
507
509
 
508
510
  **(c) Read & write behavior.** Out of scope for this library (the data is read from the
509
- sink; delivery + idempotency live in the consumer's CDC code). See #153 for the
510
- typed-consumer-IF contract design.
511
-
512
- **(d) Constraints & phase status.** 🚧 Redesign (#153). The sink runtime is gone in
513
- 0.4.0; the typed-consumer-IF replacement lands separately.
511
+ sink; delivery + idempotency live in the consumer's CDC code). graphddb only parses the
512
+ change event into a typed record; see [`cdc-projection.md`](./cdc-projection.md) for the
513
+ typed-consumer-IF contract.
514
+
515
+ **(d) Constraints & phase status.** ✅ Shipped (issue #153). The sink runtime was removed
516
+ in 0.4.0; the typed-consumer-IF replacement (`@cdcProjected` + `fromChange` + `subscribe`
517
+ + the generated `CdcModelRegistry`) is implemented and tested (unit + emulator-driven
518
+ integration).
514
519
 
515
520
  ---
516
521
 
package/docs/spec.md CHANGED
@@ -546,9 +546,10 @@ Shared vocabulary (confirmed across the epic):
546
546
  view model** — `materializedView` / `sparseView` — are declared with `@model({ kind })`
547
547
  + `@maintainedFrom` (§7.1 below). The **versioned** presets ride the `@hasOne` /
548
548
  `@hasMany` `pattern` discriminated union (`versionedLatest` / `versionedHistory`).
549
- External projection is being redesigned as a typed-consumer-IF contract (#153) and
550
- has no decorator. Unknown values on a relation are accepted by the open-ended union
551
- but not lowered.
549
+ External projection is a typed-consumer-IF contract (#153); it is not a relation
550
+ `pattern` the source model is marked with `@cdcProjected()` instead (see
551
+ [`cdc-projection.md`](./cdc-projection.md)). Unknown values on a relation are
552
+ accepted by the open-ended union but not lowered.
552
553
  - **`write.maintainedOn`** — cross-entity triggers `"<Entity>.<event>"`. The
553
554
  Entity segment is the **logical** entity name (`PostModel → 'Post'`; or the
554
555
  `@model({ prefix })` value sans `#`), and the event is one of
@@ -634,7 +635,11 @@ The latest/history key binding is derived from each target model's own `key` fie
634
635
  composed snapshot×2 IR. (A self-co-located form is a deferred follow-up.)
635
636
 
636
637
  External projection's old `defineProjection` / `ProjectionSinkDrain` runtime was
637
- **removed in 0.4.0** and is being redesigned as a typed-consumer-IF contract (#153).
638
+ **removed in 0.4.0** and is now a typed-consumer-IF contract (#153):
639
+ the source model is marked `@cdcProjected()`, `Model.fromChange(event)` parses a CDC
640
+ change event into `[oldRecord, newRecord]`, and `DDBModel.subscribe(handlers)` returns a
641
+ `ChangeHandler` the consumer mounts on its own stream (subscription / sink delivery /
642
+ idempotency stay with the consumer). See [`cdc-projection.md`](./cdc-projection.md).
638
643
 
639
644
  ### 7.2 Stream maintenance — outbox → CDC drain (`updateMode: 'stream'`, #130)
640
645