effect-dynamodb 1.24.0 → 1.25.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/dist/Aggregate.d.ts +5 -2
- package/dist/Aggregate.d.ts.map +1 -1
- package/dist/Aggregate.js +136 -53
- package/dist/Aggregate.js.map +1 -1
- package/dist/Batch.d.ts +14 -5
- package/dist/Batch.d.ts.map +1 -1
- package/dist/Batch.js +184 -119
- package/dist/Batch.js.map +1 -1
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +2 -4
- package/dist/Collection.js.map +1 -1
- package/dist/DynamoClient.d.ts.map +1 -1
- package/dist/DynamoClient.js +42 -14
- package/dist/DynamoClient.js.map +1 -1
- package/dist/Entity.d.ts +19 -13
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +287 -226
- package/dist/Entity.js.map +1 -1
- package/dist/EventStore.d.ts +416 -73
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +525 -181
- package/dist/EventStore.js.map +1 -1
- package/dist/Query.d.ts +47 -14
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +190 -68
- package/dist/Query.js.map +1 -1
- package/dist/Table.d.ts +56 -6
- package/dist/Table.d.ts.map +1 -1
- package/dist/Table.js +134 -54
- package/dist/Table.js.map +1 -1
- package/dist/Transaction.d.ts +8 -0
- package/dist/Transaction.d.ts.map +1 -1
- package/dist/Transaction.js +67 -22
- package/dist/Transaction.js.map +1 -1
- package/dist/VectorSearchEmulation.d.ts +3 -2
- package/dist/VectorSearchEmulation.d.ts.map +1 -1
- package/dist/VectorSearchEmulation.js +14 -2
- package/dist/VectorSearchEmulation.js.map +1 -1
- package/dist/internal/BoundQuery.d.ts +3 -2
- package/dist/internal/BoundQuery.d.ts.map +1 -1
- package/dist/internal/BoundQuery.js.map +1 -1
- package/dist/internal/BoundVectorQuery.d.ts +5 -1
- package/dist/internal/BoundVectorQuery.d.ts.map +1 -1
- package/dist/internal/BoundVectorQuery.js +17 -5
- package/dist/internal/BoundVectorQuery.js.map +1 -1
- package/dist/internal/EntityCombinators.d.ts +8 -5
- package/dist/internal/EntityCombinators.d.ts.map +1 -1
- package/dist/internal/EntityCombinators.js +8 -5
- package/dist/internal/EntityCombinators.js.map +1 -1
- package/dist/internal/EntityOps.d.ts +1 -1
- package/dist/internal/Expr.d.ts +4 -0
- package/dist/internal/Expr.d.ts.map +1 -1
- package/dist/internal/Expr.js +6 -4
- package/dist/internal/Expr.js.map +1 -1
- package/dist/internal/ExpressionLimits.d.ts +45 -0
- package/dist/internal/ExpressionLimits.d.ts.map +1 -0
- package/dist/internal/ExpressionLimits.js +89 -0
- package/dist/internal/ExpressionLimits.js.map +1 -0
- package/dist/internal/Hydrate.d.ts +62 -0
- package/dist/internal/Hydrate.d.ts.map +1 -0
- package/dist/internal/Hydrate.js +191 -0
- package/dist/internal/Hydrate.js.map +1 -0
- package/dist/internal/ItemSize.d.ts +60 -24
- package/dist/internal/ItemSize.d.ts.map +1 -1
- package/dist/internal/ItemSize.js +122 -39
- package/dist/internal/ItemSize.js.map +1 -1
- package/dist/internal/TransactWriteOps.d.ts +22 -46
- package/dist/internal/TransactWriteOps.d.ts.map +1 -1
- package/dist/internal/TransactWriteOps.js +26 -95
- package/dist/internal/TransactWriteOps.js.map +1 -1
- package/dist/internal/WriteDiagnosis.d.ts +353 -0
- package/dist/internal/WriteDiagnosis.d.ts.map +1 -0
- package/dist/internal/WriteDiagnosis.js +1028 -0
- package/dist/internal/WriteDiagnosis.js.map +1 -0
- package/dist/internal/sparse_check.js +43 -79
- package/dist/internal/sparse_check.js.map +1 -1
- package/package.json +2 -2
package/dist/EventStore.d.ts
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* `VersionConflict` retry policy, caller-supplied `expectedVersion`,
|
|
14
14
|
* decision-derived `additionalItems`, inline snapshots)
|
|
15
15
|
* - Stream indexes (`makeStream({ indexes })`): sub-streams ordered by a key
|
|
16
|
-
* derived from each event, on an LSI or GSI
|
|
17
|
-
*
|
|
16
|
+
* derived from each event, on an LSI or GSI, each projecting all of the
|
|
17
|
+
* event, all but its metadata, or only its keys — `readIndex`,
|
|
18
|
+
* `query.index`, and `indexDefinitions` for the `CreateTable` fragments
|
|
18
19
|
* - `fold` / `foldFrom` helpers for state reconstruction
|
|
19
20
|
*
|
|
20
21
|
* Built on the existing library primitives (DynamoSchema, KeyComposer, Query,
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
import type { AttributeDefinition, GlobalSecondaryIndex, LocalSecondaryIndex } from "@aws-sdk/client-dynamodb";
|
|
24
25
|
import * as DynamoSchema from "@effect-dynamodb/schema/DynamoSchema.js";
|
|
25
26
|
import { AdditionalItemConditionFailed, AppendTooLarge, type ConcurrentModification, DuplicateCommand, type OptimisticLockError, TransactionCancelled, type UniqueConstraintViolation, ValidationError, VersionConflict } from "@effect-dynamodb/schema/Errors.js";
|
|
26
|
-
import { type Duration, Effect, Option, Pipeable, Schedule, Schema } from "effect";
|
|
27
|
+
import { DateTime, type Duration, Effect, Option, Pipeable, Schedule, Schema } from "effect";
|
|
27
28
|
import { DynamoClient, type DynamoClientError } from "./DynamoClient.js";
|
|
28
29
|
import { type TransactWriteOp } from "./internal/TransactWriteOps.js";
|
|
29
30
|
import * as Query from "./Query.js";
|
|
@@ -58,6 +59,11 @@ export interface StreamEvent<A, M = Record<string, unknown> | undefined> {
|
|
|
58
59
|
readonly eventType: string;
|
|
59
60
|
readonly data: A;
|
|
60
61
|
readonly metadata: M;
|
|
62
|
+
/**
|
|
63
|
+
* ISO 8601 (UTC): when the event was appended, by the append's `Clock` — or
|
|
64
|
+
* the `eventTimestamps` entry it was appended with
|
|
65
|
+
* ({@link AppendOptionsBase.eventTimestamps}).
|
|
66
|
+
*/
|
|
61
67
|
readonly timestamp: string;
|
|
62
68
|
}
|
|
63
69
|
/**
|
|
@@ -130,7 +136,7 @@ export interface SnapshotConfig<TSchema extends Schema.Top = Schema.Top> {
|
|
|
130
136
|
* succeeds, at the `every` cadence. A failed snapshot write never fails the
|
|
131
137
|
* command; the snapshot simply lags until the next threshold crossing.
|
|
132
138
|
* - `"inline"` — the snapshot `Put` rides in the append's own transaction (see
|
|
133
|
-
* {@link
|
|
139
|
+
* {@link AppendOptionsBase.snapshot}), on every append, or at the `every` cadence
|
|
134
140
|
* when one is set. The snapshot is then current after every command that
|
|
135
141
|
* writes one, so {@link EventStream.readLatest} loads state in one request.
|
|
136
142
|
* The snapshot counts towards the transaction's item and size limits.
|
|
@@ -153,7 +159,7 @@ export interface SnapshotSettings {
|
|
|
153
159
|
* redelivery whose `decide` returns no events against the loaded state never
|
|
154
160
|
* appends, so it succeeds as a no-op instead of failing with
|
|
155
161
|
* `DuplicateCommand`. Whether a no-op redelivery needs to be told apart is the
|
|
156
|
-
* application's call (see {@link
|
|
162
|
+
* application's call (see {@link CommandOptionsBase.commandId}).
|
|
157
163
|
*
|
|
158
164
|
* The sentinel is co-located in the stream's own partition, so `commandId`
|
|
159
165
|
* uniqueness is scoped to the stream (which is what "have I already applied this
|
|
@@ -178,14 +184,46 @@ export interface AppendIdempotency {
|
|
|
178
184
|
readonly ttl?: Duration.Duration | string;
|
|
179
185
|
}
|
|
180
186
|
/**
|
|
181
|
-
*
|
|
187
|
+
* The metadata of an {@link EventStream.append}: one value shared by every
|
|
188
|
+
* event (`metadata`), or one entry per event (`eventMetadata`) — never both.
|
|
189
|
+
*
|
|
190
|
+
* Passing both is a compile error, and a `ValidationError` at runtime for an
|
|
191
|
+
* untyped caller. `undefined` counts as absent for either, so
|
|
192
|
+
* `{ metadata: undefined, eventMetadata: [...] }` is not "both".
|
|
193
|
+
*/
|
|
194
|
+
export type AppendMetadataOptions<TMetadata> = {
|
|
195
|
+
/**
|
|
196
|
+
* Metadata written onto **every** event of the append — a copy per item,
|
|
197
|
+
* encoded once through the stream's metadata schema when one is
|
|
198
|
+
* configured. For metadata that belongs to each event (an event id, the
|
|
199
|
+
* original occurrence time of an imported event) use `eventMetadata`:
|
|
200
|
+
* packing a list into `metadata` stores every entry on every item, so
|
|
201
|
+
* stored metadata grows with the square of the append size.
|
|
202
|
+
*/
|
|
203
|
+
readonly metadata?: TMetadata;
|
|
204
|
+
readonly eventMetadata?: undefined;
|
|
205
|
+
} | {
|
|
206
|
+
readonly metadata?: undefined;
|
|
207
|
+
/**
|
|
208
|
+
* Per-event metadata: entry `i` is written onto event `i` (and read back
|
|
209
|
+
* as that event's `StreamEvent.metadata`). Each defined entry is encoded
|
|
210
|
+
* through the stream's metadata schema, as `metadata` is; an `undefined`
|
|
211
|
+
* entry writes its event without a `metadata` attribute.
|
|
212
|
+
*
|
|
213
|
+
* Must have exactly one entry per event — a length mismatch fails with
|
|
214
|
+
* `ValidationError` before anything is read or written, also for an
|
|
215
|
+
* append of no events (where only `[]` is accepted).
|
|
216
|
+
*/
|
|
217
|
+
readonly eventMetadata?: ReadonlyArray<TMetadata | undefined>;
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* The options of {@link EventStream.append} other than its metadata (see
|
|
221
|
+
* {@link AppendMetadataOptions}).
|
|
182
222
|
*
|
|
183
223
|
* `TState` is the stream's snapshot state type (`never` for a stream without a
|
|
184
224
|
* `snapshot` config, so `snapshot` cannot be supplied there).
|
|
185
225
|
*/
|
|
186
|
-
export interface
|
|
187
|
-
/** Per-append metadata, validated against the stream's metadata schema when configured. */
|
|
188
|
-
readonly metadata?: TMetadata;
|
|
226
|
+
export interface AppendOptionsBase<TState = never> {
|
|
189
227
|
/**
|
|
190
228
|
* Caller-owned transact items committed atomically with the events — the same
|
|
191
229
|
* op union `Transaction.transactWrite` accepts (`EntityPut`, `EntityDelete`,
|
|
@@ -224,7 +262,34 @@ export interface AppendOptions<TMetadata, TState = never> {
|
|
|
224
262
|
* is `never`) it dies with `[EDD-9026]`, as `writeSnapshot` does.
|
|
225
263
|
*/
|
|
226
264
|
readonly snapshot?: TState;
|
|
265
|
+
/**
|
|
266
|
+
* Per-event timestamps: entry `i` is written as event `i`'s `timestamp` (and
|
|
267
|
+
* read back as that event's `StreamEvent.timestamp`), for history imported
|
|
268
|
+
* with its original occurrence times. An `undefined` entry keeps the
|
|
269
|
+
* append's `Clock` time. Independent of `metadata` / `eventMetadata`.
|
|
270
|
+
*
|
|
271
|
+
* Written with `DateTime.formatIso` — the format the append's own time is
|
|
272
|
+
* written in — so a zoned `DateTime` is stored as its UTC instant.
|
|
273
|
+
*
|
|
274
|
+
* - Must have exactly one entry per event, each a valid `DateTime` (finite
|
|
275
|
+
* epoch) or `undefined` — anything else fails with `ValidationError` before anything is read or
|
|
276
|
+
* written, also for an append of no events (where only `[]` is accepted).
|
|
277
|
+
* - Only the event items change: the idempotency sentinel, its TTL and the
|
|
278
|
+
* inline snapshot record when the append was written.
|
|
279
|
+
* - No ordering is enforced. `version` orders a stream; timestamps may
|
|
280
|
+
* repeat, run backwards or lie in the future, as imported history can.
|
|
281
|
+
*/
|
|
282
|
+
readonly eventTimestamps?: ReadonlyArray<DateTime.DateTime | undefined>;
|
|
227
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Options accepted by {@link EventStream.append}: {@link AppendOptionsBase}
|
|
286
|
+
* plus the exclusive `metadata` / `eventMetadata` pair
|
|
287
|
+
* ({@link AppendMetadataOptions}).
|
|
288
|
+
*
|
|
289
|
+
* A type alias rather than an interface, since it is an intersection with a
|
|
290
|
+
* union: an interface cannot extend it.
|
|
291
|
+
*/
|
|
292
|
+
export type AppendOptions<TMetadata, TState = never> = AppendOptionsBase<TState> & AppendMetadataOptions<TMetadata>;
|
|
228
293
|
/** Error channel of {@link EventStream.append}. */
|
|
229
294
|
export type AppendError = VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
|
|
230
295
|
/**
|
|
@@ -298,8 +363,10 @@ export interface LatestState<TState, TEvent, M = Record<string, unknown> | undef
|
|
|
298
363
|
* - `undefined` leaves the event out of the index (a sparse index).
|
|
299
364
|
* - An empty string (or any other value) fails the append with
|
|
300
365
|
* `ValidationError` before anything is written — DynamoDB rejects empty key
|
|
301
|
-
* values
|
|
302
|
-
*
|
|
366
|
+
* values — as does a `key` that throws. A key over DynamoDB's 1024-byte
|
|
367
|
+
* sort-key limit is sent and rejected by DynamoDB; the append then fails
|
|
368
|
+
* with `ValidationError` naming the index and the event (#144), and nothing
|
|
369
|
+
* is written.
|
|
303
370
|
*
|
|
304
371
|
* The key is derived only when an event is appended. Events are never
|
|
305
372
|
* rewritten, so changing `key` later leaves events already stored under their
|
|
@@ -322,9 +389,9 @@ export type StreamIndexKey<TEvent> = (event: TEvent, version: number) => string
|
|
|
322
389
|
* An LSI must be created with the table. A table with any LSI caps the item
|
|
323
390
|
* collection of **every** partition key value in it at 10 GB — not only the
|
|
324
391
|
* indexed stream's partitions, but every stream's (indexed or not) and every
|
|
325
|
-
* entity partition sharing the table; a write that would exceed it fails.
|
|
326
|
-
*
|
|
327
|
-
*
|
|
392
|
+
* entity partition sharing the table; a write that would exceed it fails. What
|
|
393
|
+
* the index copies from each event is its `projection` — see
|
|
394
|
+
* {@link StreamIndexProjection}.
|
|
328
395
|
*/
|
|
329
396
|
export interface LocalStreamIndexConfig<TEvent> {
|
|
330
397
|
readonly type?: "lsi" | undefined;
|
|
@@ -333,13 +400,19 @@ export interface LocalStreamIndexConfig<TEvent> {
|
|
|
333
400
|
/** An LSI uses the table partition key; supplying `pk` is refused (`[EDD-9063]`). */
|
|
334
401
|
readonly pk?: undefined;
|
|
335
402
|
readonly key: StreamIndexKey<TEvent>;
|
|
403
|
+
/**
|
|
404
|
+
* What the index copies from each event — see {@link StreamIndexProjection}.
|
|
405
|
+
* Default `"all"`. Must match the deployed index.
|
|
406
|
+
*/
|
|
407
|
+
readonly projection?: StreamIndexProjection | undefined;
|
|
336
408
|
}
|
|
337
409
|
/**
|
|
338
410
|
* A stream index on a **global** secondary index. `append` writes the stream's
|
|
339
411
|
* partition key value into the `pk` attribute of each indexed event, so the
|
|
340
412
|
* index is scoped to the same stream: the eventually consistent equivalent of
|
|
341
|
-
* an LSI, without the LSI's 10 GB item-collection cap.
|
|
342
|
-
* read of it
|
|
413
|
+
* an LSI, without the LSI's 10 GB item-collection cap. DynamoDB rejects a
|
|
414
|
+
* strongly consistent read of it, diagnosed as a `ValidationError` naming the
|
|
415
|
+
* index.
|
|
343
416
|
*
|
|
344
417
|
* Unlike an LSI, the GSI itself can be added to an existing table
|
|
345
418
|
* (`UpdateTable`), but that does not index the stream's existing events: the
|
|
@@ -350,6 +423,7 @@ export interface LocalStreamIndexConfig<TEvent> {
|
|
|
350
423
|
* - `pk` — the attribute carrying the stream's partition key (the GSI's HASH key).
|
|
351
424
|
* - `sk` — the attribute carrying the derived key (the GSI's RANGE key).
|
|
352
425
|
* - `key` — see {@link StreamIndexKey}.
|
|
426
|
+
* - `projection` — see {@link StreamIndexProjection}.
|
|
353
427
|
*/
|
|
354
428
|
export interface GlobalStreamIndexConfig<TEvent> {
|
|
355
429
|
readonly type: "gsi";
|
|
@@ -357,6 +431,11 @@ export interface GlobalStreamIndexConfig<TEvent> {
|
|
|
357
431
|
readonly pk: string;
|
|
358
432
|
readonly sk: string;
|
|
359
433
|
readonly key: StreamIndexKey<TEvent>;
|
|
434
|
+
/**
|
|
435
|
+
* What the index copies from each event — see {@link StreamIndexProjection}.
|
|
436
|
+
* Default `"all"`. Must match the deployed index.
|
|
437
|
+
*/
|
|
438
|
+
readonly projection?: StreamIndexProjection | undefined;
|
|
360
439
|
}
|
|
361
440
|
/**
|
|
362
441
|
* One entry of `makeStream({ indexes })`: a sub-stream of the stream's events,
|
|
@@ -364,30 +443,95 @@ export interface GlobalStreamIndexConfig<TEvent> {
|
|
|
364
443
|
* and {@link GlobalStreamIndexConfig}.
|
|
365
444
|
*/
|
|
366
445
|
export type StreamIndexConfig<TEvent> = LocalStreamIndexConfig<TEvent> | GlobalStreamIndexConfig<TEvent>;
|
|
446
|
+
/**
|
|
447
|
+
* What a stream index copies from each event (`projection` on an entry of
|
|
448
|
+
* `makeStream({ indexes })`). {@link indexDefinitions} emits the matching
|
|
449
|
+
* `Projection`, and the reads choose their path from it.
|
|
450
|
+
*
|
|
451
|
+
* | Value | Emitted `Projection` | Read |
|
|
452
|
+
* |---|---|---|
|
|
453
|
+
* | `"all"` (default) | `ALL` | one query |
|
|
454
|
+
* | `"withoutMetadata"` | `INCLUDE` of every event attribute except `metadata` | one query; `metadata` is `undefined` (and typed so) |
|
|
455
|
+
* | `"keys"` | `INCLUDE ["__edd_e__"]` — **not** `KEYS_ONLY` | LSI: one query with `Select: ALL_ATTRIBUTES` (DynamoDB fetches each event from the table). GSI: the query, then `BatchGetItem` for the events of each page, in index order |
|
|
456
|
+
*
|
|
457
|
+
* `__edd_e__` is always projected because every read filters on it, and
|
|
458
|
+
* DynamoDB refuses a filter on an attribute an index does not project.
|
|
459
|
+
*
|
|
460
|
+
* `metadata` is often an event's largest attribute; `"withoutMetadata"` keeps
|
|
461
|
+
* it out of the index, and `"keys"` keeps the index entries minimal at the cost
|
|
462
|
+
* of a table read per event read (rounded up to 4 KB each).
|
|
463
|
+
*
|
|
464
|
+
* **The declared projection must match the deployed index.** The library does
|
|
465
|
+
* not migrate: an LSI's projection is fixed when the table is created, and a
|
|
466
|
+
* GSI's cannot be updated (it must be deleted and created again). Tables
|
|
467
|
+
* deployed with CDK or Terraform copy `Projection` from
|
|
468
|
+
* {@link indexDefinitions}' output. An index deployed narrower than declared
|
|
469
|
+
* fails reads: DynamoDB refuses the `__edd_e__` filter ("does not project one
|
|
470
|
+
* or more filter attributes"), or rows lack event attributes, which the read
|
|
471
|
+
* reports as a `ValidationError` (`operation: "EventStore.index.decode"`)
|
|
472
|
+
* naming the index.
|
|
473
|
+
*/
|
|
474
|
+
export type StreamIndexProjection = "all" | "withoutMetadata" | "keys";
|
|
367
475
|
/**
|
|
368
476
|
* The runtime settings of one stream index, as exposed on
|
|
369
477
|
* {@link EventStream.indexes}. `pk` is the index's partition key attribute:
|
|
370
478
|
* the table's `pk` for an LSI, the configured attribute for a GSI.
|
|
479
|
+
* `projection` is resolved: `"all"` when the index declares none.
|
|
371
480
|
*/
|
|
372
481
|
export interface StreamIndexSettings {
|
|
373
482
|
readonly type: "lsi" | "gsi";
|
|
374
483
|
readonly index: string;
|
|
375
484
|
readonly pk: string;
|
|
376
485
|
readonly sk: string;
|
|
486
|
+
readonly projection: StreamIndexProjection;
|
|
377
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* The `metadata` type of an event read through a stream index of projection
|
|
490
|
+
* `P`: `undefined` for a `"withoutMetadata"` index, which does not hold it,
|
|
491
|
+
* the stream's {@link StreamMetadata} otherwise.
|
|
492
|
+
*/
|
|
493
|
+
export type IndexEventMetadata<TMetadata, P extends StreamIndexProjection> = [P] extends [
|
|
494
|
+
"withoutMetadata"
|
|
495
|
+
] ? undefined : StreamMetadata<TMetadata>;
|
|
496
|
+
/** @internal The `projection` an index config declares (`"all"` when none). */
|
|
497
|
+
type ProjectionOf<C> = C extends {
|
|
498
|
+
readonly projection: infer P extends StreamIndexProjection;
|
|
499
|
+
} ? P : "all";
|
|
500
|
+
/**
|
|
501
|
+
* The projection of each index of a `makeStream({ indexes })` config, by name.
|
|
502
|
+
* `"all"` for an index that declares none.
|
|
503
|
+
*/
|
|
504
|
+
export type IndexProjectionsOf<TIndexes> = {
|
|
505
|
+
readonly [K in keyof TIndexes & string]: ProjectionOf<TIndexes[K]>;
|
|
506
|
+
};
|
|
507
|
+
/** @internal The members a stream index entry may declare. */
|
|
508
|
+
type StreamIndexConfigKey = keyof LocalStreamIndexConfig<unknown> | keyof GlobalStreamIndexConfig<unknown>;
|
|
509
|
+
/**
|
|
510
|
+
* The `indexes` parameter of {@link makeStream}: each entry is a
|
|
511
|
+
* {@link StreamIndexConfig}, and a member it does not declare (a misspelled
|
|
512
|
+
* `projection`, say) is typed `never`, so it is a compile error. Inferring
|
|
513
|
+
* `TIndexes` from the entries turns off TypeScript's own excess-property check,
|
|
514
|
+
* which would otherwise catch it.
|
|
515
|
+
*/
|
|
516
|
+
export type StreamIndexesConfig<TIndexes, TEvent> = {
|
|
517
|
+
readonly [K in keyof TIndexes]: StreamIndexConfig<TEvent> & {
|
|
518
|
+
readonly [P in Exclude<keyof TIndexes[K], StreamIndexConfigKey>]: never;
|
|
519
|
+
};
|
|
520
|
+
};
|
|
378
521
|
/**
|
|
379
522
|
* Options accepted by {@link EventStream.readIndex}.
|
|
380
523
|
*
|
|
381
524
|
* - `beginsWith` / `between` — a sort-key condition on the derived key (at most
|
|
382
|
-
* one of them). Omitted, the whole sub-stream is read.
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
525
|
+
* one of them, string bounds). Omitted, the whole sub-stream is read.
|
|
526
|
+
* DynamoDB rejects an empty bound, and a `between` whose lower bound sorts
|
|
527
|
+
* (by UTF-8 bytes) after its upper bound; the read then fails with a
|
|
528
|
+
* `ValidationError` naming the index and the bound, DynamoDB's error as its
|
|
529
|
+
* cause.
|
|
386
530
|
* - `reverse` — descending key order.
|
|
387
531
|
* - `limit` — return at most this many events (a positive integer).
|
|
388
|
-
* - `consistentRead` — strongly consistent read.
|
|
389
|
-
*
|
|
390
|
-
*
|
|
532
|
+
* - `consistentRead` — strongly consistent read. DynamoDB rejects it on a
|
|
533
|
+
* GSI-backed index, which it reads only eventually consistently: the read
|
|
534
|
+
* fails with a `ValidationError` naming the index.
|
|
391
535
|
*/
|
|
392
536
|
export type ReadIndexOptions = {
|
|
393
537
|
readonly reverse?: boolean | undefined;
|
|
@@ -413,9 +557,15 @@ export type EventStreamTypeId = typeof EventStreamTypeId;
|
|
|
413
557
|
*
|
|
414
558
|
* `TIndexName` is the union of the stream's index names (`makeStream({ indexes })`),
|
|
415
559
|
* `never` for a stream without indexes, so {@link EventStream.readIndex} and
|
|
416
|
-
* `query.index` only accept a declared name.
|
|
560
|
+
* `query.index` only accept a declared name. `TIndexProjections` is each
|
|
561
|
+
* index's {@link StreamIndexProjection} by name, so an event read through a
|
|
562
|
+
* `"withoutMetadata"` index has `metadata: undefined`.
|
|
417
563
|
*/
|
|
418
|
-
export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never
|
|
564
|
+
export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never, TIndexProjections extends {
|
|
565
|
+
readonly [K in TIndexName]: StreamIndexProjection;
|
|
566
|
+
} = {
|
|
567
|
+
readonly [K in TIndexName]: "all";
|
|
568
|
+
}> extends Pipeable.Pipeable {
|
|
419
569
|
readonly [EventStreamTypeId]: EventStreamTypeId;
|
|
420
570
|
readonly streamName: string;
|
|
421
571
|
readonly eventSchema: Schema.Top;
|
|
@@ -515,22 +665,35 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
|
|
|
515
665
|
* Read the stream's events in the order of one of its indexes (#140) — the
|
|
516
666
|
* events whose index `key` returned a string, ordered by that key. A
|
|
517
667
|
* convenience over `query.index`; pass `beginsWith` or `between` to narrow
|
|
518
|
-
* the key range, `reverse`, `limit` and `consistentRead` (
|
|
519
|
-
* — see {@link ReadIndexOptions}.
|
|
668
|
+
* the key range, `reverse`, `limit` and `consistentRead` (which DynamoDB
|
|
669
|
+
* rejects on a GSI; the error names the index) — see {@link ReadIndexOptions}.
|
|
670
|
+
*
|
|
671
|
+
* How the events are read follows the index's {@link StreamIndexProjection}:
|
|
672
|
+
* one query for `"all"` and `"withoutMetadata"` (whose events have
|
|
673
|
+
* `metadata: undefined`, as the type says); for `"keys"`, one query with
|
|
674
|
+
* `Select: ALL_ATTRIBUTES` on an LSI, or the query followed by `BatchGetItem`
|
|
675
|
+
* (unprocessed keys retried, index order kept) on a GSI.
|
|
520
676
|
*/
|
|
521
|
-
readIndex(name:
|
|
677
|
+
readIndex<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
522
678
|
readonly query: {
|
|
523
679
|
events(streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
|
|
524
680
|
/**
|
|
525
681
|
* A query of one of the stream's indexes (#140), scoped to `streamId`:
|
|
526
682
|
* decoded events in index-key order. It composes with the `Query`
|
|
527
683
|
* combinators — `Query.where` (a condition on the derived key),
|
|
528
|
-
* `Query.reverse`, `Query.limit`, `Query.consistentRead` (
|
|
529
|
-
*
|
|
684
|
+
* `Query.reverse`, `Query.limit`, `Query.consistentRead` (DynamoDB rejects
|
|
685
|
+
* it on a GSI-backed index: a `ValidationError` naming the index) — and terminals
|
|
530
686
|
* (`Query.collect`, `Query.execute`, `Query.paginate`). The
|
|
531
687
|
* `__edd_e__ = <stream>.event` filter still applies.
|
|
688
|
+
*
|
|
689
|
+
* It reads as {@link EventStream.readIndex} does for the index's
|
|
690
|
+
* projection: a `"keys"` GSI fetches each page's events with
|
|
691
|
+
* `BatchGetItem` (`Query.select` moves onto that fetch); `Query.count`
|
|
692
|
+
* counts index entries and fetches nothing. On a `"keys"` or
|
|
693
|
+
* `"withoutMetadata"` index, a `Query.filter` on an attribute the index
|
|
694
|
+
* does not project is refused by DynamoDB — use `Query.filterBy`.
|
|
532
695
|
*/
|
|
533
|
-
index(name:
|
|
696
|
+
index<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>;
|
|
534
697
|
};
|
|
535
698
|
}
|
|
536
699
|
/**
|
|
@@ -592,7 +755,9 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
|
|
|
592
755
|
* event's derived key (and, for a GSI, the stream's partition key) on the
|
|
593
756
|
* event item; snapshots and idempotency sentinels never carry them, so they
|
|
594
757
|
* are never in an index. Read with {@link EventStream.readIndex} or
|
|
595
|
-
* `query.index`, and create the indexes with {@link indexDefinitions}.
|
|
758
|
+
* `query.index`, and create the indexes with {@link indexDefinitions}. Each
|
|
759
|
+
* index's `projection` (default `"all"`) sets what it copies from each event
|
|
760
|
+
* and how it is read — see {@link StreamIndexProjection}.
|
|
596
761
|
*
|
|
597
762
|
* Index attributes are written only at append time and events are never
|
|
598
763
|
* rewritten. Declaring an index on a stream that already holds events, or
|
|
@@ -629,7 +794,9 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
|
|
|
629
794
|
* `"inline"`.
|
|
630
795
|
* @throws `[EDD-9063]` for a malformed index: a `type` other than `"lsi"` /
|
|
631
796
|
* `"gsi"`, a `gsi` without `pk`, an `lsi` with `pk`, an empty `index` / `sk`
|
|
632
|
-
* / `pk`,
|
|
797
|
+
* / `pk`, a `key` that is not a function, a `projection` other than
|
|
798
|
+
* `"all"` / `"withoutMetadata"` / `"keys"`, or a member an index entry does
|
|
799
|
+
* not declare (such as a misspelled `projection`, also a type error).
|
|
633
800
|
* @throws `[EDD-9064]` when an index attribute is one the stream writes itself
|
|
634
801
|
* (`pk`, `sk`, `__edd_e__`, `streamId`, `version`, `eventType`, `data`,
|
|
635
802
|
* `metadata`, `timestamp`, `asOfVersion`, `state`, `commandId`, or the
|
|
@@ -641,7 +808,7 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
|
|
|
641
808
|
*/
|
|
642
809
|
export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top>, TTable extends Table, const TStreamName extends string, const TStreamId extends {
|
|
643
810
|
readonly composite: ReadonlyArray<string>;
|
|
644
|
-
}, TMetadata extends Schema.Top | undefined = undefined, TSnapshot extends SnapshotConfig | undefined = undefined, const
|
|
811
|
+
}, TMetadata extends Schema.Top | undefined = undefined, TSnapshot extends SnapshotConfig | undefined = undefined, const TIndexes extends { readonly [K in keyof TIndexes]: StreamIndexConfig<Schema.Schema.Type<TEvents[number]>>; } = {}>(config: {
|
|
645
812
|
readonly table: TTable;
|
|
646
813
|
readonly streamName: TStreamName;
|
|
647
814
|
readonly events: TEvents;
|
|
@@ -649,8 +816,8 @@ export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top
|
|
|
649
816
|
readonly metadata?: TMetadata;
|
|
650
817
|
readonly snapshot?: TSnapshot;
|
|
651
818
|
readonly casing?: DynamoSchema.Casing | undefined;
|
|
652
|
-
readonly indexes?:
|
|
653
|
-
}) => EventStream<Schema.Schema.Type<TEvents[number]>, TStreamId["composite"], TMetadata extends Schema.Top ? Schema.Schema.Type<TMetadata> : undefined, TSnapshot extends SnapshotConfig<infer TStateSchema> ? Schema.Schema.Type<TStateSchema> : never,
|
|
819
|
+
readonly indexes?: TIndexes & StreamIndexesConfig<TIndexes, Schema.Schema.Type<TEvents[number]>>;
|
|
820
|
+
}) => EventStream<Schema.Schema.Type<TEvents[number]>, TStreamId["composite"], TMetadata extends Schema.Top ? Schema.Schema.Type<TMetadata> : undefined, TSnapshot extends SnapshotConfig<infer TStateSchema> ? Schema.Schema.Type<TStateSchema> : never, keyof TIndexes & string, IndexProjectionsOf<TIndexes>>;
|
|
654
821
|
/**
|
|
655
822
|
* An EventStream whose operations have `DynamoClient` and `TableConfig` already
|
|
656
823
|
* resolved, so all methods return `Effect<A, E, never>`.
|
|
@@ -671,7 +838,11 @@ export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top
|
|
|
671
838
|
* }) {}
|
|
672
839
|
* ```
|
|
673
840
|
*/
|
|
674
|
-
export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never
|
|
841
|
+
export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never, TIndexProjections extends {
|
|
842
|
+
readonly [K in TIndexName]: StreamIndexProjection;
|
|
843
|
+
} = {
|
|
844
|
+
readonly [K in TIndexName]: "all";
|
|
845
|
+
}> extends Pipeable.Pipeable {
|
|
675
846
|
readonly [EventStreamTypeId]: EventStreamTypeId;
|
|
676
847
|
readonly streamName: string;
|
|
677
848
|
readonly eventSchema: Schema.Top;
|
|
@@ -696,7 +867,7 @@ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<
|
|
|
696
867
|
/** See {@link EventStream.readLatest}. */
|
|
697
868
|
readLatest(streamId: StreamIdInput<TStreamIdFields>, options?: ReadLatestOptions | undefined): Effect.Effect<LatestState<TState, TEvent, StreamMetadata<TMetadata>>, DynamoClientError | ValidationError, never>;
|
|
698
869
|
/** See {@link EventStream.readIndex}. */
|
|
699
|
-
readIndex(name:
|
|
870
|
+
readIndex<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>, DynamoClientError | ValidationError, never>;
|
|
700
871
|
/**
|
|
701
872
|
* The stream's query builders — unbound: run them with {@link provide}, or
|
|
702
873
|
* where `DynamoClient` and `TableConfig` are provided.
|
|
@@ -704,7 +875,7 @@ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<
|
|
|
704
875
|
readonly query: {
|
|
705
876
|
events(streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
|
|
706
877
|
/** See `EventStream.query.index`. */
|
|
707
|
-
index(name:
|
|
878
|
+
index<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>;
|
|
708
879
|
};
|
|
709
880
|
/** Escape hatch: provide DynamoClient | TableConfig to an arbitrary effect. */
|
|
710
881
|
readonly provide: <A, E>(effect: Effect.Effect<A, E, DynamoClient | TableConfig>) => Effect.Effect<A, E, never>;
|
|
@@ -723,7 +894,7 @@ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<
|
|
|
723
894
|
* yield* stream.append({ matchId: "m-1" }, [event], 0) // R = never
|
|
724
895
|
* ```
|
|
725
896
|
*/
|
|
726
|
-
export declare const bind: <TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState, TIndexName extends string = never>(stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName>) => Effect.Effect<BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName>, never, DynamoClient | TableConfig>;
|
|
897
|
+
export declare const bind: <TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState, TIndexName extends string = never, TIndexProjections extends { readonly [K in TIndexName]: StreamIndexProjection; } = { readonly [K in TIndexName]: "all"; }>(stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections>) => Effect.Effect<BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections>, never, DynamoClient | TableConfig>;
|
|
727
898
|
/**
|
|
728
899
|
* `CreateTable` fragments for the stream indexes of one or more streams — see
|
|
729
900
|
* {@link indexDefinitions}. The lists are absent when empty, because
|
|
@@ -738,11 +909,26 @@ export interface StreamIndexDefinitions {
|
|
|
738
909
|
/**
|
|
739
910
|
* Derive the `CreateTable` fragments for the stream indexes (#140) of the given
|
|
740
911
|
* streams (`EventStream` or `BoundEventStream`) — `AttributeDefinitions`,
|
|
741
|
-
* `LocalSecondaryIndexes` and `GlobalSecondaryIndexes
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
912
|
+
* `LocalSecondaryIndexes` and `GlobalSecondaryIndexes` — to merge into the
|
|
913
|
+
* caller's `CreateTable` input. Event tables are not derived by
|
|
914
|
+
* `Table.definition`, and the table's own `pk` / `sk` are not included: add
|
|
915
|
+
* them (and, on a provisioned table, each GSI's `ProvisionedThroughput`)
|
|
916
|
+
* yourself.
|
|
917
|
+
*
|
|
918
|
+
* Each index's `Projection` follows its {@link StreamIndexProjection}:
|
|
919
|
+
*
|
|
920
|
+
* | `projection` | `Projection` |
|
|
921
|
+
* |---|---|
|
|
922
|
+
* | `"all"` (default) | `{ ProjectionType: "ALL" }` |
|
|
923
|
+
* | `"withoutMetadata"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__", "data", "eventType", "streamId", "timestamp", "version"] }` |
|
|
924
|
+
* | `"keys"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__"] }` — not `KEYS_ONLY`: every read filters on `__edd_e__` |
|
|
925
|
+
*
|
|
926
|
+
* The deployed index must project what is declared — copy `Projection` from
|
|
927
|
+
* this output into CDK or Terraform definitions. Neither an LSI's nor a GSI's
|
|
928
|
+
* projection can be changed in place, and the library migrates nothing. A
|
|
929
|
+
* table projects at most 100 `NonKeyAttributes` across all its indexes;
|
|
930
|
+
* `"withoutMetadata"` spends 6 of them and `"keys"` 1 (DynamoDB refuses a
|
|
931
|
+
* table over the limit).
|
|
746
932
|
*
|
|
747
933
|
* An index shared by several streams with the same definition is emitted
|
|
748
934
|
* once. Lists are sorted by index name, attributes by attribute name.
|
|
@@ -750,7 +936,6 @@ export interface StreamIndexDefinitions {
|
|
|
750
936
|
* - An LSI can only be created with the table, and caps the item collection
|
|
751
937
|
* of every partition key value in the table at 10 GB — entity partitions
|
|
752
938
|
* and streams without indexes included.
|
|
753
|
-
* - The projection must stay `ALL`: events are decoded from the index item.
|
|
754
939
|
*
|
|
755
940
|
* @example
|
|
756
941
|
* ```typescript
|
|
@@ -773,12 +958,15 @@ export interface StreamIndexDefinitions {
|
|
|
773
958
|
* ```
|
|
774
959
|
*
|
|
775
960
|
* @throws `[EDD-9067]` when two streams define the same physical index
|
|
776
|
-
* differently (another type,
|
|
961
|
+
* differently (another type, other key attributes, or another projection).
|
|
777
962
|
*/
|
|
778
963
|
export declare const indexDefinitions: (...streams: ReadonlyArray<{
|
|
779
964
|
readonly streamName: string;
|
|
780
965
|
readonly indexes: {
|
|
781
|
-
readonly [name: string]: StreamIndexSettings
|
|
966
|
+
readonly [name: string]: Omit<StreamIndexSettings, "projection"> & {
|
|
967
|
+
/** Read as `"all"` when absent (a hand-built settings object). */
|
|
968
|
+
readonly projection?: StreamIndexProjection | undefined;
|
|
969
|
+
};
|
|
782
970
|
};
|
|
783
971
|
}>) => StreamIndexDefinitions;
|
|
784
972
|
/**
|
|
@@ -803,7 +991,7 @@ export interface CommandHandlerOptions {
|
|
|
803
991
|
* initial attempt). Omit for the default: no retry.
|
|
804
992
|
*
|
|
805
993
|
* `DuplicateCommand` is deliberately NOT retried — it is terminal. Nor is
|
|
806
|
-
* any call that supplies {@link
|
|
994
|
+
* any call that supplies {@link CommandOptionsBase.expectedVersion}: the caller asked
|
|
807
995
|
* for a conditional write, so its `VersionConflict` is always surfaced.
|
|
808
996
|
*/
|
|
809
997
|
readonly retry?: number | Schedule.Schedule<unknown, VersionConflict> | undefined;
|
|
@@ -850,7 +1038,7 @@ export interface CommandHandlerOptions {
|
|
|
850
1038
|
*
|
|
851
1039
|
* - **No snapshot item** (a stream not yet snapshotted): the verified load
|
|
852
1040
|
* runs at once, and `decide` runs once.
|
|
853
|
-
* - **With {@link
|
|
1041
|
+
* - **With {@link CommandOptionsBase.expectedVersion}**: a snapshot whose
|
|
854
1042
|
* `asOfVersion` differs from it is re-read verified before the If-Match
|
|
855
1043
|
* check, so only a genuine mismatch is reported (its `actualVersion` is
|
|
856
1044
|
* the verified head). A snapshot at the expected version holds exactly the
|
|
@@ -890,7 +1078,9 @@ export interface CommandHandlerOptions {
|
|
|
890
1078
|
}
|
|
891
1079
|
/**
|
|
892
1080
|
* The outcome of one `decide` call, handed to a function-form
|
|
893
|
-
* {@link
|
|
1081
|
+
* {@link CommandOptionsBase.additionalItems}, to `eventMetadata`
|
|
1082
|
+
* ({@link CommandMetadataOptions}) and to
|
|
1083
|
+
* {@link CommandOptionsBase.eventTimestamps}.
|
|
894
1084
|
*
|
|
895
1085
|
* - `events` — what `decide` returned (never empty: the function is not called
|
|
896
1086
|
* for a no-op decision).
|
|
@@ -911,7 +1101,7 @@ export interface Decision<State, Event> {
|
|
|
911
1101
|
readonly version: number;
|
|
912
1102
|
}
|
|
913
1103
|
/**
|
|
914
|
-
* The forms {@link
|
|
1104
|
+
* The forms {@link CommandOptionsBase.additionalItems} accepts: a static array of
|
|
915
1105
|
* transact ops, or a function of the {@link Decision} returning the array
|
|
916
1106
|
* directly (pure projection) or as an `Effect` (a projection that needs a read).
|
|
917
1107
|
* The effect's `E2` joins the handler's error channel and its `R2` the handler's
|
|
@@ -919,15 +1109,97 @@ export interface Decision<State, Event> {
|
|
|
919
1109
|
*/
|
|
920
1110
|
export type AdditionalItemsInput<State, Event, E2 = never, R2 = never> = ReadonlyArray<TransactWriteOp> | ((decision: Decision<State, Event>) => ReadonlyArray<TransactWriteOp> | Effect.Effect<ReadonlyArray<TransactWriteOp>, E2, R2>);
|
|
921
1111
|
/**
|
|
922
|
-
*
|
|
1112
|
+
* The form `eventMetadata` ({@link CommandMetadataOptions}) accepts: a
|
|
1113
|
+
* function of the {@link Decision} returning one metadata entry per event
|
|
1114
|
+
* (`undefined` for an event without metadata), directly or as an `Effect`. The
|
|
1115
|
+
* effect's `E3` joins the handler's error channel and its `R3` the handler's
|
|
1116
|
+
* requirements.
|
|
923
1117
|
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
*
|
|
927
|
-
*
|
|
1118
|
+
* Function-only: the number of events is known only once `decide` has run.
|
|
1119
|
+
* The `Effect` form lets ids be minted through the `Crypto` service
|
|
1120
|
+
* (`crypto.randomULID`, or `randomUUIDv7`), which keeps them deterministic
|
|
1121
|
+
* under `TestClock` and a test `Crypto` layer with fixed bytes.
|
|
928
1122
|
*/
|
|
929
|
-
export
|
|
1123
|
+
export type EventMetadataInput<State, Event, TMetadata, E3 = never, R3 = never> = (decision: Decision<State, Event>) => ReadonlyArray<TMetadata | undefined> | Effect.Effect<ReadonlyArray<TMetadata | undefined>, E3, R3>;
|
|
1124
|
+
/**
|
|
1125
|
+
* The form {@link CommandOptionsBase.eventTimestamps} accepts: a function of
|
|
1126
|
+
* the {@link Decision} returning one timestamp per event (`undefined` keeps
|
|
1127
|
+
* the append's `Clock` time), directly or as an `Effect` — exactly as
|
|
1128
|
+
* {@link EventMetadataInput}. The effect's `E4` joins the handler's error
|
|
1129
|
+
* channel and its `R4` the handler's requirements.
|
|
1130
|
+
*
|
|
1131
|
+
* Function-only: the number of events is known only once `decide` has run.
|
|
1132
|
+
* The `Effect` form lets times come from a service (a source log, a
|
|
1133
|
+
* per-tenant clock) or from `Clock`, which keeps them deterministic under
|
|
1134
|
+
* `TestClock`.
|
|
1135
|
+
*
|
|
1136
|
+
* `E4` / `R4` are separate from `eventMetadata`'s `E3` / `R3`: with one shared
|
|
1137
|
+
* pair, an `eventMetadata` effect and an `eventTimestamps` effect with
|
|
1138
|
+
* different error types would not compile — TypeScript fixes the parameter
|
|
1139
|
+
* from one candidate and rejects the other instead of inferring a union.
|
|
1140
|
+
*/
|
|
1141
|
+
export type EventTimestampsInput<State, Event, E4 = never, R4 = never> = (decision: Decision<State, Event>) => ReadonlyArray<DateTime.DateTime | undefined> | Effect.Effect<ReadonlyArray<DateTime.DateTime | undefined>, E4, R4>;
|
|
1142
|
+
/**
|
|
1143
|
+
* The per-call metadata of a {@link commandHandler} handler: one value shared
|
|
1144
|
+
* by every event (`metadata`), or a function deriving one entry per event from
|
|
1145
|
+
* the decision (`eventMetadata`) — never both (see
|
|
1146
|
+
* {@link AppendMetadataOptions}).
|
|
1147
|
+
*/
|
|
1148
|
+
export type CommandMetadataOptions<TMetadata, State, Event, E3 = never, R3 = never> = {
|
|
1149
|
+
/**
|
|
1150
|
+
* Metadata written onto every event the decision appends (see
|
|
1151
|
+
* {@link AppendMetadataOptions}). Fixed before `decide` runs; to give
|
|
1152
|
+
* each event its own entry, use `eventMetadata`.
|
|
1153
|
+
*/
|
|
930
1154
|
readonly metadata?: TMetadata;
|
|
1155
|
+
readonly eventMetadata?: undefined;
|
|
1156
|
+
} | {
|
|
1157
|
+
readonly metadata?: undefined;
|
|
1158
|
+
/**
|
|
1159
|
+
* Per-event metadata derived from the {@link Decision}: the function
|
|
1160
|
+
* returns one entry per event, in order, and entry `i` is written onto
|
|
1161
|
+
* event `i` (see {@link AppendMetadataOptions}).
|
|
1162
|
+
*
|
|
1163
|
+
* - Runs after `decide` and after the new events are folded, receiving
|
|
1164
|
+
* the same `Decision` a function-form `additionalItems` does — and
|
|
1165
|
+
* **before** `additionalItems`, so an effectful pair runs in a
|
|
1166
|
+
* predictable order.
|
|
1167
|
+
* - Not called when `decide` returns no events.
|
|
1168
|
+
* - Re-run on every attempt: each `VersionConflict` retry, and the
|
|
1169
|
+
* re-decision a stale unverified snapshot triggers
|
|
1170
|
+
* ({@link CommandHandlerOptions.verifySnapshot}`: false`). Ids minted
|
|
1171
|
+
* per attempt are safe — a failed attempt writes nothing.
|
|
1172
|
+
* - A result whose length differs from the decision's events fails with
|
|
1173
|
+
* `ValidationError` (`operation: "EventStore.commandHandler.eventMetadata"`),
|
|
1174
|
+
* never retried. A failure of the returned `Effect` surfaces as is,
|
|
1175
|
+
* and is not retried either.
|
|
1176
|
+
*
|
|
1177
|
+
* Without `idempotency`, a redelivery after a lost response decides and
|
|
1178
|
+
* appends again, minting new ids. With it, the redelivery fails with
|
|
1179
|
+
* `DuplicateCommand`, so the first commit's ids are the only ones written:
|
|
1180
|
+
*
|
|
1181
|
+
* ```ts
|
|
1182
|
+
* // `crypto` is the `Crypto.Crypto` service; a ULID takes its time from the
|
|
1183
|
+
* // `Clock`, so `TestClock` plus a fixed-bytes `Crypto` make it deterministic
|
|
1184
|
+
* const handle = EventStore.commandHandler(decider, stream, {
|
|
1185
|
+
* idempotency: { ttl: Duration.days(1) },
|
|
1186
|
+
* })
|
|
1187
|
+
* yield* handle(streamId, command, {
|
|
1188
|
+
* commandId: deliveryId,
|
|
1189
|
+
* eventMetadata: ({ events }) =>
|
|
1190
|
+
* Effect.forEach(events, () =>
|
|
1191
|
+
* Effect.map(crypto.randomULID, (eventId) => ({ eventId })),
|
|
1192
|
+
* ),
|
|
1193
|
+
* })
|
|
1194
|
+
* ```
|
|
1195
|
+
*/
|
|
1196
|
+
readonly eventMetadata?: EventMetadataInput<State, Event, TMetadata, E3, R3>;
|
|
1197
|
+
};
|
|
1198
|
+
/**
|
|
1199
|
+
* The options of a {@link commandHandler} handler other than its metadata
|
|
1200
|
+
* (see {@link CommandMetadataOptions}).
|
|
1201
|
+
*/
|
|
1202
|
+
export interface CommandOptionsBase<State = unknown, Event = unknown, E2 = never, R2 = never, E4 = never, R4 = never> {
|
|
931
1203
|
/**
|
|
932
1204
|
* Identifier for this command delivery. Required when the handler was created
|
|
933
1205
|
* with `idempotency`; a replayed id fails with `DuplicateCommand` — also when
|
|
@@ -964,7 +1236,7 @@ export interface CommandOptions<TMetadata, State = unknown, Event = unknown, E2
|
|
|
964
1236
|
readonly expectedVersion?: number | undefined;
|
|
965
1237
|
/**
|
|
966
1238
|
* Caller-owned transact items committed atomically with the produced events
|
|
967
|
-
* (see {@link
|
|
1239
|
+
* (see {@link AppendOptionsBase.additionalItems}).
|
|
968
1240
|
*
|
|
969
1241
|
* Either a static array, or a function of the {@link Decision} — an inline
|
|
970
1242
|
* projection — returning the array or an `Effect` of it. The function runs
|
|
@@ -974,31 +1246,80 @@ export interface CommandOptions<TMetadata, State = unknown, Event = unknown, E2
|
|
|
974
1246
|
* `AdditionalItemConditionFailed` refer to the array it returned.
|
|
975
1247
|
*/
|
|
976
1248
|
readonly additionalItems?: AdditionalItemsInput<State, Event, E2, R2>;
|
|
1249
|
+
/**
|
|
1250
|
+
* Per-event timestamps derived from the {@link Decision}: the function
|
|
1251
|
+
* returns one entry per event, in order, directly or as an `Effect` (see
|
|
1252
|
+
* {@link EventTimestampsInput}), and entry `i` is written as event `i`'s
|
|
1253
|
+
* `timestamp` — `undefined` keeps the append's `Clock` time (see
|
|
1254
|
+
* {@link AppendOptionsBase.eventTimestamps}). Independent of `metadata` /
|
|
1255
|
+
* `eventMetadata`. The idempotency sentinel, its TTL and an inline snapshot
|
|
1256
|
+
* keep the append's `Clock` time; timestamps carry no ordering check.
|
|
1257
|
+
*
|
|
1258
|
+
* - Runs after `eventMetadata` and before `additionalItems`, each to
|
|
1259
|
+
* completion before the next, all receiving the same `Decision`; not
|
|
1260
|
+
* called when `decide` returns no events; re-run on every attempt — each
|
|
1261
|
+
* `VersionConflict` retry and the re-decision a stale unverified snapshot
|
|
1262
|
+
* triggers — as `eventMetadata` is.
|
|
1263
|
+
* - A failure of the returned `Effect` surfaces as is, is not retried, and
|
|
1264
|
+
* `additionalItems` is not run.
|
|
1265
|
+
* - A result — plain or produced by the `Effect` — of the wrong length, or
|
|
1266
|
+
* with an entry that is neither a valid `DateTime` (finite epoch) nor
|
|
1267
|
+
* `undefined`, fails with `ValidationError`
|
|
1268
|
+
* (`operation: "EventStore.commandHandler.eventTimestamps"`), never
|
|
1269
|
+
* retried. A value that is not a function is refused before anything is
|
|
1270
|
+
* read.
|
|
1271
|
+
*
|
|
1272
|
+
* ```ts
|
|
1273
|
+
* // plain: the times are in the events
|
|
1274
|
+
* eventTimestamps: ({ events }) => events.map((event) => event.occurredAt)
|
|
1275
|
+
*
|
|
1276
|
+
* // Effect: the times come from a service
|
|
1277
|
+
* eventTimestamps: ({ events }) =>
|
|
1278
|
+
* Effect.forEach(events, (event) => SourceLog.originalTime(event.sourceId))
|
|
1279
|
+
* ```
|
|
1280
|
+
*/
|
|
1281
|
+
readonly eventTimestamps?: EventTimestampsInput<State, Event, E4, R4>;
|
|
977
1282
|
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Per-call options accepted by a handler produced by {@link commandHandler}:
|
|
1285
|
+
* {@link CommandOptionsBase} plus the exclusive `metadata` / `eventMetadata`
|
|
1286
|
+
* pair ({@link CommandMetadataOptions}).
|
|
1287
|
+
*
|
|
1288
|
+
* `State` / `Event` type the {@link Decision} a function-form `additionalItems`,
|
|
1289
|
+
* `eventMetadata` or `eventTimestamps` receives; `E2` / `R2` are the error and
|
|
1290
|
+
* requirements of the `Effect` `additionalItems` may return, `E3` / `R3` those
|
|
1291
|
+
* of the `Effect` `eventMetadata` may return, `E4` / `R4` those of the
|
|
1292
|
+
* `Effect` `eventTimestamps` may return — separate, so effects with different
|
|
1293
|
+
* error types all join the handler's error channel. The handler infers all of
|
|
1294
|
+
* them, so they only need naming when this type is written out by hand.
|
|
1295
|
+
*/
|
|
1296
|
+
export type CommandOptions<TMetadata, State = unknown, Event = unknown, E2 = never, R2 = never, E3 = never, R3 = never, E4 = never, R4 = never> = CommandOptionsBase<State, Event, E2, R2, E4, R4> & CommandMetadataOptions<TMetadata, State, Event, E3, R3>;
|
|
978
1297
|
/** Per-call options when the handler was created with `idempotency` — `commandId` is required. */
|
|
979
|
-
export
|
|
1298
|
+
export type IdempotentCommandOptions<TMetadata, State = unknown, Event = unknown, E2 = never, R2 = never, E3 = never, R3 = never, E4 = never, R4 = never> = CommandOptions<TMetadata, State, Event, E2, R2, E3, R3, E4, R4> & {
|
|
980
1299
|
readonly commandId: string;
|
|
981
|
-
}
|
|
1300
|
+
};
|
|
982
1301
|
/**
|
|
983
1302
|
* Options arity: configuring `idempotency` makes the handler's options parameter
|
|
984
1303
|
* required (and `commandId` within it non-optional), so a missing `commandId`
|
|
985
1304
|
* is a compile error rather than a silent downgrade to at-least-once.
|
|
986
1305
|
*/
|
|
987
|
-
type CommandOptionsArgs<TMetadata, TConfig extends CommandHandlerOptions | undefined, State, TEvent, E2, R2> = TConfig extends {
|
|
1306
|
+
type CommandOptionsArgs<TMetadata, TConfig extends CommandHandlerOptions | undefined, State, TEvent, E2, R2, E3, R3, E4, R4> = TConfig extends {
|
|
988
1307
|
readonly idempotency: object;
|
|
989
|
-
} ? [options: IdempotentCommandOptions<TMetadata, State, TEvent, E2, R2>] : [options?: CommandOptions<TMetadata, State, TEvent, E2, R2> | undefined];
|
|
1308
|
+
} ? [options: IdempotentCommandOptions<TMetadata, State, TEvent, E2, R2, E3, R3, E4, R4>] : [options?: CommandOptions<TMetadata, State, TEvent, E2, R2, E3, R3, E4, R4> | undefined];
|
|
990
1309
|
type CommandHandlerErrors<E> = E | VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
|
|
991
1310
|
/**
|
|
992
1311
|
* A handler over an {@link EventStream}. Generic per call over the error (`E2`)
|
|
993
1312
|
* and requirements (`R2`) of a function-form `additionalItems` that returns an
|
|
994
|
-
* `Effect
|
|
1313
|
+
* `Effect`, those (`E3` / `R3`) of an `eventMetadata` that does, and those
|
|
1314
|
+
* (`E4` / `R4`) of an `eventTimestamps` that does; all are `never` otherwise.
|
|
995
1315
|
*/
|
|
996
|
-
type CommandHandler<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TConfig extends CommandHandlerOptions | undefined = undefined> = <E2 = never, R2 = never>(streamId: StreamIdInput<TStreamIdFields>, command: Command, ...options: CommandOptionsArgs<TMetadata, TConfig, State, TEvent, E2, R2>) => Effect.Effect<CommandHandlerResult<State, TEvent>, CommandHandlerErrors<E> | E2, DynamoClient | TableConfig | R2>;
|
|
1316
|
+
type CommandHandler<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TConfig extends CommandHandlerOptions | undefined = undefined> = <E2 = never, R2 = never, E3 = never, R3 = never, E4 = never, R4 = never>(streamId: StreamIdInput<TStreamIdFields>, command: Command, ...options: CommandOptionsArgs<TMetadata, TConfig, State, TEvent, E2, R2, E3, R3, E4, R4>) => Effect.Effect<CommandHandlerResult<State, TEvent>, CommandHandlerErrors<E> | E2 | E3 | E4, DynamoClient | TableConfig | R2 | R3 | R4>;
|
|
997
1317
|
/**
|
|
998
1318
|
* A handler over a {@link BoundEventStream}: the stream's services are already
|
|
999
|
-
* provided, so
|
|
1319
|
+
* provided, so the `R2` / `R3` / `R4` of a function-form `additionalItems` /
|
|
1320
|
+
* `eventMetadata` / `eventTimestamps` are its only requirements.
|
|
1000
1321
|
*/
|
|
1001
|
-
type BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TConfig extends CommandHandlerOptions | undefined = undefined> = <E2 = never, R2 = never>(streamId: StreamIdInput<TStreamIdFields>, command: Command, ...options: CommandOptionsArgs<TMetadata, TConfig, State, TEvent, E2, R2>) => Effect.Effect<CommandHandlerResult<State, TEvent>, CommandHandlerErrors<E> | E2, R2>;
|
|
1322
|
+
type BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TConfig extends CommandHandlerOptions | undefined = undefined> = <E2 = never, R2 = never, E3 = never, R3 = never, E4 = never, R4 = never>(streamId: StreamIdInput<TStreamIdFields>, command: Command, ...options: CommandOptionsArgs<TMetadata, TConfig, State, TEvent, E2, R2, E3, R3, E4, R4>) => Effect.Effect<CommandHandlerResult<State, TEvent>, CommandHandlerErrors<E> | E2 | E3 | E4, R2 | R3 | R4>;
|
|
1002
1323
|
/**
|
|
1003
1324
|
* The handler the data-last {@link commandHandler} returns for stream `S`: a
|
|
1004
1325
|
* {@link BoundCommandHandler} for a `BoundEventStream` (checked first — a bound
|
|
@@ -1042,6 +1363,24 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
|
|
|
1042
1363
|
* yield* handle({ matchId: "m-1" }, command, {
|
|
1043
1364
|
* additionalItems: ({ state }) => [Scoreboard.put({ matchId: "m-1", runs: state.runs })],
|
|
1044
1365
|
* })
|
|
1366
|
+
*
|
|
1367
|
+
* // Per-event metadata derived from the decision: entry i is written onto event i
|
|
1368
|
+
* // (`crypto` is the `Crypto.Crypto` service; `randomUUIDv7` works the same way)
|
|
1369
|
+
* yield* handle({ matchId: "m-1" }, command, {
|
|
1370
|
+
* eventMetadata: ({ events }) =>
|
|
1371
|
+
* Effect.forEach(events, () => Effect.map(crypto.randomULID, (eventId) => ({ eventId }))),
|
|
1372
|
+
* })
|
|
1373
|
+
*
|
|
1374
|
+
* // Per-event timestamps derived from the decision (undefined → the Clock time)
|
|
1375
|
+
* yield* handle({ matchId: "m-1" }, command, {
|
|
1376
|
+
* eventTimestamps: ({ events }) => events.map((event) => event.occurredAt),
|
|
1377
|
+
* })
|
|
1378
|
+
*
|
|
1379
|
+
* // ...or from a service, as an Effect (its errors / requirements join the handler's)
|
|
1380
|
+
* yield* handle({ matchId: "m-1" }, command, {
|
|
1381
|
+
* eventTimestamps: ({ events }) =>
|
|
1382
|
+
* Effect.forEach(events, (event) => SourceLog.originalTime(event.sourceId)),
|
|
1383
|
+
* })
|
|
1045
1384
|
* ```
|
|
1046
1385
|
*
|
|
1047
1386
|
* Each invocation runs
|
|
@@ -1057,7 +1396,7 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
|
|
|
1057
1396
|
* item alone (one `GetItem`); an answer that is not a successful append is
|
|
1058
1397
|
* confirmed against the head first, and a snapshot that proves stale falls
|
|
1059
1398
|
* back to the verified load — in which case `decide` may run twice.
|
|
1060
|
-
* - **expectedVersion.** See {@link
|
|
1399
|
+
* - **expectedVersion.** See {@link CommandOptionsBase.expectedVersion}.
|
|
1061
1400
|
* - **Fold before append.** The new events are folded into state before the
|
|
1062
1401
|
* append, so the state returned, snapshotted and handed to a function-form
|
|
1063
1402
|
* `additionalItems` (see {@link Decision}) is always the `evolve` fold.
|
|
@@ -1067,8 +1406,8 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
|
|
|
1067
1406
|
* fresh snapshot is written (best-effort) after a successful append once the
|
|
1068
1407
|
* cadence threshold is crossed. See {@link SnapshotMode}.
|
|
1069
1408
|
* - **One atomic append.** One command, one decision, one append: a decision
|
|
1070
|
-
* too large for one transaction
|
|
1071
|
-
* is written. A large command is split by the application into stepped
|
|
1409
|
+
* too large for one transaction is rejected whole by DynamoDB and fails with
|
|
1410
|
+
* `AppendTooLarge` — nothing is written. A large command is split by the application into stepped
|
|
1072
1411
|
* commands — each an ordinary call, chained by `expectedVersion` — so a
|
|
1073
1412
|
* failure partway leaves the stream at a real intermediate state.
|
|
1074
1413
|
*
|
|
@@ -1087,9 +1426,13 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
|
|
|
1087
1426
|
* `EventStreamTypeId` brand of the second argument.
|
|
1088
1427
|
*/
|
|
1089
1428
|
export declare const commandHandler: {
|
|
1090
|
-
<State, Command, TEvent, E, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, options?: TConfig): <S extends BoundEventStream<TEvent, any, any, State, any> | EventStream<TEvent, ReadonlyArray<string>, any, State, any>>(stream: S) => CommandHandlerFor<S, State, Command, TEvent, E, TConfig>;
|
|
1091
|
-
<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string,
|
|
1092
|
-
|
|
1429
|
+
<State, Command, TEvent, E, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, options?: TConfig): <S extends BoundEventStream<TEvent, any, any, State, any, any> | EventStream<TEvent, ReadonlyArray<string>, any, State, any, any>>(stream: S) => CommandHandlerFor<S, State, Command, TEvent, E, TConfig>;
|
|
1430
|
+
<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string, TIndexProjections extends {
|
|
1431
|
+
readonly [K in TIndexName]: StreamIndexProjection;
|
|
1432
|
+
}, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, stream: BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections>, options?: TConfig): BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
|
|
1433
|
+
<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string, TIndexProjections extends {
|
|
1434
|
+
readonly [K in TIndexName]: StreamIndexProjection;
|
|
1435
|
+
}, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections>, options?: TConfig): CommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
|
|
1093
1436
|
};
|
|
1094
1437
|
/**
|
|
1095
1438
|
* Reconstruct state from events by folding through a decider's `evolve` function.
|