effect-dynamodb 1.26.0 → 1.28.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/Entity.d.ts.map +1 -1
- package/dist/Entity.js +4 -2
- package/dist/Entity.js.map +1 -1
- package/dist/EventStore.d.ts +332 -58
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +440 -100
- package/dist/EventStore.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/EntityOps.d.ts +6 -1
- package/dist/internal/EntityOps.d.ts.map +1 -1
- package/dist/internal/EntityOps.js.map +1 -1
- package/dist/internal/EventProjection.d.ts +597 -0
- package/dist/internal/EventProjection.d.ts.map +1 -0
- package/dist/internal/EventProjection.js +695 -0
- package/dist/internal/EventProjection.js.map +1 -0
- package/package.json +2 -2
package/dist/EventStore.js
CHANGED
|
@@ -15,13 +15,20 @@
|
|
|
15
15
|
* - Stream indexes (`makeStream({ indexes })`): sub-streams ordered by a key
|
|
16
16
|
* derived from each event, on an LSI or GSI, each projecting all of the
|
|
17
17
|
* event, all but its metadata, or only its keys — `readIndex`,
|
|
18
|
-
* `query.index`, and `indexDefinitions` for the `CreateTable` fragments
|
|
18
|
+
* `query.index`, and `indexDefinitions` for the `CreateTable` fragments.
|
|
19
|
+
* A GSI may derive its partition too, so several streams share one
|
|
20
|
+
* partition of it, snapshots included (#149)
|
|
19
21
|
* - `fold` / `foldFrom` helpers for state reconstruction
|
|
20
22
|
* - Packed storage (`makeStream({ storedAs: { data } })`, `snapshot.storedAs`):
|
|
21
23
|
* event data and snapshot state as one JSON string, optionally compressed —
|
|
22
24
|
* read by the stored form, so a stream may mix native, JSON and compressed
|
|
23
25
|
* items; `measureAppend` / `measureSnapshot` for caller-side size planning,
|
|
24
26
|
* and `decodeStreamImage` for DynamoDB Streams records
|
|
27
|
+
* - Change-feed projections (#151): `projection` folds a stream into a
|
|
28
|
+
* read-model entity in version order, filling gaps from the store instead of
|
|
29
|
+
* waiting for delivery (`applyBatch`, `apply`, `catchUp`, `rebuild`,
|
|
30
|
+
* `processBatch`, `bindProjection`), plus `decoder`, `eventFilterPattern`
|
|
31
|
+
* and `batchResponse` for the change feed itself
|
|
25
32
|
*
|
|
26
33
|
* Built on the existing library primitives (DynamoSchema, KeyComposer, Query,
|
|
27
34
|
* DynamoClient, Marshaller).
|
|
@@ -35,8 +42,11 @@ import { isDispatchEligible, packedShape, packValue, unpackStoredSync, } from "@
|
|
|
35
42
|
import * as KeyComposer from "@effect-dynamodb/schema/KeyComposer.js";
|
|
36
43
|
import { Context, DateTime, Effect, Function, Option, Pipeable, Result, Schedule, Schema, } from "effect";
|
|
37
44
|
import { DynamoClient } from "./DynamoClient.js";
|
|
45
|
+
import { ProjectionDecode, } from "./internal/EventProjection.js";
|
|
46
|
+
import { eq } from "./internal/Expr.js";
|
|
38
47
|
import { itemBytes, transactItemBytes } from "./internal/ItemSize.js";
|
|
39
48
|
import { codecRequirements, currentRegistry, decodeImage, imageEntityType, runtimeCodecCheck, unregisteredCodecError, withRegistry, } from "./internal/Packed.js";
|
|
49
|
+
import { createPathBuilder } from "./internal/PathBuilder.js";
|
|
40
50
|
import { buildTransactWriteItems, GUARDED_TRANSACTION_ATTEMPTS, judgeCancellation, transactItemTarget, } from "./internal/TransactWriteOps.js";
|
|
41
51
|
import { diagnosedValidationError, diagnoseFailure, mergeKeyAttributes, withCause, } from "./internal/WriteDiagnosis.js";
|
|
42
52
|
import { fromAttributeMap, toAttributeMap } from "./Marshaller.js";
|
|
@@ -145,21 +155,31 @@ const STREAM_INDEX_CONFIG_KEYS = [
|
|
|
145
155
|
"pk",
|
|
146
156
|
"sk",
|
|
147
157
|
"key",
|
|
158
|
+
"partition",
|
|
159
|
+
"snapshot",
|
|
148
160
|
"projection",
|
|
149
161
|
];
|
|
162
|
+
/**
|
|
163
|
+
* @internal `pk = <streamPk>`: keeps one stream id's items in a partition of a
|
|
164
|
+
* stream index several streams (or stream ids) share (#149). The table's `pk`
|
|
165
|
+
* is projected into every index, whatever its projection.
|
|
166
|
+
*/
|
|
167
|
+
const streamPkFilter = (streamPk) => eq(createPathBuilder().pk, streamPk);
|
|
150
168
|
/**
|
|
151
169
|
* @internal Validate `makeStream({ indexes })` at definition time.
|
|
152
170
|
*
|
|
153
171
|
* @throws `[EDD-9063]` for a malformed entry (unknown `type`, a `gsi` without
|
|
154
|
-
* `pk`, an `lsi` with `pk`, an empty name, a
|
|
155
|
-
*
|
|
156
|
-
*
|
|
172
|
+
* `pk`, an `lsi` with `pk`, `partition` or `snapshot`, an empty name, a
|
|
173
|
+
* `key` / `partition` / `snapshot` that is not a function, a `snapshot`
|
|
174
|
+
* without `partition` or on a stream without a snapshot config, an unknown
|
|
175
|
+
* `projection`, a member the entry type does not declare — a misspelled
|
|
176
|
+
* `projection` would otherwise silently mean `"all"`).
|
|
157
177
|
* @throws `[EDD-9064]` for an index attribute that collides with a
|
|
158
178
|
* stream-owned attribute.
|
|
159
179
|
* @throws `[EDD-9065]` for two indexes sharing a physical index name or an
|
|
160
180
|
* attribute.
|
|
161
181
|
*/
|
|
162
|
-
const resolveStreamIndexes = (streamName, indexes) => {
|
|
182
|
+
const resolveStreamIndexes = (streamName, indexes, hasSnapshotConfig) => {
|
|
163
183
|
if (indexes === undefined)
|
|
164
184
|
return [];
|
|
165
185
|
const fail = (code, message) => {
|
|
@@ -196,7 +216,30 @@ const resolveStreamIndexes = (streamName, indexes) => {
|
|
|
196
216
|
'declare type: "gsi".');
|
|
197
217
|
}
|
|
198
218
|
if (typeof entry.key !== "function") {
|
|
199
|
-
fail("EDD-9063", `index "${name}": key must be a function (event, version) => string | undefined.`);
|
|
219
|
+
fail("EDD-9063", `index "${name}": key must be a function (event, version, streamId) => string | undefined.`);
|
|
220
|
+
}
|
|
221
|
+
if (entry.partition !== undefined) {
|
|
222
|
+
if (type !== "gsi") {
|
|
223
|
+
fail("EDD-9063", `index "${name}": an "lsi" index is partitioned by the stream; only a "gsi" index ` +
|
|
224
|
+
'derives its partition. Remove partition, or declare type: "gsi".');
|
|
225
|
+
}
|
|
226
|
+
if (typeof entry.partition !== "function") {
|
|
227
|
+
fail("EDD-9063", `index "${name}": partition must be a function (event, version, streamId) => string | undefined.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (entry.snapshot !== undefined) {
|
|
231
|
+
if (typeof entry.snapshot !== "function") {
|
|
232
|
+
fail("EDD-9063", `index "${name}": snapshot must be a function (state, asOfVersion, streamId) => ` +
|
|
233
|
+
"{ partition, key } | undefined.");
|
|
234
|
+
}
|
|
235
|
+
if (entry.partition === undefined) {
|
|
236
|
+
fail("EDD-9063", `index "${name}": snapshot requires partition — only a "gsi" index with a derived ` +
|
|
237
|
+
"partition indexes the stream's snapshot.");
|
|
238
|
+
}
|
|
239
|
+
if (!hasSnapshotConfig) {
|
|
240
|
+
fail("EDD-9063", `index "${name}": snapshot requires the stream's snapshot config ` +
|
|
241
|
+
"(makeStream({ snapshot: { schema } })); the stream writes no snapshot to index.");
|
|
242
|
+
}
|
|
200
243
|
}
|
|
201
244
|
const projection = (entry.projection ?? "all");
|
|
202
245
|
if (projection !== "all" && projection !== "withoutMetadata" && projection !== "keys") {
|
|
@@ -233,7 +276,11 @@ const resolveStreamIndexes = (streamName, indexes) => {
|
|
|
233
276
|
pk,
|
|
234
277
|
sk,
|
|
235
278
|
projection,
|
|
279
|
+
partition: entry.partition === undefined ? "stream" : "derived",
|
|
280
|
+
snapshot: entry.snapshot !== undefined,
|
|
236
281
|
key: entry.key,
|
|
282
|
+
derivePartition: entry.partition,
|
|
283
|
+
snapshotEntry: entry.snapshot,
|
|
237
284
|
});
|
|
238
285
|
}
|
|
239
286
|
return resolved;
|
|
@@ -302,6 +349,24 @@ const EventStreamTypeId = Symbol.for("effect-dynamodb/EventStream");
|
|
|
302
349
|
* attach it; a hand-built stream object falls back to joining the id's values.
|
|
303
350
|
*/
|
|
304
351
|
const StreamIdFormatter = Symbol.for("effect-dynamodb/EventStream/StreamIdFormatter");
|
|
352
|
+
/** @internal {@link EventStream.parseStreamId}, shared by `makeStream` and `bind`. */
|
|
353
|
+
const parseStreamIdWith = (streamName, fields, streamId) => {
|
|
354
|
+
const parts = fields.length === 1 ? [streamId] : streamId.split("#");
|
|
355
|
+
if (parts.length !== fields.length) {
|
|
356
|
+
return Effect.fail(new ValidationError({
|
|
357
|
+
entityType: `${streamName.toLowerCase()}.event`,
|
|
358
|
+
operation: "EventStore.parseStreamId",
|
|
359
|
+
cause: `Stream id ${JSON.stringify(streamId)} has ${parts.length} "#"-separated parts, but ` +
|
|
360
|
+
`the stream id has ${fields.length} fields (${fields.join(", ")}): a value containing ` +
|
|
361
|
+
`"#" makes the id ambiguous.`,
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
const id = {};
|
|
365
|
+
fields.forEach((field, i) => {
|
|
366
|
+
id[field] = parts[i];
|
|
367
|
+
});
|
|
368
|
+
return Effect.succeed(id);
|
|
369
|
+
};
|
|
305
370
|
/** @internal */
|
|
306
371
|
const formatStreamIdOf = (stream, streamId) => {
|
|
307
372
|
const formatter = stream[StreamIdFormatter];
|
|
@@ -430,11 +495,20 @@ const eventTimestampsProblem = (timestamps, count, label, events) => {
|
|
|
430
495
|
* `indexes` declares sub-streams ordered by a key derived from each event
|
|
431
496
|
* (#140), on a local (default) or global secondary index. `append` writes each
|
|
432
497
|
* event's derived key (and, for a GSI, the stream's partition key) on the
|
|
433
|
-
* event item
|
|
434
|
-
*
|
|
435
|
-
* `query.index`, and create the
|
|
436
|
-
* index's `projection` (default
|
|
437
|
-
* and how it is read — see
|
|
498
|
+
* event item. Idempotency sentinels never carry them; snapshots carry an
|
|
499
|
+
* entry only in a GSI that derives its partition and declares `snapshot`.
|
|
500
|
+
* Read with {@link EventStream.readIndex} or `query.index`, and create the
|
|
501
|
+
* indexes with {@link indexDefinitions}. Each index's `projection` (default
|
|
502
|
+
* `"all"`) sets what it copies from each event and how it is read — see
|
|
503
|
+
* {@link StreamIndexProjection}.
|
|
504
|
+
*
|
|
505
|
+
* A GSI's `partition` (#149) derives the partition from the stream id and the
|
|
506
|
+
* event instead, stored raw, so several streams declaring the same GSI write
|
|
507
|
+
* one partition of it — an order's line, payment and shipment streams under
|
|
508
|
+
* `ORDER#<orderId>`, read by one query — and its `snapshot` puts each stream's
|
|
509
|
+
* snapshot item in that partition too, in the same `Put` (so an inline
|
|
510
|
+
* snapshot enters the index in the append's own transaction). See
|
|
511
|
+
* {@link GlobalStreamIndexConfig}.
|
|
438
512
|
*
|
|
439
513
|
* Index attributes are written only at append time and events are never
|
|
440
514
|
* rewritten. Declaring an index on a stream that already holds events, or
|
|
@@ -446,6 +520,36 @@ const eventTimestampsProblem = (timestamps, count, label, events) => {
|
|
|
446
520
|
*
|
|
447
521
|
* @example
|
|
448
522
|
* ```typescript
|
|
523
|
+
* // Lines and payments of an order share `gsi1` under `ORDER#<orderId>`.
|
|
524
|
+
* const OrderLines = EventStore.makeStream({
|
|
525
|
+
* table: EventsTable,
|
|
526
|
+
* streamName: "OrderLine",
|
|
527
|
+
* events: [LineAdded, LineRemoved],
|
|
528
|
+
* streamId: { composite: ["orderId", "lineId"] },
|
|
529
|
+
* snapshot: { schema: LineState, mode: "inline" },
|
|
530
|
+
* indexes: {
|
|
531
|
+
* byOrder: {
|
|
532
|
+
* type: "gsi",
|
|
533
|
+
* index: "gsi1",
|
|
534
|
+
* pk: "gsi1pk",
|
|
535
|
+
* sk: "gsi1sk",
|
|
536
|
+
* partition: (_event, _version, streamId) => `ORDER#${streamId.orderId}`,
|
|
537
|
+
* key: (_event, version, streamId) => `LINE#${streamId.lineId}#EVT#${pad(version)}`,
|
|
538
|
+
* snapshot: (_state, _asOfVersion, streamId) => ({
|
|
539
|
+
* partition: `ORDER#${streamId.orderId}`,
|
|
540
|
+
* key: `LINE#${streamId.lineId}#STATE`,
|
|
541
|
+
* }),
|
|
542
|
+
* },
|
|
543
|
+
* },
|
|
544
|
+
* })
|
|
545
|
+
*
|
|
546
|
+
* const lineEvents = yield* OrderLines.readIndex("byOrder", { orderId, lineId }, {
|
|
547
|
+
* partition: `ORDER#${orderId}`,
|
|
548
|
+
* })
|
|
549
|
+
* ```
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* ```typescript
|
|
449
553
|
* const Entries = EventStore.makeStream({
|
|
450
554
|
* table: EventsTable,
|
|
451
555
|
* streamName: "Ledger",
|
|
@@ -470,10 +574,12 @@ const eventTimestampsProblem = (timestamps, count, label, events) => {
|
|
|
470
574
|
* @throws `[EDD-9062]` when `snapshot.mode` is neither `"after-append"` nor
|
|
471
575
|
* `"inline"`.
|
|
472
576
|
* @throws `[EDD-9063]` for a malformed index: a `type` other than `"lsi"` /
|
|
473
|
-
* `"gsi"`, a `gsi` without `pk`, an `lsi` with `pk`,
|
|
474
|
-
*
|
|
475
|
-
* `
|
|
476
|
-
*
|
|
577
|
+
* `"gsi"`, a `gsi` without `pk`, an `lsi` with `pk`, `partition` or
|
|
578
|
+
* `snapshot`, an empty `index` / `sk` / `pk`, a `key` / `partition` /
|
|
579
|
+
* `snapshot` that is not a function, a `snapshot` without `partition` or on
|
|
580
|
+
* a stream without a `snapshot` config, a `projection` other than `"all"` /
|
|
581
|
+
* `"withoutMetadata"` / `"keys"`, or a member an index entry does not
|
|
582
|
+
* declare (such as a misspelled `projection`, also a type error).
|
|
477
583
|
* @throws `[EDD-9064]` when an index attribute is one the stream writes itself
|
|
478
584
|
* (`pk`, `sk`, `__edd_e__`, `streamId`, `version`, `eventType`, `data`,
|
|
479
585
|
* `metadata`, `timestamp`, `asOfVersion`, `state`, `commandId`, or the
|
|
@@ -578,7 +684,7 @@ export const makeStream = (config) => {
|
|
|
578
684
|
},
|
|
579
685
|
];
|
|
580
686
|
// Stream indexes (#140) — validated at definition time (EDD-9063..9065).
|
|
581
|
-
const streamIndexes = resolveStreamIndexes(config.streamName, config.indexes);
|
|
687
|
+
const streamIndexes = resolveStreamIndexes(config.streamName, config.indexes, snapshot !== undefined);
|
|
582
688
|
const indexSettings = {};
|
|
583
689
|
for (const idx of streamIndexes) {
|
|
584
690
|
indexSettings[idx.name] = {
|
|
@@ -587,8 +693,12 @@ export const makeStream = (config) => {
|
|
|
587
693
|
pk: idx.pk,
|
|
588
694
|
sk: idx.sk,
|
|
589
695
|
projection: idx.projection,
|
|
696
|
+
partition: idx.partition,
|
|
697
|
+
snapshot: idx.snapshot,
|
|
590
698
|
};
|
|
591
699
|
}
|
|
700
|
+
/** The indexes that hold the stream's snapshot (#149). */
|
|
701
|
+
const snapshotIndexes = streamIndexes.filter((idx) => idx.snapshotEntry !== undefined);
|
|
592
702
|
/** Every attribute the indexes write on an event item. */
|
|
593
703
|
const indexAttributes = streamIndexes.flatMap((idx) => idx.type === "gsi" ? [idx.pk, idx.sk] : [idx.sk]);
|
|
594
704
|
// Build union schema from event schemas for decoding
|
|
@@ -719,34 +829,61 @@ export const makeStream = (config) => {
|
|
|
719
829
|
// append
|
|
720
830
|
// ---------------------------------------------------------------------------
|
|
721
831
|
/**
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* rules on a key — not empty, at
|
|
726
|
-
*
|
|
727
|
-
*
|
|
832
|
+
* One value an index function derives for an item (#140, #149): an event's
|
|
833
|
+
* `key` or `partition`, a string or `undefined` to leave the item out.
|
|
834
|
+
* Another type, or a function that throws, is a `ValidationError`, raised
|
|
835
|
+
* before anything is written. DynamoDB's own rules on a key — not empty, at
|
|
836
|
+
* most 1024 bytes for a sort key and 2048 for a partition key — are not
|
|
837
|
+
* pre-checked: DynamoDB rejects the write and the rejection is diagnosed,
|
|
838
|
+
* naming the index and the item (see `sendAppendTransaction`).
|
|
728
839
|
*/
|
|
729
|
-
const
|
|
730
|
-
try:
|
|
840
|
+
const deriveIndexValue = (idx, what, subject, derive) => Effect.try({
|
|
841
|
+
try: derive,
|
|
731
842
|
catch: (cause) => new ValidationError({ entityType, operation: "EventStore.append.indexes", cause }),
|
|
732
843
|
}).pipe(Effect.flatMap((derived) => {
|
|
733
844
|
if (derived === undefined)
|
|
734
845
|
return Effect.succeed(undefined);
|
|
735
|
-
const refuse = (problem) => Effect.fail(new ValidationError({
|
|
736
|
-
entityType,
|
|
737
|
-
operation: "EventStore.append.indexes",
|
|
738
|
-
cause: `The key of index "${idx.name}" for the event at version ${version} ${problem}. ` +
|
|
739
|
-
"Nothing was written.",
|
|
740
|
-
}));
|
|
741
846
|
// A string, or the event is not indexed: what the stream's own key
|
|
742
847
|
// contract means, which DynamoDB cannot see (a number would be sent
|
|
743
848
|
// as `N`). An empty string is DynamoDB's to reject, and is diagnosed
|
|
744
849
|
// from its rejection, naming the index and the event (#144).
|
|
745
850
|
if (typeof derived !== "string") {
|
|
746
|
-
return
|
|
851
|
+
return Effect.fail(new ValidationError({
|
|
852
|
+
entityType,
|
|
853
|
+
operation: "EventStore.append.indexes",
|
|
854
|
+
cause: `The ${what} of index "${idx.name}" for ${subject} must be a string or ` +
|
|
855
|
+
`undefined; received ${String(derived)}. Nothing was written.`,
|
|
856
|
+
}));
|
|
747
857
|
}
|
|
748
858
|
return Effect.succeed(derived);
|
|
749
859
|
}));
|
|
860
|
+
/**
|
|
861
|
+
* The snapshot's entry in a stream index (#149): its partition and key, or
|
|
862
|
+
* `undefined` to leave it out. Anything but an object of two strings, or an
|
|
863
|
+
* entry function that throws, is a `ValidationError` before anything is
|
|
864
|
+
* written; DynamoDB's rules on the values are its own, as for events.
|
|
865
|
+
*/
|
|
866
|
+
const deriveSnapshotEntry = (idx, entry, state, streamId, asOfVersion, operation) => Effect.try({
|
|
867
|
+
try: () => entry(state, asOfVersion, streamId),
|
|
868
|
+
catch: (cause) => new ValidationError({ entityType: snapshotEntityType, operation, cause }),
|
|
869
|
+
}).pipe(Effect.flatMap((derived) => {
|
|
870
|
+
if (derived === undefined)
|
|
871
|
+
return Effect.succeed(undefined);
|
|
872
|
+
const { partition, key } = typeof derived === "object" && derived !== null
|
|
873
|
+
? derived
|
|
874
|
+
: { partition: undefined, key: undefined };
|
|
875
|
+
if (typeof partition !== "string" || typeof key !== "string") {
|
|
876
|
+
return Effect.fail(new ValidationError({
|
|
877
|
+
entityType: snapshotEntityType,
|
|
878
|
+
operation,
|
|
879
|
+
cause: `The snapshot entry of index "${idx.name}" at version ${asOfVersion} must be ` +
|
|
880
|
+
"{ partition: string, key: string } or undefined; received " +
|
|
881
|
+
`${derived !== null && typeof derived === "object" ? JSON.stringify(derived) : String(derived)}. ` +
|
|
882
|
+
"Nothing was written.",
|
|
883
|
+
}));
|
|
884
|
+
}
|
|
885
|
+
return Effect.succeed({ partition, key });
|
|
886
|
+
}));
|
|
750
887
|
/**
|
|
751
888
|
* Encode snapshot state to wire form through the state schema, with the
|
|
752
889
|
* same `decode → encode` fallback as events and metadata. A structurally
|
|
@@ -852,48 +989,80 @@ export const makeStream = (config) => {
|
|
|
852
989
|
};
|
|
853
990
|
/**
|
|
854
991
|
* A diagnosed rejection of an append (#144) other than its item count. An
|
|
855
|
-
* over-long (or empty) stream-index
|
|
856
|
-
* index
|
|
857
|
-
* the `indexes` option reports its other key problems
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
992
|
+
* over-long (or empty) stream-index key OF AN EVENT OR THE INLINE SNAPSHOT
|
|
993
|
+
* — a value an index function returned — is named by its index and item,
|
|
994
|
+
* as the `indexes` option reports its other key problems (see
|
|
995
|
+
* {@link indexKeyError}). Only the stream's own items carry stream-index
|
|
996
|
+
* keys: an additional item writing the same physical attribute
|
|
997
|
+
* (single-table designs share `gsi1sk` and the like) is its own entity's
|
|
998
|
+
* key, and is named as any other item.
|
|
861
999
|
*/
|
|
862
1000
|
const appendLimitError = (diagnosis, original, tx) => {
|
|
863
|
-
if (
|
|
864
|
-
diagnosis._tag === "EmptyKey") {
|
|
1001
|
+
if (diagnosis._tag === "KeyTooLong" || diagnosis._tag === "EmptyKey") {
|
|
865
1002
|
const position = tx.targets().indexOf(diagnosis.target);
|
|
866
1003
|
const isEvent = position >= tx.guardCount && position < tx.guardCount + tx.eventCount;
|
|
867
|
-
const
|
|
868
|
-
|
|
1004
|
+
const isSnapshot = position >= 0 && position === tx.snapshotIndex;
|
|
1005
|
+
const named = isEvent || isSnapshot
|
|
1006
|
+
? indexKeyError(diagnosis, original, isSnapshot, "the append", "EventStore.append.indexes")
|
|
869
1007
|
: undefined;
|
|
870
|
-
if (
|
|
871
|
-
return
|
|
872
|
-
entityType,
|
|
873
|
-
operation: "EventStore.append.indexes",
|
|
874
|
-
cause: withCause(`The key of index "${idx.name}" for ${diagnosis.target.source} ` +
|
|
875
|
-
(diagnosis._tag === "KeyTooLong"
|
|
876
|
-
? `is ${diagnosis.bytes} bytes, longer than DynamoDB's 1024-byte limit for a ` +
|
|
877
|
-
"sort key."
|
|
878
|
-
: "is an empty string, which DynamoDB does not accept as a key value; the " +
|
|
879
|
-
"key function must return a non-empty string, or undefined to leave the " +
|
|
880
|
-
"event out of the index.") +
|
|
881
|
-
" DynamoDB rejected the append. Nothing was written.", original),
|
|
882
|
-
});
|
|
883
|
-
}
|
|
1008
|
+
if (named !== undefined)
|
|
1009
|
+
return named;
|
|
884
1010
|
}
|
|
885
1011
|
return diagnosedValidationError(diagnosis, original, {
|
|
886
1012
|
operation: "EventStore.append",
|
|
887
1013
|
entityType,
|
|
888
1014
|
});
|
|
889
1015
|
};
|
|
1016
|
+
/**
|
|
1017
|
+
* An over-long or empty key value in a stream-index attribute of an event
|
|
1018
|
+
* or snapshot item, named by its index and item, or `undefined` when the
|
|
1019
|
+
* attribute is not one the item's indexes derive. An event carries each
|
|
1020
|
+
* index's `key` (and a derived `partition`); a snapshot, each snapshot
|
|
1021
|
+
* index's entry. A stream-scoped GSI's partition is the stream's own key,
|
|
1022
|
+
* which no function returned: it is named as any key.
|
|
1023
|
+
*/
|
|
1024
|
+
const indexKeyError = (diagnosis, original, isSnapshot, request, operation) => {
|
|
1025
|
+
const candidates = isSnapshot ? snapshotIndexes : streamIndexes;
|
|
1026
|
+
const kind = diagnosis._tag === "KeyTooLong"
|
|
1027
|
+
? diagnosis.kind
|
|
1028
|
+
: candidates.some((idx) => idx.sk === diagnosis.attribute)
|
|
1029
|
+
? "sort"
|
|
1030
|
+
: "partition";
|
|
1031
|
+
const idx = candidates.find((candidate) => kind === "sort"
|
|
1032
|
+
? candidate.sk === diagnosis.attribute
|
|
1033
|
+
: candidate.partition === "derived" && candidate.pk === diagnosis.attribute);
|
|
1034
|
+
if (idx === undefined)
|
|
1035
|
+
return undefined;
|
|
1036
|
+
const value = isSnapshot
|
|
1037
|
+
? `The snapshot entry's ${kind === "sort" ? "key" : "partition"}`
|
|
1038
|
+
: `The ${kind === "sort" ? "key" : "partition"}`;
|
|
1039
|
+
const leaveOut = isSnapshot
|
|
1040
|
+
? "the snapshot function must return non-empty strings, or undefined to leave the " +
|
|
1041
|
+
"snapshot out of the index."
|
|
1042
|
+
: `the ${kind === "sort" ? "key" : "partition"} function must return a non-empty ` +
|
|
1043
|
+
"string, or undefined to leave the event out of the index.";
|
|
1044
|
+
return new ValidationError({
|
|
1045
|
+
entityType: isSnapshot ? snapshotEntityType : entityType,
|
|
1046
|
+
operation,
|
|
1047
|
+
cause: withCause(`${value} of index "${idx.name}" for ${diagnosis.target.source} ` +
|
|
1048
|
+
(diagnosis._tag === "KeyTooLong"
|
|
1049
|
+
? `is ${diagnosis.bytes} bytes, longer than DynamoDB's ` +
|
|
1050
|
+
`${kind === "sort" ? "1024-byte limit for a sort" : "2048-byte limit for a partition"} key.`
|
|
1051
|
+
: `is an empty string, which DynamoDB does not accept as a key value; ${leaveOut}`) +
|
|
1052
|
+
` DynamoDB rejected ${request}. Nothing was written.`, original),
|
|
1053
|
+
});
|
|
1054
|
+
};
|
|
890
1055
|
/**
|
|
891
1056
|
* The snapshot item for `state` at `asOfVersion`: the state encoded through
|
|
892
1057
|
* the state schema, then packed per `snapshot.storedAs` (#147) — so every
|
|
893
|
-
* size measured afterwards is
|
|
894
|
-
*
|
|
1058
|
+
* size measured afterwards is stored bytes — with its entry in each index
|
|
1059
|
+
* that holds snapshots (#149). Shared by `writeSnapshot`, the inline
|
|
1060
|
+
* snapshot of `append`, and `measureSnapshot` / `measureAppend`.
|
|
1061
|
+
*
|
|
1062
|
+
* `eventPartitions` — the partition the append's events have in each
|
|
1063
|
+
* index, by index name — is what the inline snapshot's entry must match.
|
|
895
1064
|
*/
|
|
896
|
-
const buildSnapshotItem = (
|
|
1065
|
+
const buildSnapshotItem = (streamId, state, asOfVersion, now, operation, eventPartitions) => Effect.gen(function* () {
|
|
897
1066
|
const encoded = yield* encodeSnapshotState(state, operation);
|
|
898
1067
|
const stored = stateStoredAs === undefined
|
|
899
1068
|
? encoded
|
|
@@ -902,15 +1071,36 @@ export const makeStream = (config) => {
|
|
|
902
1071
|
attribute: "state",
|
|
903
1072
|
operation: `${operation}.packed`,
|
|
904
1073
|
});
|
|
905
|
-
|
|
906
|
-
pk,
|
|
1074
|
+
const item = {
|
|
1075
|
+
pk: composeStreamPk(streamId),
|
|
907
1076
|
sk: snapshotSk,
|
|
908
1077
|
__edd_e__: snapshotEntityType,
|
|
909
|
-
streamId:
|
|
1078
|
+
streamId: composeStreamIdString(streamId),
|
|
910
1079
|
asOfVersion,
|
|
911
1080
|
state: stored,
|
|
912
1081
|
timestamp: now,
|
|
913
|
-
}
|
|
1082
|
+
};
|
|
1083
|
+
// The snapshot's index entries (#149), stored raw on the item itself,
|
|
1084
|
+
// so they are written by the same `Put` and count towards its size.
|
|
1085
|
+
for (const idx of snapshotIndexes) {
|
|
1086
|
+
const entry = yield* deriveSnapshotEntry(idx, idx.snapshotEntry, state, streamId, asOfVersion, operation);
|
|
1087
|
+
if (entry === undefined)
|
|
1088
|
+
continue;
|
|
1089
|
+
const events = eventPartitions?.get(idx.name);
|
|
1090
|
+
if (events !== undefined && events.partition !== entry.partition) {
|
|
1091
|
+
return yield* new ValidationError({
|
|
1092
|
+
entityType: snapshotEntityType,
|
|
1093
|
+
operation,
|
|
1094
|
+
cause: `The inline snapshot's partition in index "${idx.name}" is ` +
|
|
1095
|
+
`${JSON.stringify(entry.partition)}, but ${events.subject} has ` +
|
|
1096
|
+
`${JSON.stringify(events.partition)}: an append's events and its inline snapshot ` +
|
|
1097
|
+
"must be in the same partition of an index. Nothing was written.",
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
item[idx.pk] = entry.partition;
|
|
1101
|
+
item[idx.sk] = entry.key;
|
|
1102
|
+
}
|
|
1103
|
+
return toAttributeMap(item);
|
|
914
1104
|
});
|
|
915
1105
|
/**
|
|
916
1106
|
* The stream's own items of an append — everything but the caller's
|
|
@@ -1021,6 +1211,10 @@ export const makeStream = (config) => {
|
|
|
1021
1211
|
else if (options?.metadata !== undefined) {
|
|
1022
1212
|
encodedMetadata = options.metadata;
|
|
1023
1213
|
}
|
|
1214
|
+
// The partition the events have in each index that derives one (#149),
|
|
1215
|
+
// by index name, with the first event that has it: the events and the
|
|
1216
|
+
// inline snapshot of one append must agree.
|
|
1217
|
+
const eventPartitions = new Map();
|
|
1024
1218
|
// Build the event puts — one Put per event, each with attribute_not_exists(pk).
|
|
1025
1219
|
// Events are encoded to wire form through their schema (codec symmetry
|
|
1026
1220
|
// with the read path, which decodes through the same schema), so this is
|
|
@@ -1079,15 +1273,39 @@ export const makeStream = (config) => {
|
|
|
1079
1273
|
item.metadata = itemMetadata;
|
|
1080
1274
|
}
|
|
1081
1275
|
// Stream indexes (#140): the derived key, stored raw, and for a
|
|
1082
|
-
// GSI the stream's partition key
|
|
1083
|
-
//
|
|
1276
|
+
// GSI the stream's partition key — or the derived partition, raw
|
|
1277
|
+
// too (#149). The attributes are part of the event item itself,
|
|
1278
|
+
// so they count towards the size checks. Both functions run for
|
|
1279
|
+
// every event, so each one's contract is checked on every event.
|
|
1280
|
+
const subject = `the event at version ${version}`;
|
|
1084
1281
|
for (const idx of streamIndexes) {
|
|
1085
|
-
const derived = yield*
|
|
1086
|
-
|
|
1282
|
+
const derived = yield* deriveIndexValue(idx, "key", subject, () => idx.key(event, version, streamId));
|
|
1283
|
+
let partition = pk;
|
|
1284
|
+
if (idx.derivePartition !== undefined) {
|
|
1285
|
+
const derivePartition = idx.derivePartition;
|
|
1286
|
+
partition = yield* deriveIndexValue(idx, "partition", subject, () => derivePartition(event, version, streamId));
|
|
1287
|
+
if (partition !== undefined) {
|
|
1288
|
+
const first = eventPartitions.get(idx.name);
|
|
1289
|
+
if (first === undefined) {
|
|
1290
|
+
eventPartitions.set(idx.name, { partition, subject });
|
|
1291
|
+
}
|
|
1292
|
+
else if (first.partition !== partition) {
|
|
1293
|
+
return yield* new ValidationError({
|
|
1294
|
+
entityType,
|
|
1295
|
+
operation: "EventStore.append.indexes",
|
|
1296
|
+
cause: `The partition of index "${idx.name}" for ${subject} is ` +
|
|
1297
|
+
`${JSON.stringify(partition)}, but ${first.subject} has ` +
|
|
1298
|
+
`${JSON.stringify(first.partition)}: every event of one append must be ` +
|
|
1299
|
+
"in the same partition of an index. Nothing was written.",
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (derived === undefined || partition === undefined)
|
|
1087
1305
|
continue;
|
|
1088
1306
|
item[idx.sk] = derived;
|
|
1089
1307
|
if (idx.type === "gsi")
|
|
1090
|
-
item[idx.pk] =
|
|
1308
|
+
item[idx.pk] = partition;
|
|
1091
1309
|
}
|
|
1092
1310
|
return {
|
|
1093
1311
|
Put: {
|
|
@@ -1136,7 +1354,7 @@ export const makeStream = (config) => {
|
|
|
1136
1354
|
snapshotPut = {
|
|
1137
1355
|
Put: {
|
|
1138
1356
|
TableName: tableName,
|
|
1139
|
-
Item: yield* buildSnapshotItem(
|
|
1357
|
+
Item: yield* buildSnapshotItem(streamId, snapshotState, expectedVersion + events.length, now, "EventStore.append.snapshot", eventPartitions),
|
|
1140
1358
|
},
|
|
1141
1359
|
};
|
|
1142
1360
|
}
|
|
@@ -1215,6 +1433,7 @@ export const makeStream = (config) => {
|
|
|
1215
1433
|
if (sentinelPut !== undefined) {
|
|
1216
1434
|
pushStreamItem(sentinelPut, entityType, "the idempotency sentinel");
|
|
1217
1435
|
}
|
|
1436
|
+
const snapshotIndex = snapshotPut === undefined ? -1 : items.length;
|
|
1218
1437
|
if (snapshotPut !== undefined) {
|
|
1219
1438
|
pushStreamItem(snapshotPut, snapshotEntityType, "the inline snapshot");
|
|
1220
1439
|
}
|
|
@@ -1227,6 +1446,7 @@ export const makeStream = (config) => {
|
|
|
1227
1446
|
eventCount: eventItems.length,
|
|
1228
1447
|
additional,
|
|
1229
1448
|
sentinelIndex,
|
|
1449
|
+
snapshotIndex,
|
|
1230
1450
|
expectedVersion,
|
|
1231
1451
|
};
|
|
1232
1452
|
};
|
|
@@ -1289,7 +1509,7 @@ export const makeStream = (config) => {
|
|
|
1289
1509
|
if (snapshot === undefined)
|
|
1290
1510
|
return yield* snapshotUnavailable("measureSnapshot");
|
|
1291
1511
|
const now = DateTime.formatIso(yield* DateTime.now);
|
|
1292
|
-
const item = yield* buildSnapshotItem(
|
|
1512
|
+
const item = yield* buildSnapshotItem(streamId, state, asOfVersion, now, "EventStore.measureSnapshot");
|
|
1293
1513
|
return itemBytes(item);
|
|
1294
1514
|
});
|
|
1295
1515
|
// ---------------------------------------------------------------------------
|
|
@@ -1336,8 +1556,11 @@ export const makeStream = (config) => {
|
|
|
1336
1556
|
// Versions are integers, so the inclusive lower bound `afterVersion + 1`
|
|
1337
1557
|
// is exactly the old exclusive `#sk > eventSk(afterVersion)`. The upper
|
|
1338
1558
|
// bound keeps the snapshot item (which sorts after every event) out of the
|
|
1339
|
-
// scanned range.
|
|
1340
|
-
const
|
|
1559
|
+
// scanned range; `toVersion` narrows it to the versions wanted.
|
|
1560
|
+
const upper = options?.toVersion === undefined ? maxEventSk : composeEventSk(options.toVersion);
|
|
1561
|
+
if (options?.toVersion !== undefined && options.toVersion <= afterVersion)
|
|
1562
|
+
return [];
|
|
1563
|
+
const query = withReadOptions(buildEventsQuery(streamId).pipe(Query.where({ between: [composeEventSk(afterVersion + 1), upper] })), options);
|
|
1341
1564
|
return yield* Query.collect(query);
|
|
1342
1565
|
});
|
|
1343
1566
|
// ---------------------------------------------------------------------------
|
|
@@ -1379,7 +1602,7 @@ export const makeStream = (config) => {
|
|
|
1379
1602
|
const now = DateTime.formatIso(yield* DateTime.now);
|
|
1380
1603
|
// Packed per `snapshot.storedAs` (#147): the diagnosis below measures
|
|
1381
1604
|
// the stored bytes.
|
|
1382
|
-
const item = yield* buildSnapshotItem(
|
|
1605
|
+
const item = yield* buildSnapshotItem(streamId, state, asOfVersion, now, "EventStore.writeSnapshot");
|
|
1383
1606
|
const put = {
|
|
1384
1607
|
TableName: tableName,
|
|
1385
1608
|
Item: item,
|
|
@@ -1397,11 +1620,19 @@ export const makeStream = (config) => {
|
|
|
1397
1620
|
item: { Put: put },
|
|
1398
1621
|
target: () => transactItemTarget({ Put: put }, tableName, ["pk", "sk"], snapshotEntityType, "the snapshot"),
|
|
1399
1622
|
}),
|
|
1400
|
-
keys: () => ({
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
entityType: snapshotEntityType,
|
|
1623
|
+
keys: () => ({
|
|
1624
|
+
partition: ["pk", ...snapshotIndexes.map((idx) => idx.pk)],
|
|
1625
|
+
sort: ["sk", ...snapshotIndexes.map((idx) => idx.sk)],
|
|
1404
1626
|
}),
|
|
1627
|
+
// A snapshot index entry's empty or over-long value (#149) is named
|
|
1628
|
+
// by its index, as an inline snapshot's is.
|
|
1629
|
+
rewrite: (diagnosis, original) => (diagnosis._tag === "KeyTooLong" || diagnosis._tag === "EmptyKey"
|
|
1630
|
+
? indexKeyError(diagnosis, original, true, "the snapshot", "EventStore.writeSnapshot")
|
|
1631
|
+
: undefined) ??
|
|
1632
|
+
diagnosedValidationError(diagnosis, original, {
|
|
1633
|
+
operation: "EventStore.writeSnapshot",
|
|
1634
|
+
entityType: snapshotEntityType,
|
|
1635
|
+
}),
|
|
1405
1636
|
}));
|
|
1406
1637
|
});
|
|
1407
1638
|
/**
|
|
@@ -1591,15 +1822,44 @@ export const makeStream = (config) => {
|
|
|
1591
1822
|
// Stream indexes — query.index / readIndex (#140)
|
|
1592
1823
|
// ---------------------------------------------------------------------------
|
|
1593
1824
|
/**
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1597
|
-
*
|
|
1825
|
+
* @internal Why `partition` cannot be passed for index `name`, or
|
|
1826
|
+
* `undefined` when it can: required (a string) for an index that derives
|
|
1827
|
+
* its partition (#149), refused for any other. Unreachable through the
|
|
1828
|
+
* types. An unknown name is left to {@link buildIndexQuery}.
|
|
1829
|
+
*/
|
|
1830
|
+
const invalidIndexPartition = (name, partition) => {
|
|
1831
|
+
const idx = streamIndexes.find((candidate) => candidate.name === name);
|
|
1832
|
+
if (idx === undefined)
|
|
1833
|
+
return undefined;
|
|
1834
|
+
if (idx.partition === "derived") {
|
|
1835
|
+
return typeof partition === "string"
|
|
1836
|
+
? undefined
|
|
1837
|
+
: `Index "${name}" derives its partition: pass the partition to read as ` +
|
|
1838
|
+
`options.partition (a string); received ${String(partition)}.`;
|
|
1839
|
+
}
|
|
1840
|
+
return partition === undefined
|
|
1841
|
+
? undefined
|
|
1842
|
+
: `Index "${name}" is scoped to the stream; it takes no partition.`;
|
|
1843
|
+
};
|
|
1844
|
+
/**
|
|
1845
|
+
* A query of one stream index. A stream-scoped index is queried in the
|
|
1846
|
+
* stream's partition: the LSI shares the table's `pk`; a GSI's `pk`
|
|
1847
|
+
* attribute carries the same value. No sort-key condition — the index holds
|
|
1848
|
+
* only this stream's indexed events under that key; `Query.where` narrows
|
|
1849
|
+
* the derived-key range.
|
|
1850
|
+
*
|
|
1851
|
+
* An index that derives its partition (#149) is queried in `partition`,
|
|
1852
|
+
* which other streams (and this one's other stream ids, and snapshots) may
|
|
1853
|
+
* share: the `__edd_e__` filter keeps this stream's events, and a filter on
|
|
1854
|
+
* the table's `pk` — projected by every index — keeps `streamId`'s.
|
|
1598
1855
|
*
|
|
1599
1856
|
* @throws `[EDD-9066]` for a name the stream does not declare (unreachable
|
|
1600
1857
|
* through the types).
|
|
1858
|
+
* @throws `[EDD-9080]` for a missing `partition` on an index that derives
|
|
1859
|
+
* it, or one passed to an index that does not (unreachable through the
|
|
1860
|
+
* types).
|
|
1601
1861
|
*/
|
|
1602
|
-
const buildIndexQuery = (name, streamId) => {
|
|
1862
|
+
const buildIndexQuery = (name, streamId, partition) => {
|
|
1603
1863
|
const idx = streamIndexes.find((candidate) => candidate.name === name);
|
|
1604
1864
|
if (idx === undefined) {
|
|
1605
1865
|
throw new Error(`[EDD-9066] EventStream "${config.streamName}": unknown index "${String(name)}". ` +
|
|
@@ -1607,11 +1867,16 @@ export const makeStream = (config) => {
|
|
|
1607
1867
|
? "The stream declares no indexes."
|
|
1608
1868
|
: `Declared: ${streamIndexes.map((candidate) => `"${candidate.name}"`).join(", ")}.`));
|
|
1609
1869
|
}
|
|
1610
|
-
|
|
1870
|
+
const partitionProblem = invalidIndexPartition(name, partition);
|
|
1871
|
+
if (partitionProblem !== undefined) {
|
|
1872
|
+
throw new Error(`[EDD-9080] EventStream "${config.streamName}": ${partitionProblem}`);
|
|
1873
|
+
}
|
|
1874
|
+
const streamPk = composeStreamPk(streamId);
|
|
1875
|
+
const query = Query.make({
|
|
1611
1876
|
tableName: "",
|
|
1612
1877
|
indexName: idx.index,
|
|
1613
1878
|
pkField: idx.pk,
|
|
1614
|
-
pkValue:
|
|
1879
|
+
pkValue: idx.partition === "derived" ? partition : streamPk,
|
|
1615
1880
|
skField: idx.sk,
|
|
1616
1881
|
entityTypes: [entityType],
|
|
1617
1882
|
decoder: indexDecoder(idx),
|
|
@@ -1639,6 +1904,7 @@ export const makeStream = (config) => {
|
|
|
1639
1904
|
}
|
|
1640
1905
|
: {}),
|
|
1641
1906
|
});
|
|
1907
|
+
return idx.partition === "derived" ? Query.filterExpr(query, streamPkFilter(streamPk)) : query;
|
|
1642
1908
|
};
|
|
1643
1909
|
/**
|
|
1644
1910
|
* @internal The decoder of events read through `idx`.
|
|
@@ -1685,7 +1951,11 @@ export const makeStream = (config) => {
|
|
|
1685
1951
|
if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
|
|
1686
1952
|
return yield* refuse(`limit must be a positive integer; received ${String(limit)}. Nothing was sent.`);
|
|
1687
1953
|
}
|
|
1688
|
-
|
|
1954
|
+
const partitionProblem = invalidIndexPartition(name, options?.partition);
|
|
1955
|
+
if (partitionProblem !== undefined) {
|
|
1956
|
+
return yield* refuse(`${partitionProblem} Nothing was sent.`);
|
|
1957
|
+
}
|
|
1958
|
+
let query = buildIndexQuery(name, streamId, options?.partition);
|
|
1689
1959
|
if (options?.beginsWith !== undefined) {
|
|
1690
1960
|
query = Query.where(query, { beginsWith: options.beginsWith });
|
|
1691
1961
|
}
|
|
@@ -1702,7 +1972,7 @@ export const makeStream = (config) => {
|
|
|
1702
1972
|
});
|
|
1703
1973
|
const queryNamespace = {
|
|
1704
1974
|
events: (streamId) => buildEventsQuery(streamId),
|
|
1705
|
-
index: (name, streamId) => buildIndexQuery(name, streamId),
|
|
1975
|
+
index: (name, streamId, options) => buildIndexQuery(name, streamId, options?.partition),
|
|
1706
1976
|
};
|
|
1707
1977
|
// ---------------------------------------------------------------------------
|
|
1708
1978
|
// Return EventStream
|
|
@@ -1720,6 +1990,9 @@ export const makeStream = (config) => {
|
|
|
1720
1990
|
},
|
|
1721
1991
|
streamName: config.streamName,
|
|
1722
1992
|
eventSchema: eventUnion,
|
|
1993
|
+
streamIdFields: compositeFields,
|
|
1994
|
+
eventMarker: entityType,
|
|
1995
|
+
parseStreamId: (id) => parseStreamIdWith(config.streamName, compositeFields, id),
|
|
1723
1996
|
snapshotConfig: snapshotSettings,
|
|
1724
1997
|
storedAs: storage,
|
|
1725
1998
|
indexes: indexSettings,
|
|
@@ -1775,6 +2048,9 @@ export const bind = (stream) => Effect.gen(function* () {
|
|
|
1775
2048
|
},
|
|
1776
2049
|
streamName: stream.streamName,
|
|
1777
2050
|
eventSchema: stream.eventSchema,
|
|
2051
|
+
streamIdFields: stream.streamIdFields,
|
|
2052
|
+
eventMarker: stream.eventMarker,
|
|
2053
|
+
parseStreamId: (id) => stream.parseStreamId(id),
|
|
1778
2054
|
snapshotConfig: stream.snapshotConfig,
|
|
1779
2055
|
storedAs: stream.storedAs,
|
|
1780
2056
|
indexes: stream.indexes,
|
|
@@ -1785,7 +2061,7 @@ export const bind = (stream) => Effect.gen(function* () {
|
|
|
1785
2061
|
readFrom: (streamId, afterVersion, options) => provide(stream.readFrom(streamId, afterVersion, options)),
|
|
1786
2062
|
currentVersion: (streamId, options) => provide(stream.currentVersion(streamId, options)),
|
|
1787
2063
|
readLatest: (streamId, options) => provide(stream.readLatest(streamId, options)),
|
|
1788
|
-
readIndex: (name, streamId, options) => provide(stream.readIndex(name, streamId, options)),
|
|
2064
|
+
readIndex: (name, streamId, ...options) => provide(stream.readIndex(name, streamId, ...options)),
|
|
1789
2065
|
// The captured context carries a `Compression` registry provided where
|
|
1790
2066
|
// the stream was bound (#147).
|
|
1791
2067
|
measureAppend: (streamId, events, expectedVersion, options) => provide(stream.measureAppend(streamId, events, expectedVersion, options)),
|
|
@@ -1810,12 +2086,22 @@ export const bind = (stream) => Effect.gen(function* () {
|
|
|
1810
2086
|
* Neither list names a key attribute: table and index keys are always
|
|
1811
2087
|
* projected.
|
|
1812
2088
|
*/
|
|
1813
|
-
const projectionDefinition = (projection) => projection === "all"
|
|
2089
|
+
const projectionDefinition = (projection, snapshot) => projection === "all"
|
|
1814
2090
|
? { ProjectionType: "ALL" }
|
|
1815
2091
|
: {
|
|
1816
2092
|
ProjectionType: "INCLUDE",
|
|
1817
|
-
NonKeyAttributes: projection === "keys"
|
|
2093
|
+
NonKeyAttributes: projection === "keys"
|
|
2094
|
+
? ["__edd_e__"]
|
|
2095
|
+
: snapshot
|
|
2096
|
+
? [...EVENT_ITEM_ATTRIBUTES, ...SNAPSHOT_ONLY_ATTRIBUTES].sort()
|
|
2097
|
+
: [...EVENT_ITEM_ATTRIBUTES],
|
|
1818
2098
|
};
|
|
2099
|
+
/**
|
|
2100
|
+
* @internal The snapshot item's attributes an event item does not carry: what
|
|
2101
|
+
* a `"withoutMetadata"` index that holds snapshots (#149) projects besides
|
|
2102
|
+
* {@link EVENT_ITEM_ATTRIBUTES}.
|
|
2103
|
+
*/
|
|
2104
|
+
const SNAPSHOT_ONLY_ATTRIBUTES = ["asOfVersion", "state"];
|
|
1819
2105
|
/**
|
|
1820
2106
|
* Derive the `CreateTable` fragments for the stream indexes (#140) of the given
|
|
1821
2107
|
* streams (`EventStream` or `BoundEventStream`) — `AttributeDefinitions`,
|
|
@@ -1830,7 +2116,7 @@ const projectionDefinition = (projection) => projection === "all"
|
|
|
1830
2116
|
* | `projection` | `Projection` |
|
|
1831
2117
|
* |---|---|
|
|
1832
2118
|
* | `"all"` (default) | `{ ProjectionType: "ALL" }` |
|
|
1833
|
-
* | `"withoutMetadata"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__", "data", "eventType", "streamId", "timestamp", "version"] }` |
|
|
2119
|
+
* | `"withoutMetadata"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__", "data", "eventType", "streamId", "timestamp", "version"] }` — plus `"asOfVersion"` and `"state"` when a stream declaring the index puts its snapshot in it (#149) |
|
|
1834
2120
|
* | `"keys"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__"] }` — not `KEYS_ONLY`: every read filters on `__edd_e__` |
|
|
1835
2121
|
*
|
|
1836
2122
|
* The deployed index must project what is declared — copy `Projection` from
|
|
@@ -1841,7 +2127,12 @@ const projectionDefinition = (projection) => projection === "all"
|
|
|
1841
2127
|
* table over the limit).
|
|
1842
2128
|
*
|
|
1843
2129
|
* An index shared by several streams with the same definition is emitted
|
|
1844
|
-
* once
|
|
2130
|
+
* once — the way several streams write one GSI whose partition they derive
|
|
2131
|
+
* (`partition`, #149): an order's line, payment and shipment streams under
|
|
2132
|
+
* `ORDER#<orderId>`, say. Whether each declaration derives its partition or
|
|
2133
|
+
* indexes its snapshot is not part of the definition: the index's key schema
|
|
2134
|
+
* and projection kind are. Lists are sorted by index name, attributes by
|
|
2135
|
+
* attribute name.
|
|
1845
2136
|
*
|
|
1846
2137
|
* - An LSI can only be created with the table, and caps the item collection
|
|
1847
2138
|
* of every partition key value in the table at 10 GB — entity partitions
|
|
@@ -1869,25 +2160,32 @@ const projectionDefinition = (projection) => projection === "all"
|
|
|
1869
2160
|
*
|
|
1870
2161
|
* @throws `[EDD-9067]` when two streams define the same physical index
|
|
1871
2162
|
* differently (another type, other key attributes, or another projection).
|
|
2163
|
+
* Whether a declaration derives its partition or indexes its snapshot is not
|
|
2164
|
+
* part of the index's definition: streams that differ only there share it.
|
|
1872
2165
|
*/
|
|
1873
2166
|
export const indexDefinitions = (...streams) => {
|
|
2167
|
+
/** Per physical index: its definition, its first declarer, and whether any declarer indexes snapshots. */
|
|
1874
2168
|
const byIndex = new Map();
|
|
1875
2169
|
for (const stream of streams) {
|
|
1876
2170
|
for (const [name, raw] of Object.entries(stream.indexes)) {
|
|
1877
|
-
const settings = {
|
|
2171
|
+
const settings = {
|
|
2172
|
+
type: raw.type,
|
|
2173
|
+
index: raw.index,
|
|
2174
|
+
pk: raw.pk,
|
|
2175
|
+
sk: raw.sk,
|
|
2176
|
+
projection: raw.projection ?? "all",
|
|
2177
|
+
};
|
|
2178
|
+
const snapshot = raw.snapshot === true;
|
|
1878
2179
|
const owner = `index "${name}" of stream "${stream.streamName}"`;
|
|
1879
2180
|
const existing = byIndex.get(settings.index);
|
|
1880
2181
|
if (existing === undefined) {
|
|
1881
|
-
byIndex.set(settings.index, {
|
|
1882
|
-
type: settings.type,
|
|
1883
|
-
index: settings.index,
|
|
1884
|
-
pk: settings.pk,
|
|
1885
|
-
sk: settings.sk,
|
|
1886
|
-
projection: settings.projection,
|
|
1887
|
-
owner,
|
|
1888
|
-
});
|
|
2182
|
+
byIndex.set(settings.index, { ...settings, owner, snapshot });
|
|
1889
2183
|
continue;
|
|
1890
2184
|
}
|
|
2185
|
+
// Several streams writing one GSI they derive the partition of (#149)
|
|
2186
|
+
// is the point: one declaration indexing snapshots and another not
|
|
2187
|
+
// still share it, and the projection covers the snapshots.
|
|
2188
|
+
existing.snapshot ||= snapshot;
|
|
1891
2189
|
if (existing.type !== settings.type ||
|
|
1892
2190
|
existing.pk !== settings.pk ||
|
|
1893
2191
|
existing.sk !== settings.sk ||
|
|
@@ -1910,7 +2208,7 @@ export const indexDefinitions = (...streams) => {
|
|
|
1910
2208
|
{ AttributeName: settings.pk, KeyType: "HASH" },
|
|
1911
2209
|
{ AttributeName: settings.sk, KeyType: "RANGE" },
|
|
1912
2210
|
],
|
|
1913
|
-
Projection: projectionDefinition(settings.projection),
|
|
2211
|
+
Projection: projectionDefinition(settings.projection, settings.snapshot),
|
|
1914
2212
|
};
|
|
1915
2213
|
attributes.add(settings.sk);
|
|
1916
2214
|
if (settings.type === "gsi") {
|
|
@@ -2450,4 +2748,46 @@ export const foldFrom = Function.dual(3, (decider, startState, events) => {
|
|
|
2450
2748
|
}
|
|
2451
2749
|
return state;
|
|
2452
2750
|
});
|
|
2751
|
+
// ---------------------------------------------------------------------------
|
|
2752
|
+
// Change-feed projections (#151)
|
|
2753
|
+
// ---------------------------------------------------------------------------
|
|
2754
|
+
export { batchResponse, decoder, eventFilterPattern, processBatch, projection, } from "./internal/EventProjection.js";
|
|
2755
|
+
/**
|
|
2756
|
+
* Bind a projection to the services its operations need — `DynamoClient`,
|
|
2757
|
+
* the `TableConfig`s of the stream's table and the read model's, `evolve`'s
|
|
2758
|
+
* requirements, and a `Compression` registry for packed event data — so every
|
|
2759
|
+
* operation has `R = never`. Like {@link bind}, it checks the registry holds
|
|
2760
|
+
* the codecs the stream's `storedAs` names (`[EDD-9078]`).
|
|
2761
|
+
*
|
|
2762
|
+
* @example
|
|
2763
|
+
* ```ts
|
|
2764
|
+
* const summaries = yield* EventStore.bindProjection(OrderSummaryProjection)
|
|
2765
|
+
* export const handler = (event: { Records: Array<EventStore.StreamRecord> }) =>
|
|
2766
|
+
* summaries.processBatch(event.Records, { source: "lambda" }).pipe(
|
|
2767
|
+
* Effect.map((result) => result.response),
|
|
2768
|
+
* Effect.runPromise,
|
|
2769
|
+
* )
|
|
2770
|
+
* ```
|
|
2771
|
+
*/
|
|
2772
|
+
export const bindProjection = (projection) => Effect.gen(function* () {
|
|
2773
|
+
const ctx = yield* Effect.context();
|
|
2774
|
+
// The codec check `bind` makes, against the registry captured here.
|
|
2775
|
+
yield* Effect.provide(bind(projection.stream), ctx);
|
|
2776
|
+
const provide = (effect) => Effect.provide(effect, ctx);
|
|
2777
|
+
return {
|
|
2778
|
+
...projection,
|
|
2779
|
+
pipe() {
|
|
2780
|
+
// eslint-disable-next-line prefer-rest-params
|
|
2781
|
+
return Pipeable.pipeArguments(this, arguments);
|
|
2782
|
+
},
|
|
2783
|
+
applyBatch: (events, options) => provide(projection.applyBatch(events, options)),
|
|
2784
|
+
apply: (event) => provide(projection.apply(event)),
|
|
2785
|
+
catchUp: (streamId) => provide(projection.catchUp(streamId)),
|
|
2786
|
+
// A multi-projection batch decodes through this, under the captured context.
|
|
2787
|
+
[ProjectionDecode]: (image) => provide(projection[ProjectionDecode](image)),
|
|
2788
|
+
rebuild: (streamId) => provide(projection.rebuild(streamId)),
|
|
2789
|
+
// Both overloads share one implementation.
|
|
2790
|
+
processBatch: ((records, options) => provide(projection.processBatch(records, options))),
|
|
2791
|
+
};
|
|
2792
|
+
});
|
|
2453
2793
|
//# sourceMappingURL=EventStore.js.map
|