effect-dynamodb 1.26.0 → 1.27.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/EventStore.d.ts +272 -53
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +363 -98
- package/dist/EventStore.js.map +1 -1
- package/package.json +2 -2
package/dist/EventStore.js
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
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 —
|
|
@@ -35,8 +37,10 @@ import { isDispatchEligible, packedShape, packValue, unpackStoredSync, } from "@
|
|
|
35
37
|
import * as KeyComposer from "@effect-dynamodb/schema/KeyComposer.js";
|
|
36
38
|
import { Context, DateTime, Effect, Function, Option, Pipeable, Result, Schedule, Schema, } from "effect";
|
|
37
39
|
import { DynamoClient } from "./DynamoClient.js";
|
|
40
|
+
import { eq } from "./internal/Expr.js";
|
|
38
41
|
import { itemBytes, transactItemBytes } from "./internal/ItemSize.js";
|
|
39
42
|
import { codecRequirements, currentRegistry, decodeImage, imageEntityType, runtimeCodecCheck, unregisteredCodecError, withRegistry, } from "./internal/Packed.js";
|
|
43
|
+
import { createPathBuilder } from "./internal/PathBuilder.js";
|
|
40
44
|
import { buildTransactWriteItems, GUARDED_TRANSACTION_ATTEMPTS, judgeCancellation, transactItemTarget, } from "./internal/TransactWriteOps.js";
|
|
41
45
|
import { diagnosedValidationError, diagnoseFailure, mergeKeyAttributes, withCause, } from "./internal/WriteDiagnosis.js";
|
|
42
46
|
import { fromAttributeMap, toAttributeMap } from "./Marshaller.js";
|
|
@@ -145,21 +149,31 @@ const STREAM_INDEX_CONFIG_KEYS = [
|
|
|
145
149
|
"pk",
|
|
146
150
|
"sk",
|
|
147
151
|
"key",
|
|
152
|
+
"partition",
|
|
153
|
+
"snapshot",
|
|
148
154
|
"projection",
|
|
149
155
|
];
|
|
156
|
+
/**
|
|
157
|
+
* @internal `pk = <streamPk>`: keeps one stream id's items in a partition of a
|
|
158
|
+
* stream index several streams (or stream ids) share (#149). The table's `pk`
|
|
159
|
+
* is projected into every index, whatever its projection.
|
|
160
|
+
*/
|
|
161
|
+
const streamPkFilter = (streamPk) => eq(createPathBuilder().pk, streamPk);
|
|
150
162
|
/**
|
|
151
163
|
* @internal Validate `makeStream({ indexes })` at definition time.
|
|
152
164
|
*
|
|
153
165
|
* @throws `[EDD-9063]` for a malformed entry (unknown `type`, a `gsi` without
|
|
154
|
-
* `pk`, an `lsi` with `pk`, an empty name, a
|
|
155
|
-
*
|
|
156
|
-
*
|
|
166
|
+
* `pk`, an `lsi` with `pk`, `partition` or `snapshot`, an empty name, a
|
|
167
|
+
* `key` / `partition` / `snapshot` that is not a function, a `snapshot`
|
|
168
|
+
* without `partition` or on a stream without a snapshot config, an unknown
|
|
169
|
+
* `projection`, a member the entry type does not declare — a misspelled
|
|
170
|
+
* `projection` would otherwise silently mean `"all"`).
|
|
157
171
|
* @throws `[EDD-9064]` for an index attribute that collides with a
|
|
158
172
|
* stream-owned attribute.
|
|
159
173
|
* @throws `[EDD-9065]` for two indexes sharing a physical index name or an
|
|
160
174
|
* attribute.
|
|
161
175
|
*/
|
|
162
|
-
const resolveStreamIndexes = (streamName, indexes) => {
|
|
176
|
+
const resolveStreamIndexes = (streamName, indexes, hasSnapshotConfig) => {
|
|
163
177
|
if (indexes === undefined)
|
|
164
178
|
return [];
|
|
165
179
|
const fail = (code, message) => {
|
|
@@ -196,7 +210,30 @@ const resolveStreamIndexes = (streamName, indexes) => {
|
|
|
196
210
|
'declare type: "gsi".');
|
|
197
211
|
}
|
|
198
212
|
if (typeof entry.key !== "function") {
|
|
199
|
-
fail("EDD-9063", `index "${name}": key must be a function (event, version) => string | undefined.`);
|
|
213
|
+
fail("EDD-9063", `index "${name}": key must be a function (event, version, streamId) => string | undefined.`);
|
|
214
|
+
}
|
|
215
|
+
if (entry.partition !== undefined) {
|
|
216
|
+
if (type !== "gsi") {
|
|
217
|
+
fail("EDD-9063", `index "${name}": an "lsi" index is partitioned by the stream; only a "gsi" index ` +
|
|
218
|
+
'derives its partition. Remove partition, or declare type: "gsi".');
|
|
219
|
+
}
|
|
220
|
+
if (typeof entry.partition !== "function") {
|
|
221
|
+
fail("EDD-9063", `index "${name}": partition must be a function (event, version, streamId) => string | undefined.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (entry.snapshot !== undefined) {
|
|
225
|
+
if (typeof entry.snapshot !== "function") {
|
|
226
|
+
fail("EDD-9063", `index "${name}": snapshot must be a function (state, asOfVersion, streamId) => ` +
|
|
227
|
+
"{ partition, key } | undefined.");
|
|
228
|
+
}
|
|
229
|
+
if (entry.partition === undefined) {
|
|
230
|
+
fail("EDD-9063", `index "${name}": snapshot requires partition — only a "gsi" index with a derived ` +
|
|
231
|
+
"partition indexes the stream's snapshot.");
|
|
232
|
+
}
|
|
233
|
+
if (!hasSnapshotConfig) {
|
|
234
|
+
fail("EDD-9063", `index "${name}": snapshot requires the stream's snapshot config ` +
|
|
235
|
+
"(makeStream({ snapshot: { schema } })); the stream writes no snapshot to index.");
|
|
236
|
+
}
|
|
200
237
|
}
|
|
201
238
|
const projection = (entry.projection ?? "all");
|
|
202
239
|
if (projection !== "all" && projection !== "withoutMetadata" && projection !== "keys") {
|
|
@@ -233,7 +270,11 @@ const resolveStreamIndexes = (streamName, indexes) => {
|
|
|
233
270
|
pk,
|
|
234
271
|
sk,
|
|
235
272
|
projection,
|
|
273
|
+
partition: entry.partition === undefined ? "stream" : "derived",
|
|
274
|
+
snapshot: entry.snapshot !== undefined,
|
|
236
275
|
key: entry.key,
|
|
276
|
+
derivePartition: entry.partition,
|
|
277
|
+
snapshotEntry: entry.snapshot,
|
|
237
278
|
});
|
|
238
279
|
}
|
|
239
280
|
return resolved;
|
|
@@ -430,11 +471,20 @@ const eventTimestampsProblem = (timestamps, count, label, events) => {
|
|
|
430
471
|
* `indexes` declares sub-streams ordered by a key derived from each event
|
|
431
472
|
* (#140), on a local (default) or global secondary index. `append` writes each
|
|
432
473
|
* 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
|
|
474
|
+
* event item. Idempotency sentinels never carry them; snapshots carry an
|
|
475
|
+
* entry only in a GSI that derives its partition and declares `snapshot`.
|
|
476
|
+
* Read with {@link EventStream.readIndex} or `query.index`, and create the
|
|
477
|
+
* indexes with {@link indexDefinitions}. Each index's `projection` (default
|
|
478
|
+
* `"all"`) sets what it copies from each event and how it is read — see
|
|
479
|
+
* {@link StreamIndexProjection}.
|
|
480
|
+
*
|
|
481
|
+
* A GSI's `partition` (#149) derives the partition from the stream id and the
|
|
482
|
+
* event instead, stored raw, so several streams declaring the same GSI write
|
|
483
|
+
* one partition of it — an order's line, payment and shipment streams under
|
|
484
|
+
* `ORDER#<orderId>`, read by one query — and its `snapshot` puts each stream's
|
|
485
|
+
* snapshot item in that partition too, in the same `Put` (so an inline
|
|
486
|
+
* snapshot enters the index in the append's own transaction). See
|
|
487
|
+
* {@link GlobalStreamIndexConfig}.
|
|
438
488
|
*
|
|
439
489
|
* Index attributes are written only at append time and events are never
|
|
440
490
|
* rewritten. Declaring an index on a stream that already holds events, or
|
|
@@ -446,6 +496,36 @@ const eventTimestampsProblem = (timestamps, count, label, events) => {
|
|
|
446
496
|
*
|
|
447
497
|
* @example
|
|
448
498
|
* ```typescript
|
|
499
|
+
* // Lines and payments of an order share `gsi1` under `ORDER#<orderId>`.
|
|
500
|
+
* const OrderLines = EventStore.makeStream({
|
|
501
|
+
* table: EventsTable,
|
|
502
|
+
* streamName: "OrderLine",
|
|
503
|
+
* events: [LineAdded, LineRemoved],
|
|
504
|
+
* streamId: { composite: ["orderId", "lineId"] },
|
|
505
|
+
* snapshot: { schema: LineState, mode: "inline" },
|
|
506
|
+
* indexes: {
|
|
507
|
+
* byOrder: {
|
|
508
|
+
* type: "gsi",
|
|
509
|
+
* index: "gsi1",
|
|
510
|
+
* pk: "gsi1pk",
|
|
511
|
+
* sk: "gsi1sk",
|
|
512
|
+
* partition: (_event, _version, streamId) => `ORDER#${streamId.orderId}`,
|
|
513
|
+
* key: (_event, version, streamId) => `LINE#${streamId.lineId}#EVT#${pad(version)}`,
|
|
514
|
+
* snapshot: (_state, _asOfVersion, streamId) => ({
|
|
515
|
+
* partition: `ORDER#${streamId.orderId}`,
|
|
516
|
+
* key: `LINE#${streamId.lineId}#STATE`,
|
|
517
|
+
* }),
|
|
518
|
+
* },
|
|
519
|
+
* },
|
|
520
|
+
* })
|
|
521
|
+
*
|
|
522
|
+
* const lineEvents = yield* OrderLines.readIndex("byOrder", { orderId, lineId }, {
|
|
523
|
+
* partition: `ORDER#${orderId}`,
|
|
524
|
+
* })
|
|
525
|
+
* ```
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```typescript
|
|
449
529
|
* const Entries = EventStore.makeStream({
|
|
450
530
|
* table: EventsTable,
|
|
451
531
|
* streamName: "Ledger",
|
|
@@ -470,10 +550,12 @@ const eventTimestampsProblem = (timestamps, count, label, events) => {
|
|
|
470
550
|
* @throws `[EDD-9062]` when `snapshot.mode` is neither `"after-append"` nor
|
|
471
551
|
* `"inline"`.
|
|
472
552
|
* @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
|
-
*
|
|
553
|
+
* `"gsi"`, a `gsi` without `pk`, an `lsi` with `pk`, `partition` or
|
|
554
|
+
* `snapshot`, an empty `index` / `sk` / `pk`, a `key` / `partition` /
|
|
555
|
+
* `snapshot` that is not a function, a `snapshot` without `partition` or on
|
|
556
|
+
* a stream without a `snapshot` config, a `projection` other than `"all"` /
|
|
557
|
+
* `"withoutMetadata"` / `"keys"`, or a member an index entry does not
|
|
558
|
+
* declare (such as a misspelled `projection`, also a type error).
|
|
477
559
|
* @throws `[EDD-9064]` when an index attribute is one the stream writes itself
|
|
478
560
|
* (`pk`, `sk`, `__edd_e__`, `streamId`, `version`, `eventType`, `data`,
|
|
479
561
|
* `metadata`, `timestamp`, `asOfVersion`, `state`, `commandId`, or the
|
|
@@ -578,7 +660,7 @@ export const makeStream = (config) => {
|
|
|
578
660
|
},
|
|
579
661
|
];
|
|
580
662
|
// Stream indexes (#140) — validated at definition time (EDD-9063..9065).
|
|
581
|
-
const streamIndexes = resolveStreamIndexes(config.streamName, config.indexes);
|
|
663
|
+
const streamIndexes = resolveStreamIndexes(config.streamName, config.indexes, snapshot !== undefined);
|
|
582
664
|
const indexSettings = {};
|
|
583
665
|
for (const idx of streamIndexes) {
|
|
584
666
|
indexSettings[idx.name] = {
|
|
@@ -587,8 +669,12 @@ export const makeStream = (config) => {
|
|
|
587
669
|
pk: idx.pk,
|
|
588
670
|
sk: idx.sk,
|
|
589
671
|
projection: idx.projection,
|
|
672
|
+
partition: idx.partition,
|
|
673
|
+
snapshot: idx.snapshot,
|
|
590
674
|
};
|
|
591
675
|
}
|
|
676
|
+
/** The indexes that hold the stream's snapshot (#149). */
|
|
677
|
+
const snapshotIndexes = streamIndexes.filter((idx) => idx.snapshotEntry !== undefined);
|
|
592
678
|
/** Every attribute the indexes write on an event item. */
|
|
593
679
|
const indexAttributes = streamIndexes.flatMap((idx) => idx.type === "gsi" ? [idx.pk, idx.sk] : [idx.sk]);
|
|
594
680
|
// Build union schema from event schemas for decoding
|
|
@@ -719,34 +805,61 @@ export const makeStream = (config) => {
|
|
|
719
805
|
// append
|
|
720
806
|
// ---------------------------------------------------------------------------
|
|
721
807
|
/**
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* rules on a key — not empty, at
|
|
726
|
-
*
|
|
727
|
-
*
|
|
808
|
+
* One value an index function derives for an item (#140, #149): an event's
|
|
809
|
+
* `key` or `partition`, a string or `undefined` to leave the item out.
|
|
810
|
+
* Another type, or a function that throws, is a `ValidationError`, raised
|
|
811
|
+
* before anything is written. DynamoDB's own rules on a key — not empty, at
|
|
812
|
+
* most 1024 bytes for a sort key and 2048 for a partition key — are not
|
|
813
|
+
* pre-checked: DynamoDB rejects the write and the rejection is diagnosed,
|
|
814
|
+
* naming the index and the item (see `sendAppendTransaction`).
|
|
728
815
|
*/
|
|
729
|
-
const
|
|
730
|
-
try:
|
|
816
|
+
const deriveIndexValue = (idx, what, subject, derive) => Effect.try({
|
|
817
|
+
try: derive,
|
|
731
818
|
catch: (cause) => new ValidationError({ entityType, operation: "EventStore.append.indexes", cause }),
|
|
732
819
|
}).pipe(Effect.flatMap((derived) => {
|
|
733
820
|
if (derived === undefined)
|
|
734
821
|
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
822
|
// A string, or the event is not indexed: what the stream's own key
|
|
742
823
|
// contract means, which DynamoDB cannot see (a number would be sent
|
|
743
824
|
// as `N`). An empty string is DynamoDB's to reject, and is diagnosed
|
|
744
825
|
// from its rejection, naming the index and the event (#144).
|
|
745
826
|
if (typeof derived !== "string") {
|
|
746
|
-
return
|
|
827
|
+
return Effect.fail(new ValidationError({
|
|
828
|
+
entityType,
|
|
829
|
+
operation: "EventStore.append.indexes",
|
|
830
|
+
cause: `The ${what} of index "${idx.name}" for ${subject} must be a string or ` +
|
|
831
|
+
`undefined; received ${String(derived)}. Nothing was written.`,
|
|
832
|
+
}));
|
|
747
833
|
}
|
|
748
834
|
return Effect.succeed(derived);
|
|
749
835
|
}));
|
|
836
|
+
/**
|
|
837
|
+
* The snapshot's entry in a stream index (#149): its partition and key, or
|
|
838
|
+
* `undefined` to leave it out. Anything but an object of two strings, or an
|
|
839
|
+
* entry function that throws, is a `ValidationError` before anything is
|
|
840
|
+
* written; DynamoDB's rules on the values are its own, as for events.
|
|
841
|
+
*/
|
|
842
|
+
const deriveSnapshotEntry = (idx, entry, state, streamId, asOfVersion, operation) => Effect.try({
|
|
843
|
+
try: () => entry(state, asOfVersion, streamId),
|
|
844
|
+
catch: (cause) => new ValidationError({ entityType: snapshotEntityType, operation, cause }),
|
|
845
|
+
}).pipe(Effect.flatMap((derived) => {
|
|
846
|
+
if (derived === undefined)
|
|
847
|
+
return Effect.succeed(undefined);
|
|
848
|
+
const { partition, key } = typeof derived === "object" && derived !== null
|
|
849
|
+
? derived
|
|
850
|
+
: { partition: undefined, key: undefined };
|
|
851
|
+
if (typeof partition !== "string" || typeof key !== "string") {
|
|
852
|
+
return Effect.fail(new ValidationError({
|
|
853
|
+
entityType: snapshotEntityType,
|
|
854
|
+
operation,
|
|
855
|
+
cause: `The snapshot entry of index "${idx.name}" at version ${asOfVersion} must be ` +
|
|
856
|
+
"{ partition: string, key: string } or undefined; received " +
|
|
857
|
+
`${derived !== null && typeof derived === "object" ? JSON.stringify(derived) : String(derived)}. ` +
|
|
858
|
+
"Nothing was written.",
|
|
859
|
+
}));
|
|
860
|
+
}
|
|
861
|
+
return Effect.succeed({ partition, key });
|
|
862
|
+
}));
|
|
750
863
|
/**
|
|
751
864
|
* Encode snapshot state to wire form through the state schema, with the
|
|
752
865
|
* same `decode → encode` fallback as events and metadata. A structurally
|
|
@@ -852,48 +965,80 @@ export const makeStream = (config) => {
|
|
|
852
965
|
};
|
|
853
966
|
/**
|
|
854
967
|
* 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
|
-
*
|
|
968
|
+
* over-long (or empty) stream-index key OF AN EVENT OR THE INLINE SNAPSHOT
|
|
969
|
+
* — a value an index function returned — is named by its index and item,
|
|
970
|
+
* as the `indexes` option reports its other key problems (see
|
|
971
|
+
* {@link indexKeyError}). Only the stream's own items carry stream-index
|
|
972
|
+
* keys: an additional item writing the same physical attribute
|
|
973
|
+
* (single-table designs share `gsi1sk` and the like) is its own entity's
|
|
974
|
+
* key, and is named as any other item.
|
|
861
975
|
*/
|
|
862
976
|
const appendLimitError = (diagnosis, original, tx) => {
|
|
863
|
-
if (
|
|
864
|
-
diagnosis._tag === "EmptyKey") {
|
|
977
|
+
if (diagnosis._tag === "KeyTooLong" || diagnosis._tag === "EmptyKey") {
|
|
865
978
|
const position = tx.targets().indexOf(diagnosis.target);
|
|
866
979
|
const isEvent = position >= tx.guardCount && position < tx.guardCount + tx.eventCount;
|
|
867
|
-
const
|
|
868
|
-
|
|
980
|
+
const isSnapshot = position >= 0 && position === tx.snapshotIndex;
|
|
981
|
+
const named = isEvent || isSnapshot
|
|
982
|
+
? indexKeyError(diagnosis, original, isSnapshot, "the append", "EventStore.append.indexes")
|
|
869
983
|
: 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
|
-
}
|
|
984
|
+
if (named !== undefined)
|
|
985
|
+
return named;
|
|
884
986
|
}
|
|
885
987
|
return diagnosedValidationError(diagnosis, original, {
|
|
886
988
|
operation: "EventStore.append",
|
|
887
989
|
entityType,
|
|
888
990
|
});
|
|
889
991
|
};
|
|
992
|
+
/**
|
|
993
|
+
* An over-long or empty key value in a stream-index attribute of an event
|
|
994
|
+
* or snapshot item, named by its index and item, or `undefined` when the
|
|
995
|
+
* attribute is not one the item's indexes derive. An event carries each
|
|
996
|
+
* index's `key` (and a derived `partition`); a snapshot, each snapshot
|
|
997
|
+
* index's entry. A stream-scoped GSI's partition is the stream's own key,
|
|
998
|
+
* which no function returned: it is named as any key.
|
|
999
|
+
*/
|
|
1000
|
+
const indexKeyError = (diagnosis, original, isSnapshot, request, operation) => {
|
|
1001
|
+
const candidates = isSnapshot ? snapshotIndexes : streamIndexes;
|
|
1002
|
+
const kind = diagnosis._tag === "KeyTooLong"
|
|
1003
|
+
? diagnosis.kind
|
|
1004
|
+
: candidates.some((idx) => idx.sk === diagnosis.attribute)
|
|
1005
|
+
? "sort"
|
|
1006
|
+
: "partition";
|
|
1007
|
+
const idx = candidates.find((candidate) => kind === "sort"
|
|
1008
|
+
? candidate.sk === diagnosis.attribute
|
|
1009
|
+
: candidate.partition === "derived" && candidate.pk === diagnosis.attribute);
|
|
1010
|
+
if (idx === undefined)
|
|
1011
|
+
return undefined;
|
|
1012
|
+
const value = isSnapshot
|
|
1013
|
+
? `The snapshot entry's ${kind === "sort" ? "key" : "partition"}`
|
|
1014
|
+
: `The ${kind === "sort" ? "key" : "partition"}`;
|
|
1015
|
+
const leaveOut = isSnapshot
|
|
1016
|
+
? "the snapshot function must return non-empty strings, or undefined to leave the " +
|
|
1017
|
+
"snapshot out of the index."
|
|
1018
|
+
: `the ${kind === "sort" ? "key" : "partition"} function must return a non-empty ` +
|
|
1019
|
+
"string, or undefined to leave the event out of the index.";
|
|
1020
|
+
return new ValidationError({
|
|
1021
|
+
entityType: isSnapshot ? snapshotEntityType : entityType,
|
|
1022
|
+
operation,
|
|
1023
|
+
cause: withCause(`${value} of index "${idx.name}" for ${diagnosis.target.source} ` +
|
|
1024
|
+
(diagnosis._tag === "KeyTooLong"
|
|
1025
|
+
? `is ${diagnosis.bytes} bytes, longer than DynamoDB's ` +
|
|
1026
|
+
`${kind === "sort" ? "1024-byte limit for a sort" : "2048-byte limit for a partition"} key.`
|
|
1027
|
+
: `is an empty string, which DynamoDB does not accept as a key value; ${leaveOut}`) +
|
|
1028
|
+
` DynamoDB rejected ${request}. Nothing was written.`, original),
|
|
1029
|
+
});
|
|
1030
|
+
};
|
|
890
1031
|
/**
|
|
891
1032
|
* The snapshot item for `state` at `asOfVersion`: the state encoded through
|
|
892
1033
|
* the state schema, then packed per `snapshot.storedAs` (#147) — so every
|
|
893
|
-
* size measured afterwards is
|
|
894
|
-
*
|
|
1034
|
+
* size measured afterwards is stored bytes — with its entry in each index
|
|
1035
|
+
* that holds snapshots (#149). Shared by `writeSnapshot`, the inline
|
|
1036
|
+
* snapshot of `append`, and `measureSnapshot` / `measureAppend`.
|
|
1037
|
+
*
|
|
1038
|
+
* `eventPartitions` — the partition the append's events have in each
|
|
1039
|
+
* index, by index name — is what the inline snapshot's entry must match.
|
|
895
1040
|
*/
|
|
896
|
-
const buildSnapshotItem = (
|
|
1041
|
+
const buildSnapshotItem = (streamId, state, asOfVersion, now, operation, eventPartitions) => Effect.gen(function* () {
|
|
897
1042
|
const encoded = yield* encodeSnapshotState(state, operation);
|
|
898
1043
|
const stored = stateStoredAs === undefined
|
|
899
1044
|
? encoded
|
|
@@ -902,15 +1047,36 @@ export const makeStream = (config) => {
|
|
|
902
1047
|
attribute: "state",
|
|
903
1048
|
operation: `${operation}.packed`,
|
|
904
1049
|
});
|
|
905
|
-
|
|
906
|
-
pk,
|
|
1050
|
+
const item = {
|
|
1051
|
+
pk: composeStreamPk(streamId),
|
|
907
1052
|
sk: snapshotSk,
|
|
908
1053
|
__edd_e__: snapshotEntityType,
|
|
909
|
-
streamId:
|
|
1054
|
+
streamId: composeStreamIdString(streamId),
|
|
910
1055
|
asOfVersion,
|
|
911
1056
|
state: stored,
|
|
912
1057
|
timestamp: now,
|
|
913
|
-
}
|
|
1058
|
+
};
|
|
1059
|
+
// The snapshot's index entries (#149), stored raw on the item itself,
|
|
1060
|
+
// so they are written by the same `Put` and count towards its size.
|
|
1061
|
+
for (const idx of snapshotIndexes) {
|
|
1062
|
+
const entry = yield* deriveSnapshotEntry(idx, idx.snapshotEntry, state, streamId, asOfVersion, operation);
|
|
1063
|
+
if (entry === undefined)
|
|
1064
|
+
continue;
|
|
1065
|
+
const events = eventPartitions?.get(idx.name);
|
|
1066
|
+
if (events !== undefined && events.partition !== entry.partition) {
|
|
1067
|
+
return yield* new ValidationError({
|
|
1068
|
+
entityType: snapshotEntityType,
|
|
1069
|
+
operation,
|
|
1070
|
+
cause: `The inline snapshot's partition in index "${idx.name}" is ` +
|
|
1071
|
+
`${JSON.stringify(entry.partition)}, but ${events.subject} has ` +
|
|
1072
|
+
`${JSON.stringify(events.partition)}: an append's events and its inline snapshot ` +
|
|
1073
|
+
"must be in the same partition of an index. Nothing was written.",
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
item[idx.pk] = entry.partition;
|
|
1077
|
+
item[idx.sk] = entry.key;
|
|
1078
|
+
}
|
|
1079
|
+
return toAttributeMap(item);
|
|
914
1080
|
});
|
|
915
1081
|
/**
|
|
916
1082
|
* The stream's own items of an append — everything but the caller's
|
|
@@ -1021,6 +1187,10 @@ export const makeStream = (config) => {
|
|
|
1021
1187
|
else if (options?.metadata !== undefined) {
|
|
1022
1188
|
encodedMetadata = options.metadata;
|
|
1023
1189
|
}
|
|
1190
|
+
// The partition the events have in each index that derives one (#149),
|
|
1191
|
+
// by index name, with the first event that has it: the events and the
|
|
1192
|
+
// inline snapshot of one append must agree.
|
|
1193
|
+
const eventPartitions = new Map();
|
|
1024
1194
|
// Build the event puts — one Put per event, each with attribute_not_exists(pk).
|
|
1025
1195
|
// Events are encoded to wire form through their schema (codec symmetry
|
|
1026
1196
|
// with the read path, which decodes through the same schema), so this is
|
|
@@ -1079,15 +1249,39 @@ export const makeStream = (config) => {
|
|
|
1079
1249
|
item.metadata = itemMetadata;
|
|
1080
1250
|
}
|
|
1081
1251
|
// Stream indexes (#140): the derived key, stored raw, and for a
|
|
1082
|
-
// GSI the stream's partition key
|
|
1083
|
-
//
|
|
1252
|
+
// GSI the stream's partition key — or the derived partition, raw
|
|
1253
|
+
// too (#149). The attributes are part of the event item itself,
|
|
1254
|
+
// so they count towards the size checks. Both functions run for
|
|
1255
|
+
// every event, so each one's contract is checked on every event.
|
|
1256
|
+
const subject = `the event at version ${version}`;
|
|
1084
1257
|
for (const idx of streamIndexes) {
|
|
1085
|
-
const derived = yield*
|
|
1086
|
-
|
|
1258
|
+
const derived = yield* deriveIndexValue(idx, "key", subject, () => idx.key(event, version, streamId));
|
|
1259
|
+
let partition = pk;
|
|
1260
|
+
if (idx.derivePartition !== undefined) {
|
|
1261
|
+
const derivePartition = idx.derivePartition;
|
|
1262
|
+
partition = yield* deriveIndexValue(idx, "partition", subject, () => derivePartition(event, version, streamId));
|
|
1263
|
+
if (partition !== undefined) {
|
|
1264
|
+
const first = eventPartitions.get(idx.name);
|
|
1265
|
+
if (first === undefined) {
|
|
1266
|
+
eventPartitions.set(idx.name, { partition, subject });
|
|
1267
|
+
}
|
|
1268
|
+
else if (first.partition !== partition) {
|
|
1269
|
+
return yield* new ValidationError({
|
|
1270
|
+
entityType,
|
|
1271
|
+
operation: "EventStore.append.indexes",
|
|
1272
|
+
cause: `The partition of index "${idx.name}" for ${subject} is ` +
|
|
1273
|
+
`${JSON.stringify(partition)}, but ${first.subject} has ` +
|
|
1274
|
+
`${JSON.stringify(first.partition)}: every event of one append must be ` +
|
|
1275
|
+
"in the same partition of an index. Nothing was written.",
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (derived === undefined || partition === undefined)
|
|
1087
1281
|
continue;
|
|
1088
1282
|
item[idx.sk] = derived;
|
|
1089
1283
|
if (idx.type === "gsi")
|
|
1090
|
-
item[idx.pk] =
|
|
1284
|
+
item[idx.pk] = partition;
|
|
1091
1285
|
}
|
|
1092
1286
|
return {
|
|
1093
1287
|
Put: {
|
|
@@ -1136,7 +1330,7 @@ export const makeStream = (config) => {
|
|
|
1136
1330
|
snapshotPut = {
|
|
1137
1331
|
Put: {
|
|
1138
1332
|
TableName: tableName,
|
|
1139
|
-
Item: yield* buildSnapshotItem(
|
|
1333
|
+
Item: yield* buildSnapshotItem(streamId, snapshotState, expectedVersion + events.length, now, "EventStore.append.snapshot", eventPartitions),
|
|
1140
1334
|
},
|
|
1141
1335
|
};
|
|
1142
1336
|
}
|
|
@@ -1215,6 +1409,7 @@ export const makeStream = (config) => {
|
|
|
1215
1409
|
if (sentinelPut !== undefined) {
|
|
1216
1410
|
pushStreamItem(sentinelPut, entityType, "the idempotency sentinel");
|
|
1217
1411
|
}
|
|
1412
|
+
const snapshotIndex = snapshotPut === undefined ? -1 : items.length;
|
|
1218
1413
|
if (snapshotPut !== undefined) {
|
|
1219
1414
|
pushStreamItem(snapshotPut, snapshotEntityType, "the inline snapshot");
|
|
1220
1415
|
}
|
|
@@ -1227,6 +1422,7 @@ export const makeStream = (config) => {
|
|
|
1227
1422
|
eventCount: eventItems.length,
|
|
1228
1423
|
additional,
|
|
1229
1424
|
sentinelIndex,
|
|
1425
|
+
snapshotIndex,
|
|
1230
1426
|
expectedVersion,
|
|
1231
1427
|
};
|
|
1232
1428
|
};
|
|
@@ -1289,7 +1485,7 @@ export const makeStream = (config) => {
|
|
|
1289
1485
|
if (snapshot === undefined)
|
|
1290
1486
|
return yield* snapshotUnavailable("measureSnapshot");
|
|
1291
1487
|
const now = DateTime.formatIso(yield* DateTime.now);
|
|
1292
|
-
const item = yield* buildSnapshotItem(
|
|
1488
|
+
const item = yield* buildSnapshotItem(streamId, state, asOfVersion, now, "EventStore.measureSnapshot");
|
|
1293
1489
|
return itemBytes(item);
|
|
1294
1490
|
});
|
|
1295
1491
|
// ---------------------------------------------------------------------------
|
|
@@ -1379,7 +1575,7 @@ export const makeStream = (config) => {
|
|
|
1379
1575
|
const now = DateTime.formatIso(yield* DateTime.now);
|
|
1380
1576
|
// Packed per `snapshot.storedAs` (#147): the diagnosis below measures
|
|
1381
1577
|
// the stored bytes.
|
|
1382
|
-
const item = yield* buildSnapshotItem(
|
|
1578
|
+
const item = yield* buildSnapshotItem(streamId, state, asOfVersion, now, "EventStore.writeSnapshot");
|
|
1383
1579
|
const put = {
|
|
1384
1580
|
TableName: tableName,
|
|
1385
1581
|
Item: item,
|
|
@@ -1397,11 +1593,19 @@ export const makeStream = (config) => {
|
|
|
1397
1593
|
item: { Put: put },
|
|
1398
1594
|
target: () => transactItemTarget({ Put: put }, tableName, ["pk", "sk"], snapshotEntityType, "the snapshot"),
|
|
1399
1595
|
}),
|
|
1400
|
-
keys: () => ({
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
entityType: snapshotEntityType,
|
|
1596
|
+
keys: () => ({
|
|
1597
|
+
partition: ["pk", ...snapshotIndexes.map((idx) => idx.pk)],
|
|
1598
|
+
sort: ["sk", ...snapshotIndexes.map((idx) => idx.sk)],
|
|
1404
1599
|
}),
|
|
1600
|
+
// A snapshot index entry's empty or over-long value (#149) is named
|
|
1601
|
+
// by its index, as an inline snapshot's is.
|
|
1602
|
+
rewrite: (diagnosis, original) => (diagnosis._tag === "KeyTooLong" || diagnosis._tag === "EmptyKey"
|
|
1603
|
+
? indexKeyError(diagnosis, original, true, "the snapshot", "EventStore.writeSnapshot")
|
|
1604
|
+
: undefined) ??
|
|
1605
|
+
diagnosedValidationError(diagnosis, original, {
|
|
1606
|
+
operation: "EventStore.writeSnapshot",
|
|
1607
|
+
entityType: snapshotEntityType,
|
|
1608
|
+
}),
|
|
1405
1609
|
}));
|
|
1406
1610
|
});
|
|
1407
1611
|
/**
|
|
@@ -1591,15 +1795,44 @@ export const makeStream = (config) => {
|
|
|
1591
1795
|
// Stream indexes — query.index / readIndex (#140)
|
|
1592
1796
|
// ---------------------------------------------------------------------------
|
|
1593
1797
|
/**
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1597
|
-
*
|
|
1798
|
+
* @internal Why `partition` cannot be passed for index `name`, or
|
|
1799
|
+
* `undefined` when it can: required (a string) for an index that derives
|
|
1800
|
+
* its partition (#149), refused for any other. Unreachable through the
|
|
1801
|
+
* types. An unknown name is left to {@link buildIndexQuery}.
|
|
1802
|
+
*/
|
|
1803
|
+
const invalidIndexPartition = (name, partition) => {
|
|
1804
|
+
const idx = streamIndexes.find((candidate) => candidate.name === name);
|
|
1805
|
+
if (idx === undefined)
|
|
1806
|
+
return undefined;
|
|
1807
|
+
if (idx.partition === "derived") {
|
|
1808
|
+
return typeof partition === "string"
|
|
1809
|
+
? undefined
|
|
1810
|
+
: `Index "${name}" derives its partition: pass the partition to read as ` +
|
|
1811
|
+
`options.partition (a string); received ${String(partition)}.`;
|
|
1812
|
+
}
|
|
1813
|
+
return partition === undefined
|
|
1814
|
+
? undefined
|
|
1815
|
+
: `Index "${name}" is scoped to the stream; it takes no partition.`;
|
|
1816
|
+
};
|
|
1817
|
+
/**
|
|
1818
|
+
* A query of one stream index. A stream-scoped index is queried in the
|
|
1819
|
+
* stream's partition: the LSI shares the table's `pk`; a GSI's `pk`
|
|
1820
|
+
* attribute carries the same value. No sort-key condition — the index holds
|
|
1821
|
+
* only this stream's indexed events under that key; `Query.where` narrows
|
|
1822
|
+
* the derived-key range.
|
|
1823
|
+
*
|
|
1824
|
+
* An index that derives its partition (#149) is queried in `partition`,
|
|
1825
|
+
* which other streams (and this one's other stream ids, and snapshots) may
|
|
1826
|
+
* share: the `__edd_e__` filter keeps this stream's events, and a filter on
|
|
1827
|
+
* the table's `pk` — projected by every index — keeps `streamId`'s.
|
|
1598
1828
|
*
|
|
1599
1829
|
* @throws `[EDD-9066]` for a name the stream does not declare (unreachable
|
|
1600
1830
|
* through the types).
|
|
1831
|
+
* @throws `[EDD-9080]` for a missing `partition` on an index that derives
|
|
1832
|
+
* it, or one passed to an index that does not (unreachable through the
|
|
1833
|
+
* types).
|
|
1601
1834
|
*/
|
|
1602
|
-
const buildIndexQuery = (name, streamId) => {
|
|
1835
|
+
const buildIndexQuery = (name, streamId, partition) => {
|
|
1603
1836
|
const idx = streamIndexes.find((candidate) => candidate.name === name);
|
|
1604
1837
|
if (idx === undefined) {
|
|
1605
1838
|
throw new Error(`[EDD-9066] EventStream "${config.streamName}": unknown index "${String(name)}". ` +
|
|
@@ -1607,11 +1840,16 @@ export const makeStream = (config) => {
|
|
|
1607
1840
|
? "The stream declares no indexes."
|
|
1608
1841
|
: `Declared: ${streamIndexes.map((candidate) => `"${candidate.name}"`).join(", ")}.`));
|
|
1609
1842
|
}
|
|
1610
|
-
|
|
1843
|
+
const partitionProblem = invalidIndexPartition(name, partition);
|
|
1844
|
+
if (partitionProblem !== undefined) {
|
|
1845
|
+
throw new Error(`[EDD-9080] EventStream "${config.streamName}": ${partitionProblem}`);
|
|
1846
|
+
}
|
|
1847
|
+
const streamPk = composeStreamPk(streamId);
|
|
1848
|
+
const query = Query.make({
|
|
1611
1849
|
tableName: "",
|
|
1612
1850
|
indexName: idx.index,
|
|
1613
1851
|
pkField: idx.pk,
|
|
1614
|
-
pkValue:
|
|
1852
|
+
pkValue: idx.partition === "derived" ? partition : streamPk,
|
|
1615
1853
|
skField: idx.sk,
|
|
1616
1854
|
entityTypes: [entityType],
|
|
1617
1855
|
decoder: indexDecoder(idx),
|
|
@@ -1639,6 +1877,7 @@ export const makeStream = (config) => {
|
|
|
1639
1877
|
}
|
|
1640
1878
|
: {}),
|
|
1641
1879
|
});
|
|
1880
|
+
return idx.partition === "derived" ? Query.filterExpr(query, streamPkFilter(streamPk)) : query;
|
|
1642
1881
|
};
|
|
1643
1882
|
/**
|
|
1644
1883
|
* @internal The decoder of events read through `idx`.
|
|
@@ -1685,7 +1924,11 @@ export const makeStream = (config) => {
|
|
|
1685
1924
|
if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
|
|
1686
1925
|
return yield* refuse(`limit must be a positive integer; received ${String(limit)}. Nothing was sent.`);
|
|
1687
1926
|
}
|
|
1688
|
-
|
|
1927
|
+
const partitionProblem = invalidIndexPartition(name, options?.partition);
|
|
1928
|
+
if (partitionProblem !== undefined) {
|
|
1929
|
+
return yield* refuse(`${partitionProblem} Nothing was sent.`);
|
|
1930
|
+
}
|
|
1931
|
+
let query = buildIndexQuery(name, streamId, options?.partition);
|
|
1689
1932
|
if (options?.beginsWith !== undefined) {
|
|
1690
1933
|
query = Query.where(query, { beginsWith: options.beginsWith });
|
|
1691
1934
|
}
|
|
@@ -1702,7 +1945,7 @@ export const makeStream = (config) => {
|
|
|
1702
1945
|
});
|
|
1703
1946
|
const queryNamespace = {
|
|
1704
1947
|
events: (streamId) => buildEventsQuery(streamId),
|
|
1705
|
-
index: (name, streamId) => buildIndexQuery(name, streamId),
|
|
1948
|
+
index: (name, streamId, options) => buildIndexQuery(name, streamId, options?.partition),
|
|
1706
1949
|
};
|
|
1707
1950
|
// ---------------------------------------------------------------------------
|
|
1708
1951
|
// Return EventStream
|
|
@@ -1785,7 +2028,7 @@ export const bind = (stream) => Effect.gen(function* () {
|
|
|
1785
2028
|
readFrom: (streamId, afterVersion, options) => provide(stream.readFrom(streamId, afterVersion, options)),
|
|
1786
2029
|
currentVersion: (streamId, options) => provide(stream.currentVersion(streamId, options)),
|
|
1787
2030
|
readLatest: (streamId, options) => provide(stream.readLatest(streamId, options)),
|
|
1788
|
-
readIndex: (name, streamId, options) => provide(stream.readIndex(name, streamId, options)),
|
|
2031
|
+
readIndex: (name, streamId, ...options) => provide(stream.readIndex(name, streamId, ...options)),
|
|
1789
2032
|
// The captured context carries a `Compression` registry provided where
|
|
1790
2033
|
// the stream was bound (#147).
|
|
1791
2034
|
measureAppend: (streamId, events, expectedVersion, options) => provide(stream.measureAppend(streamId, events, expectedVersion, options)),
|
|
@@ -1810,12 +2053,22 @@ export const bind = (stream) => Effect.gen(function* () {
|
|
|
1810
2053
|
* Neither list names a key attribute: table and index keys are always
|
|
1811
2054
|
* projected.
|
|
1812
2055
|
*/
|
|
1813
|
-
const projectionDefinition = (projection) => projection === "all"
|
|
2056
|
+
const projectionDefinition = (projection, snapshot) => projection === "all"
|
|
1814
2057
|
? { ProjectionType: "ALL" }
|
|
1815
2058
|
: {
|
|
1816
2059
|
ProjectionType: "INCLUDE",
|
|
1817
|
-
NonKeyAttributes: projection === "keys"
|
|
2060
|
+
NonKeyAttributes: projection === "keys"
|
|
2061
|
+
? ["__edd_e__"]
|
|
2062
|
+
: snapshot
|
|
2063
|
+
? [...EVENT_ITEM_ATTRIBUTES, ...SNAPSHOT_ONLY_ATTRIBUTES].sort()
|
|
2064
|
+
: [...EVENT_ITEM_ATTRIBUTES],
|
|
1818
2065
|
};
|
|
2066
|
+
/**
|
|
2067
|
+
* @internal The snapshot item's attributes an event item does not carry: what
|
|
2068
|
+
* a `"withoutMetadata"` index that holds snapshots (#149) projects besides
|
|
2069
|
+
* {@link EVENT_ITEM_ATTRIBUTES}.
|
|
2070
|
+
*/
|
|
2071
|
+
const SNAPSHOT_ONLY_ATTRIBUTES = ["asOfVersion", "state"];
|
|
1819
2072
|
/**
|
|
1820
2073
|
* Derive the `CreateTable` fragments for the stream indexes (#140) of the given
|
|
1821
2074
|
* streams (`EventStream` or `BoundEventStream`) — `AttributeDefinitions`,
|
|
@@ -1830,7 +2083,7 @@ const projectionDefinition = (projection) => projection === "all"
|
|
|
1830
2083
|
* | `projection` | `Projection` |
|
|
1831
2084
|
* |---|---|
|
|
1832
2085
|
* | `"all"` (default) | `{ ProjectionType: "ALL" }` |
|
|
1833
|
-
* | `"withoutMetadata"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__", "data", "eventType", "streamId", "timestamp", "version"] }` |
|
|
2086
|
+
* | `"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
2087
|
* | `"keys"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__"] }` — not `KEYS_ONLY`: every read filters on `__edd_e__` |
|
|
1835
2088
|
*
|
|
1836
2089
|
* The deployed index must project what is declared — copy `Projection` from
|
|
@@ -1841,7 +2094,12 @@ const projectionDefinition = (projection) => projection === "all"
|
|
|
1841
2094
|
* table over the limit).
|
|
1842
2095
|
*
|
|
1843
2096
|
* An index shared by several streams with the same definition is emitted
|
|
1844
|
-
* once
|
|
2097
|
+
* once — the way several streams write one GSI whose partition they derive
|
|
2098
|
+
* (`partition`, #149): an order's line, payment and shipment streams under
|
|
2099
|
+
* `ORDER#<orderId>`, say. Whether each declaration derives its partition or
|
|
2100
|
+
* indexes its snapshot is not part of the definition: the index's key schema
|
|
2101
|
+
* and projection kind are. Lists are sorted by index name, attributes by
|
|
2102
|
+
* attribute name.
|
|
1845
2103
|
*
|
|
1846
2104
|
* - An LSI can only be created with the table, and caps the item collection
|
|
1847
2105
|
* of every partition key value in the table at 10 GB — entity partitions
|
|
@@ -1869,25 +2127,32 @@ const projectionDefinition = (projection) => projection === "all"
|
|
|
1869
2127
|
*
|
|
1870
2128
|
* @throws `[EDD-9067]` when two streams define the same physical index
|
|
1871
2129
|
* differently (another type, other key attributes, or another projection).
|
|
2130
|
+
* Whether a declaration derives its partition or indexes its snapshot is not
|
|
2131
|
+
* part of the index's definition: streams that differ only there share it.
|
|
1872
2132
|
*/
|
|
1873
2133
|
export const indexDefinitions = (...streams) => {
|
|
2134
|
+
/** Per physical index: its definition, its first declarer, and whether any declarer indexes snapshots. */
|
|
1874
2135
|
const byIndex = new Map();
|
|
1875
2136
|
for (const stream of streams) {
|
|
1876
2137
|
for (const [name, raw] of Object.entries(stream.indexes)) {
|
|
1877
|
-
const settings = {
|
|
2138
|
+
const settings = {
|
|
2139
|
+
type: raw.type,
|
|
2140
|
+
index: raw.index,
|
|
2141
|
+
pk: raw.pk,
|
|
2142
|
+
sk: raw.sk,
|
|
2143
|
+
projection: raw.projection ?? "all",
|
|
2144
|
+
};
|
|
2145
|
+
const snapshot = raw.snapshot === true;
|
|
1878
2146
|
const owner = `index "${name}" of stream "${stream.streamName}"`;
|
|
1879
2147
|
const existing = byIndex.get(settings.index);
|
|
1880
2148
|
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
|
-
});
|
|
2149
|
+
byIndex.set(settings.index, { ...settings, owner, snapshot });
|
|
1889
2150
|
continue;
|
|
1890
2151
|
}
|
|
2152
|
+
// Several streams writing one GSI they derive the partition of (#149)
|
|
2153
|
+
// is the point: one declaration indexing snapshots and another not
|
|
2154
|
+
// still share it, and the projection covers the snapshots.
|
|
2155
|
+
existing.snapshot ||= snapshot;
|
|
1891
2156
|
if (existing.type !== settings.type ||
|
|
1892
2157
|
existing.pk !== settings.pk ||
|
|
1893
2158
|
existing.sk !== settings.sk ||
|
|
@@ -1910,7 +2175,7 @@ export const indexDefinitions = (...streams) => {
|
|
|
1910
2175
|
{ AttributeName: settings.pk, KeyType: "HASH" },
|
|
1911
2176
|
{ AttributeName: settings.sk, KeyType: "RANGE" },
|
|
1912
2177
|
],
|
|
1913
|
-
Projection: projectionDefinition(settings.projection),
|
|
2178
|
+
Projection: projectionDefinition(settings.projection, settings.snapshot),
|
|
1914
2179
|
};
|
|
1915
2180
|
attributes.add(settings.sk);
|
|
1916
2181
|
if (settings.type === "gsi") {
|