effect-dynamodb 1.24.0 → 1.26.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.
Files changed (106) hide show
  1. package/dist/Aggregate.d.ts +12 -2
  2. package/dist/Aggregate.d.ts.map +1 -1
  3. package/dist/Aggregate.js +284 -55
  4. package/dist/Aggregate.js.map +1 -1
  5. package/dist/Batch.d.ts +14 -5
  6. package/dist/Batch.d.ts.map +1 -1
  7. package/dist/Batch.js +184 -119
  8. package/dist/Batch.js.map +1 -1
  9. package/dist/Collection.d.ts +9 -1
  10. package/dist/Collection.d.ts.map +1 -1
  11. package/dist/Collection.js +5 -4
  12. package/dist/Collection.js.map +1 -1
  13. package/dist/DynamoClient.d.ts +80 -28
  14. package/dist/DynamoClient.d.ts.map +1 -1
  15. package/dist/DynamoClient.js +125 -25
  16. package/dist/DynamoClient.js.map +1 -1
  17. package/dist/Entity.d.ts +99 -40
  18. package/dist/Entity.d.ts.map +1 -1
  19. package/dist/Entity.js +693 -302
  20. package/dist/Entity.js.map +1 -1
  21. package/dist/EventStore.d.ts +598 -75
  22. package/dist/EventStore.d.ts.map +1 -1
  23. package/dist/EventStore.js +852 -221
  24. package/dist/EventStore.js.map +1 -1
  25. package/dist/Query.d.ts +100 -14
  26. package/dist/Query.d.ts.map +1 -1
  27. package/dist/Query.js +275 -71
  28. package/dist/Query.js.map +1 -1
  29. package/dist/StorageEncoding.d.ts +160 -0
  30. package/dist/StorageEncoding.d.ts.map +1 -0
  31. package/dist/StorageEncoding.js +258 -0
  32. package/dist/StorageEncoding.js.map +1 -0
  33. package/dist/Table.d.ts +56 -6
  34. package/dist/Table.d.ts.map +1 -1
  35. package/dist/Table.js +142 -60
  36. package/dist/Table.js.map +1 -1
  37. package/dist/Transaction.d.ts +8 -0
  38. package/dist/Transaction.d.ts.map +1 -1
  39. package/dist/Transaction.js +72 -22
  40. package/dist/Transaction.js.map +1 -1
  41. package/dist/VectorSearchEmulation.d.ts +3 -2
  42. package/dist/VectorSearchEmulation.d.ts.map +1 -1
  43. package/dist/VectorSearchEmulation.js +14 -2
  44. package/dist/VectorSearchEmulation.js.map +1 -1
  45. package/dist/compression/zlib.d.ts +75 -0
  46. package/dist/compression/zlib.d.ts.map +1 -0
  47. package/dist/compression/zlib.js +109 -0
  48. package/dist/compression/zlib.js.map +1 -0
  49. package/dist/index.d.ts +4 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +3 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/internal/BoundCrud.d.ts +39 -35
  54. package/dist/internal/BoundCrud.d.ts.map +1 -1
  55. package/dist/internal/BoundCrud.js.map +1 -1
  56. package/dist/internal/BoundQuery.d.ts +6 -5
  57. package/dist/internal/BoundQuery.d.ts.map +1 -1
  58. package/dist/internal/BoundQuery.js.map +1 -1
  59. package/dist/internal/BoundVectorQuery.d.ts +17 -1
  60. package/dist/internal/BoundVectorQuery.d.ts.map +1 -1
  61. package/dist/internal/BoundVectorQuery.js +31 -6
  62. package/dist/internal/BoundVectorQuery.js.map +1 -1
  63. package/dist/internal/EntityCombinators.d.ts +8 -5
  64. package/dist/internal/EntityCombinators.d.ts.map +1 -1
  65. package/dist/internal/EntityCombinators.js +24 -6
  66. package/dist/internal/EntityCombinators.js.map +1 -1
  67. package/dist/internal/EntityOps.d.ts +1 -1
  68. package/dist/internal/Expr.d.ts +9 -3
  69. package/dist/internal/Expr.d.ts.map +1 -1
  70. package/dist/internal/Expr.js +7 -5
  71. package/dist/internal/Expr.js.map +1 -1
  72. package/dist/internal/ExpressionLimits.d.ts +45 -0
  73. package/dist/internal/ExpressionLimits.d.ts.map +1 -0
  74. package/dist/internal/ExpressionLimits.js +89 -0
  75. package/dist/internal/ExpressionLimits.js.map +1 -0
  76. package/dist/internal/Hydrate.d.ts +62 -0
  77. package/dist/internal/Hydrate.d.ts.map +1 -0
  78. package/dist/internal/Hydrate.js +191 -0
  79. package/dist/internal/Hydrate.js.map +1 -0
  80. package/dist/internal/ItemSize.d.ts +60 -24
  81. package/dist/internal/ItemSize.d.ts.map +1 -1
  82. package/dist/internal/ItemSize.js +122 -39
  83. package/dist/internal/ItemSize.js.map +1 -1
  84. package/dist/internal/Packed.d.ts +289 -0
  85. package/dist/internal/Packed.d.ts.map +1 -0
  86. package/dist/internal/Packed.js +654 -0
  87. package/dist/internal/Packed.js.map +1 -0
  88. package/dist/internal/PathBuilder.d.ts +45 -2
  89. package/dist/internal/PathBuilder.d.ts.map +1 -1
  90. package/dist/internal/PathBuilder.js +4 -0
  91. package/dist/internal/PathBuilder.js.map +1 -1
  92. package/dist/internal/TransactWriteOps.d.ts +22 -46
  93. package/dist/internal/TransactWriteOps.d.ts.map +1 -1
  94. package/dist/internal/TransactWriteOps.js +26 -95
  95. package/dist/internal/TransactWriteOps.js.map +1 -1
  96. package/dist/internal/TransactableOps.d.ts +1 -1
  97. package/dist/internal/TransactableOps.d.ts.map +1 -1
  98. package/dist/internal/TransactableOps.js +2 -0
  99. package/dist/internal/TransactableOps.js.map +1 -1
  100. package/dist/internal/WriteDiagnosis.d.ts +353 -0
  101. package/dist/internal/WriteDiagnosis.d.ts.map +1 -0
  102. package/dist/internal/WriteDiagnosis.js +1028 -0
  103. package/dist/internal/WriteDiagnosis.js.map +1 -0
  104. package/dist/internal/sparse_check.js +43 -79
  105. package/dist/internal/sparse_check.js.map +1 -1
  106. package/package.json +6 -2
@@ -0,0 +1,1028 @@
1
+ /**
2
+ * @internal Failure-time diagnosis of DynamoDB's rejections (#144).
3
+ *
4
+ * The library does not pre-check limits or request rules DynamoDB enforces
5
+ * itself — the 400 KB item, the 4 MB and 100-item transaction, the 2048-byte
6
+ * partition key and 1024-byte sort key, one operation per item, the limits on
7
+ * one expression, an `IN ()` with no operands, an empty key value, a reversed
8
+ * `BETWEEN`, a consistent read of a GSI, a table's vector index limits, a
9
+ * vector search's `TopK`. It sends the request; when DynamoDB rejects it, this
10
+ * module:
11
+ *
12
+ * 1. **recognises** the rejection by a stable fragment of its message (a
13
+ * request-wide `ValidationException` or `LimitExceededException`, or a
14
+ * `TransactionCanceledException` whose positional reason carries
15
+ * `Code: "ValidationError"`);
16
+ * 2. **confirms** it against the request the site sent — an item count, a
17
+ * key's UTF-8 bytes, an expression's length or operators, two entries
18
+ * naming one item, an empty key value, the order of two bounds (each
19
+ * confirmed only when the request breaks the rule), or, for item and
20
+ * transaction sizes, by checking the request holds something the message
21
+ * can apply to and measuring it exactly (`ItemSize.ts`);
22
+ * 3. **attributes** it to the culprit, using the targets the site built.
23
+ *
24
+ * A site turns the {@link Diagnosis} into its own tagged error with
25
+ * DynamoDB's error kept as the cause ({@link withCause}); anything
26
+ * unrecognised or unconfirmed passes through unchanged. Nothing is measured,
27
+ * and no target is built, unless DynamoDB has rejected the request.
28
+ *
29
+ * Every site goes through {@link diagnoseFailure}: writes ({@link SentWrite}),
30
+ * reads ({@link SentRead}), table management ({@link SentTable}) and vector
31
+ * searches ({@link SentSearch}) share the one recognise / confirm / rewrite
32
+ * path.
33
+ */
34
+ import { isAwsTransactionCancelled, isAwsValidationError, TRANSACT_WRITE_ITEMS_LIMIT, ValidationError, } from "@effect-dynamodb/schema/Errors.js";
35
+ import { MAX_DIMENSIONS, MAX_INLINE_FILTERS, MAX_TOP_K, MAX_VECTOR_INDEXES_PER_TABLE, MIN_DIMENSIONS, MIN_TOP_K, } from "@effect-dynamodb/schema/VectorIndex.js";
36
+ import { Effect } from "effect";
37
+ import { countOperators, EXPRESSION_MAX_LENGTH, EXPRESSION_MAX_OPERATORS, expressionCost, IN_MAX_OPERANDS, largestInOperands, } from "./ExpressionLimits.js";
38
+ import { attributeBytes, ITEM_MAX_BYTES, itemBytes, TRANSACT_WRITE_MAX_BYTES, transactItemBytes, } from "./ItemSize.js";
39
+ /** DynamoDB's limit on a partition (hash) key value, in UTF-8 bytes. */
40
+ export const PARTITION_KEY_MAX_BYTES = 2048;
41
+ /** DynamoDB's limit on a sort (range) key value, in UTF-8 bytes. */
42
+ export const SORT_KEY_MAX_BYTES = 1024;
43
+ const NO_KEYS = { partition: [], sort: [] };
44
+ /**
45
+ * The target of a transact entry in `tableName`, keyed by `keyFields` (the
46
+ * table's primary key attributes): read from a Put's item, or any other
47
+ * entry's `Key` (a `TransactGetItems` entry's included).
48
+ */
49
+ export const transactItemTarget = (item, tableName, keyFields, entityType, source) => {
50
+ const attributes = "Get" in item && item.Get !== undefined
51
+ ? item.Get.Key
52
+ : (() => {
53
+ const write = item;
54
+ return (write.Put?.Item ?? write.Delete?.Key ?? write.Update?.Key ?? write.ConditionCheck?.Key);
55
+ })();
56
+ const parts = keyFields.map((field) => {
57
+ const value = attributes?.[field];
58
+ return { field, value, text: value?.S ?? value?.N ?? JSON.stringify(value ?? null) };
59
+ });
60
+ return {
61
+ identity: [
62
+ tableName,
63
+ ...parts.map((p) => `${p.field}=${JSON.stringify(p.value ?? null)}`),
64
+ ].join("\u0000"),
65
+ entityType,
66
+ source,
67
+ key: parts.map((p) => `${p.field}=${p.text}`).join(", "),
68
+ };
69
+ };
70
+ // ---------------------------------------------------------------------------
71
+ // Recognise
72
+ // ---------------------------------------------------------------------------
73
+ const ITEM_SIZE = /Item size has exceeded the maximum allowed size/i;
74
+ const ITEM_SIZE_UPDATE = /Item size to update has exceeded/i;
75
+ const ITEM_COUNT = /length less than or equal to 100\b/i;
76
+ const PAYLOAD = /payload size cannot exceed 4\s?MB/i;
77
+ const PAYLOAD_SIZE = /Payload Size:\s*(\d+)/i;
78
+ // A key value over its limit. DynamoDB Local words a table key's (partition
79
+ // and sort key in one message: "Hash primary key values must be under 2048
80
+ // bytes, and range primary key values must be under 1024 bytes") and an index
81
+ // key's ("One or more parameter values were invalid: Size limit exceeded")
82
+ // alike for both kinds, so either is confirmed against both. AWS words each
83
+ // kind its own way: "Aggregated size of all range keys has exceeded the size
84
+ // limit of 1024 bytes" (documented) and "Size of hashkey has exceeded the
85
+ // maximum size limit of 2048 bytes" (as AWS reports it; unverified here — Local
86
+ // cannot produce it).
87
+ const KEY_SIZE_EITHER = /Size limit exceeded|Hash primary key values must be under 2048 bytes|range primary key values must be under 1024 bytes/i;
88
+ const KEY_SIZE_SORT = /Aggregated size of all range keys/i;
89
+ const KEY_SIZE_PARTITION = /Size of hash\s?key has exceeded/i;
90
+ const KEY_SIZE = new RegExp([KEY_SIZE_EITHER, KEY_SIZE_SORT, KEY_SIZE_PARTITION].map((r) => r.source).join("|"), "i");
91
+ // A transaction (or `TransactGetItems`) naming one item twice; a batch
92
+ // request doing so.
93
+ const REPEATED = /cannot include multiple operations on one item/i;
94
+ const REPEATED_BATCH = /Provided list of item keys contains duplicates/i;
95
+ const EXPRESSION_KIND = /Invalid (\w+)Expression:/i;
96
+ const EXPRESSION_LENGTH = /Expression size has exceeded the maximum allowed size/i;
97
+ const EXPRESSION_OPERATORS = /too many operators/i;
98
+ const EXPRESSION_IN = /IN operator is provided with too many operands/i;
99
+ const EMPTY_KEY = /cannot contain an empty string value/i;
100
+ const EMPTY_KEY_INDEX = /IndexName:\s*([^,\s]+),\s*IndexKey:\s*([^,\s]+)/i;
101
+ const EMPTY_KEY_TABLE = /(?<!Index)Key:\s*([^,\s]+)/;
102
+ const BETWEEN_REVERSED = /BETWEEN operator requires upper bound to be greater than or equal to/i;
103
+ // DynamoDB Local 3.1.0 says "Consistent read cannot be true when querying a GSI";
104
+ // AWS and later Local releases say "Consistent reads are not supported on global
105
+ // secondary indexes".
106
+ const CONSISTENT_GSI = /Consistent read cannot be true when (querying|scanning) a GSI|Consistent reads are not supported on global secondary indexes/i;
107
+ // `IN ()` — an `isIn()` with no values — is a syntax error to DynamoDB
108
+ // (Local: 'Invalid ConditionExpression: Syntax error; token: ")", near: "()"',
109
+ // the `near` running on to the next token — `"())"`, `"() AND"` — when one
110
+ // follows; `FilterExpression` likewise), request-wide even in a transaction.
111
+ // The message names no attribute: confirmed by finding the `IN ()` in the
112
+ // request.
113
+ const EMPTY_IN = /Syntax error; token: "\)", near: "\(\)/i;
114
+ const EMPTY_IN_OPERAND = /((?:size\(\s*)?[#\w.[\]]+\)?)\s+IN\s*\(\s*\)/i;
115
+ // A vector index's dimensions or a vector search's TopK outside DynamoDB's
116
+ // range. DynamoDB Local has neither vector indexes nor `SearchVectors`, so
117
+ // these follow AWS's documented ranges (Dimensions 1..4096, TopK 1..100) and
118
+ // its standard member-constraint wording ("Value '5000' at
119
+ // 'vectorIndexes.1.member.dimensions' failed to satisfy constraint: Member
120
+ // must have value less than or equal to 4096"); unverified on AWS. Matched on
121
+ // the member's name only, and confirmed against the request.
122
+ const VECTOR_DIMENSIONS = /dimension/i;
123
+ const TOP_K = /top\s?k/i;
124
+ // AWS documents the vector-index quotas (18 inline filters per index, 5
125
+ // vector indexes per table, adjustable) but not the wording of their
126
+ // rejection, and DynamoDB Local has no vector indexes: matched loosely, and
127
+ // confirmed against the request.
128
+ const VECTOR = /vector/i;
129
+ const VECTOR_FILTERS = /inline.?filter/i;
130
+ const VECTOR_COUNT = /limit|exceed|maximum|at most|too many/i;
131
+ const STATED_LIMIT = /(?:limit|maximum|at most|exceeds?)\D{0,40}?(\d+)/i;
132
+ // Limits on vector indexes that are not their count per table: how many may
133
+ // be created at once ("Only 1 … index can be created or deleted
134
+ // simultaneously per table"), or a name's length.
135
+ const VECTOR_NOT_COUNT = /simultaneous|at a time|concurrent|in progress|length/i;
136
+ const messageOf = (cause) => {
137
+ if (cause instanceof Error)
138
+ return cause.message;
139
+ if (cause !== null && typeof cause === "object" && "message" in cause) {
140
+ return String(cause.message);
141
+ }
142
+ return "";
143
+ };
144
+ const isAwsLimitExceeded = (cause) => cause != null &&
145
+ typeof cause === "object" &&
146
+ "name" in cause &&
147
+ cause.name === "LimitExceededException";
148
+ /**
149
+ * The failure a site's error carries, if it is one this module can read: a
150
+ * DynamoDB `ValidationException` or `LimitExceededException` (however the
151
+ * client classified it), a `TransactionCanceledException`, or a site's
152
+ * `TransactionCancelled`.
153
+ */
154
+ const readFailure = (error) => {
155
+ if (error === null || typeof error !== "object" || !("_tag" in error))
156
+ return undefined;
157
+ const tagged = error;
158
+ if (tagged._tag === "TransactionCancelled") {
159
+ return {
160
+ _tag: "Cancelled",
161
+ reasons: error.reasons ??
162
+ [],
163
+ };
164
+ }
165
+ if (tagged._tag !== "DynamoError" && tagged._tag !== "DynamoValidationError")
166
+ return undefined;
167
+ const cause = tagged.cause;
168
+ if (isAwsValidationError(cause) || isAwsLimitExceeded(cause)) {
169
+ return { _tag: "Request", message: messageOf(cause) };
170
+ }
171
+ if (isAwsTransactionCancelled(cause)) {
172
+ return {
173
+ _tag: "Cancelled",
174
+ reasons: (cause.CancellationReasons ?? []).map((r) => r === undefined ? undefined : { code: r.Code, message: r.Message }),
175
+ };
176
+ }
177
+ return undefined;
178
+ };
179
+ /** Whether `error` is a rejection this module could diagnose at all. */
180
+ export const isDiagnosable = (error) => readFailure(error) !== undefined;
181
+ // ---------------------------------------------------------------------------
182
+ // Measure
183
+ // ---------------------------------------------------------------------------
184
+ /**
185
+ * The attributes an entry writes, as far as its request shows them: a Put's
186
+ * item, any entry's key, and an Update's `SET #name = :value` pairs.
187
+ */
188
+ const writtenAttributes = (item) => {
189
+ if (item.Put !== undefined)
190
+ return item.Put.Item ?? {};
191
+ const keyed = item.Delete ?? item.ConditionCheck;
192
+ if (keyed !== undefined)
193
+ return keyed.Key ?? {};
194
+ const update = item.Update;
195
+ if (update === undefined)
196
+ return {};
197
+ const written = { ...update.Key };
198
+ const names = update.ExpressionAttributeNames ?? {};
199
+ const values = update.ExpressionAttributeValues ?? {};
200
+ const setClause = /\bSET\b([\s\S]*?)(?=\b(?:REMOVE|ADD|DELETE)\b|$)/i.exec(update.UpdateExpression ?? "")?.[1];
201
+ if (setClause !== undefined) {
202
+ for (const match of setClause.matchAll(/(#[\w]+)\s*=\s*(:[\w]+)\s*(?=,|$)/g)) {
203
+ const name = names[match[1]];
204
+ const value = values[match[2]];
205
+ if (name !== undefined && value !== undefined)
206
+ written[name] = value;
207
+ }
208
+ }
209
+ return written;
210
+ };
211
+ /** A key attribute's size, by DynamoDB's rules: a string's UTF-8 bytes, a binary's bytes. */
212
+ const keyBytes = (value) => attributeBytes(value);
213
+ /** An empty string or binary: what DynamoDB refuses as a key value. */
214
+ const isEmptyKeyValue = (value) => value !== undefined &&
215
+ ((value.S !== undefined && value.S === "") || (value.B !== undefined && value.B.length === 0));
216
+ /**
217
+ * The first key attribute of `item` over its limit — a partition key over
218
+ * {@link PARTITION_KEY_MAX_BYTES}, a sort key over {@link SORT_KEY_MAX_BYTES} —
219
+ * among the `kinds` a rejection can mean.
220
+ */
221
+ const oversizedKey = (item, keys, kinds) => {
222
+ const written = writtenAttributes(item);
223
+ for (const kind of kinds) {
224
+ const limit = kind === "partition" ? PARTITION_KEY_MAX_BYTES : SORT_KEY_MAX_BYTES;
225
+ for (const attribute of kind === "partition" ? keys.partition : keys.sort) {
226
+ const value = written[attribute];
227
+ if (value === undefined)
228
+ continue;
229
+ const bytes = keyBytes(value);
230
+ if (bytes > limit)
231
+ return { kind, attribute, bytes };
232
+ }
233
+ }
234
+ return undefined;
235
+ };
236
+ /** The key kinds a key-size message can mean. */
237
+ const keySizeKinds = (message) => KEY_SIZE_EITHER.test(message)
238
+ ? ["partition", "sort"]
239
+ : KEY_SIZE_PARTITION.test(message)
240
+ ? ["partition"]
241
+ : ["sort"];
242
+ /** The attribute (path) an `IN ()` in `expression` tests, its placeholders resolved. */
243
+ const emptyInOperand = (expression, names) => {
244
+ const operand = EMPTY_IN_OPERAND.exec(expression ?? "")?.[1];
245
+ return operand === undefined
246
+ ? undefined
247
+ : operand.replace(/#\w+/g, (token) => names?.[token] ?? token);
248
+ };
249
+ /** A Put's item size — the 400 KB measure, which its expressions do not count toward. */
250
+ const putBytes = (item) => item.Put === undefined ? undefined : itemBytes(item.Put.Item ?? {});
251
+ /** An entry's expression of `kind`, if it carries one. */
252
+ const expressionOf = (item, kind) => {
253
+ if (kind === "UpdateExpression")
254
+ return item.Update?.UpdateExpression;
255
+ return (item.Put?.ConditionExpression ??
256
+ item.Update?.ConditionExpression ??
257
+ item.Delete?.ConditionExpression ??
258
+ item.ConditionCheck?.ConditionExpression);
259
+ };
260
+ /**
261
+ * UTF-8 byte order — the order DynamoDB sorts (and evaluates `BETWEEN` on)
262
+ * string keys. JavaScript's `<` compares UTF-16 code units, which disagrees
263
+ * for characters outside the BMP.
264
+ */
265
+ const utf8 = new TextEncoder();
266
+ export const compareUtf8 = (a, b) => compareBytes(utf8.encode(a), utf8.encode(b));
267
+ const compareBytes = (left, right) => {
268
+ const length = Math.min(left.length, right.length);
269
+ for (let i = 0; i < length; i++) {
270
+ const diff = left[i] - right[i];
271
+ if (diff !== 0)
272
+ return diff;
273
+ }
274
+ return left.length - right.length;
275
+ };
276
+ /** `a` sorts after `b` as DynamoDB orders key values of their (shared) type. */
277
+ const sortsAfter = (a, b) => {
278
+ if (a.S !== undefined && b.S !== undefined)
279
+ return compareUtf8(a.S, b.S) > 0;
280
+ if (a.N !== undefined && b.N !== undefined)
281
+ return Number(a.N) > Number(b.N);
282
+ if (a.B !== undefined && b.B !== undefined)
283
+ return compareBytes(a.B, b.B) > 0;
284
+ return false;
285
+ };
286
+ /** A key value, readable. */
287
+ const keyValueText = (value) => value.S ??
288
+ value.N ??
289
+ (value.B !== undefined ? `<${value.B.length} bytes>` : JSON.stringify(value));
290
+ /**
291
+ * The comparisons of a key condition — `#a = :v`, `#a BETWEEN :l AND :h`,
292
+ * `begins_with(#a, :v)` — with their attributes resolved through `names`.
293
+ */
294
+ const keyComparisons = (input) => {
295
+ const expression = input.KeyConditionExpression ?? "";
296
+ const names = input.ExpressionAttributeNames ?? {};
297
+ const values = input.ExpressionAttributeValues ?? {};
298
+ const attribute = (token) => names[token] ?? token;
299
+ const value = (token) => values[token];
300
+ const found = [];
301
+ const defined = (vs) => vs.filter((v) => v !== undefined);
302
+ for (const m of expression.matchAll(/([#\w.]+)\s+BETWEEN\s+(:\w+)\s+AND\s+(:\w+)/gi)) {
303
+ found.push({
304
+ attribute: attribute(m[1]),
305
+ operator: "between",
306
+ values: defined([value(m[2]), value(m[3])]),
307
+ });
308
+ }
309
+ for (const m of expression.matchAll(/begins_with\s*\(\s*([#\w.]+)\s*,\s*(:\w+)\s*\)/gi)) {
310
+ found.push({ attribute: attribute(m[1]), operator: "other", values: defined([value(m[2])]) });
311
+ }
312
+ for (const m of expression.matchAll(/([#\w.]+)\s*(?:=|<>|<=|>=|<|>)\s*(:\w+)/g)) {
313
+ found.push({ attribute: attribute(m[1]), operator: "other", values: defined([value(m[2])]) });
314
+ }
315
+ return found;
316
+ };
317
+ // ---------------------------------------------------------------------------
318
+ // Confirm and attribute: writes
319
+ // ---------------------------------------------------------------------------
320
+ const entriesOf = (sent) => sent._tag === "Item"
321
+ ? [sent.item]
322
+ : sent._tag === "TransactGet" || sent._tag === "BatchGet"
323
+ ? []
324
+ : sent.items;
325
+ const targetsOf = (sent) => {
326
+ if (sent._tag === "Item")
327
+ return [sent.target()];
328
+ if (sent._tag === "TransactGet")
329
+ return sent.targets?.() ?? [];
330
+ return sent.targets();
331
+ };
332
+ const targetAt = (sent, index) => {
333
+ if (sent._tag === "Item")
334
+ return index === 0 ? sent.target() : undefined;
335
+ if (sent._tag === "TransactGet")
336
+ return sent.targets?.()[index];
337
+ return sent.targets()[index];
338
+ };
339
+ /** The first two entries of `sent` that target one item, if any do. */
340
+ const repeatedItem = (sent) => {
341
+ if (sent._tag === "Item")
342
+ return undefined;
343
+ const seen = new Map();
344
+ for (const target of targetsOf(sent)) {
345
+ const first = seen.get(target.identity);
346
+ if (first !== undefined) {
347
+ return {
348
+ _tag: "RepeatedItem",
349
+ first,
350
+ second: target,
351
+ batch: sent._tag === "Batch" || sent._tag === "BatchGet",
352
+ };
353
+ }
354
+ seen.set(target.identity, target);
355
+ }
356
+ return undefined;
357
+ };
358
+ /** The `Put` over the item limit, else the largest `Put` (likely), else `undefined`. */
359
+ const oversizedPut = (sent) => {
360
+ const entries = entriesOf(sent);
361
+ let largest;
362
+ for (const [index, item] of entries.entries()) {
363
+ const bytes = putBytes(item);
364
+ if (bytes === undefined)
365
+ continue;
366
+ if (bytes > ITEM_MAX_BYTES) {
367
+ const target = targetAt(sent, index);
368
+ return target === undefined
369
+ ? undefined
370
+ : { _tag: "ItemTooLarge", target, bytes, likely: false };
371
+ }
372
+ if (largest === undefined || bytes > largest.bytes)
373
+ largest = { index, bytes };
374
+ }
375
+ if (largest === undefined)
376
+ return undefined;
377
+ const target = targetAt(sent, largest.index);
378
+ if (target === undefined)
379
+ return undefined;
380
+ return {
381
+ _tag: "ItemTooLarge",
382
+ target,
383
+ bytes: largest.bytes,
384
+ likely: sent._tag !== "Item",
385
+ };
386
+ };
387
+ /** The one `Update` an item-size-to-update message can mean, if the request holds one. */
388
+ const growingUpdate = (sent) => {
389
+ const entries = entriesOf(sent);
390
+ const updates = [...entries.entries()].filter(([, item]) => item.Update !== undefined);
391
+ if (updates.length !== 1)
392
+ return undefined;
393
+ const target = targetAt(sent, updates[0][0]);
394
+ return target === undefined
395
+ ? undefined
396
+ : { _tag: "ItemTooLarge", target, bytes: undefined, likely: sent._tag !== "Item" };
397
+ };
398
+ const keyAt = (sent, index, keys, kinds) => {
399
+ const item = entriesOf(sent)[index];
400
+ if (item === undefined)
401
+ return undefined;
402
+ const over = oversizedKey(item, keys, kinds);
403
+ if (over === undefined)
404
+ return undefined;
405
+ const target = targetAt(sent, index);
406
+ return target === undefined ? undefined : { _tag: "KeyTooLong", target, ...over };
407
+ };
408
+ const anyKey = (sent, keys, kinds) => {
409
+ const entries = entriesOf(sent);
410
+ for (let index = 0; index < entries.length; index++) {
411
+ const found = keyAt(sent, index, keys, kinds);
412
+ if (found !== undefined)
413
+ return found;
414
+ }
415
+ return undefined;
416
+ };
417
+ /**
418
+ * An `IN ()` rejection, confirmed: the first entry whose condition holds an
419
+ * `IN` with no operands.
420
+ */
421
+ const emptyInCondition = (sent) => {
422
+ for (const [index, item] of entriesOf(sent).entries()) {
423
+ const entry = item.Put ?? item.Update ?? item.Delete ?? item.ConditionCheck;
424
+ const attribute = emptyInOperand(entry?.ConditionExpression, entry?.ExpressionAttributeNames);
425
+ if (attribute === undefined)
426
+ continue;
427
+ const target = targetAt(sent, index);
428
+ return target === undefined
429
+ ? undefined
430
+ : { _tag: "EmptyIn", expression: "ConditionExpression", attribute, target };
431
+ }
432
+ return undefined;
433
+ };
434
+ /**
435
+ * An expression-limit rejection, confirmed: the first entry whose expression
436
+ * of the kind DynamoDB named (either, if it named none) breaks that limit.
437
+ */
438
+ const oversizedExpression = (message, sent) => {
439
+ const limit = EXPRESSION_LENGTH.test(message)
440
+ ? "length"
441
+ : EXPRESSION_OPERATORS.test(message)
442
+ ? "operators"
443
+ : EXPRESSION_IN.test(message)
444
+ ? "inOperands"
445
+ : undefined;
446
+ if (limit === undefined)
447
+ return undefined;
448
+ const named = EXPRESSION_KIND.exec(message)?.[1]?.toLowerCase();
449
+ const kinds = named === "condition"
450
+ ? ["ConditionExpression"]
451
+ : named === "update"
452
+ ? ["UpdateExpression"]
453
+ : named === undefined
454
+ ? ["ConditionExpression", "UpdateExpression"]
455
+ : [];
456
+ const entries = entriesOf(sent);
457
+ for (const kind of kinds) {
458
+ for (const [index, item] of entries.entries()) {
459
+ const expression = expressionOf(item, kind);
460
+ if (expression === undefined)
461
+ continue;
462
+ const counting = kind === "UpdateExpression" ? "update" : "condition";
463
+ const measured = limit === "length"
464
+ ? expression.length
465
+ : limit === "operators"
466
+ ? countOperators(expression, counting)
467
+ : largestInOperands(expression);
468
+ const max = limit === "length"
469
+ ? EXPRESSION_MAX_LENGTH
470
+ : limit === "operators"
471
+ ? EXPRESSION_MAX_OPERATORS
472
+ : IN_MAX_OPERANDS;
473
+ if (measured <= max)
474
+ continue;
475
+ const target = targetAt(sent, index);
476
+ if (target === undefined)
477
+ return undefined;
478
+ const callerText = kind !== "ConditionExpression"
479
+ ? undefined
480
+ : sent._tag === "Item"
481
+ ? sent.callerCondition?.()
482
+ : sent._tag === "Transaction"
483
+ ? sent.callerCondition?.(index)
484
+ : undefined;
485
+ const cost = callerText === undefined ? undefined : expressionCost(callerText);
486
+ return {
487
+ _tag: "ExpressionTooLarge",
488
+ target,
489
+ expression: kind,
490
+ limit,
491
+ measured,
492
+ caller: cost === undefined || callerText === undefined
493
+ ? undefined
494
+ : { ...cost, inOperands: largestInOperands(callerText) },
495
+ };
496
+ }
497
+ }
498
+ return undefined;
499
+ };
500
+ /**
501
+ * An empty-key rejection, confirmed: an entry writes an empty value to the
502
+ * key attribute DynamoDB named.
503
+ */
504
+ const emptyKey = (message, sent) => {
505
+ const onIndex = EMPTY_KEY_INDEX.exec(message);
506
+ const attribute = onIndex?.[2] ?? EMPTY_KEY_TABLE.exec(message)?.[1];
507
+ if (attribute === undefined)
508
+ return undefined;
509
+ const entries = entriesOf(sent);
510
+ for (const [index, item] of entries.entries()) {
511
+ if (!isEmptyKeyValue(writtenAttributes(item)[attribute]))
512
+ continue;
513
+ const target = targetAt(sent, index);
514
+ return target === undefined
515
+ ? undefined
516
+ : { _tag: "EmptyKey", target, attribute, index: onIndex?.[1] };
517
+ }
518
+ return undefined;
519
+ };
520
+ const diagnoseRequest = (message, sent, keys) => {
521
+ if (REPEATED.test(message) || REPEATED_BATCH.test(message))
522
+ return repeatedItem(sent);
523
+ if (ITEM_COUNT.test(message)) {
524
+ const count = sent._tag === "TransactGet"
525
+ ? sent.count
526
+ : sent._tag === "Transaction"
527
+ ? sent.items.length
528
+ : undefined;
529
+ return count !== undefined && count > TRANSACT_WRITE_ITEMS_LIMIT
530
+ ? { _tag: "TooManyItems", count, limit: TRANSACT_WRITE_ITEMS_LIMIT }
531
+ : undefined;
532
+ }
533
+ if (PAYLOAD.test(message)) {
534
+ if (sent._tag !== "Transaction" || sent.items.length === 0)
535
+ return undefined;
536
+ const sized = sent.items.map((item, index) => ({ index, bytes: transactItemBytes(item) }));
537
+ const measured = sized.reduce((sum, { bytes }) => sum + bytes, 0);
538
+ const targets = sent.targets();
539
+ const largest = [...sized]
540
+ .sort((a, b) => b.bytes - a.bytes)
541
+ .slice(0, 3)
542
+ .flatMap(({ index, bytes }) => {
543
+ const target = targets[index];
544
+ return target === undefined ? [] : [{ target, bytes }];
545
+ });
546
+ const reported = PAYLOAD_SIZE.exec(message)?.[1];
547
+ return {
548
+ _tag: "TransactionTooLarge",
549
+ reported: reported === undefined ? undefined : Number(reported),
550
+ measured,
551
+ largest,
552
+ };
553
+ }
554
+ if (KEY_SIZE.test(message))
555
+ return anyKey(sent, keys, keySizeKinds(message));
556
+ if (EMPTY_KEY.test(message))
557
+ return emptyKey(message, sent);
558
+ if (EMPTY_IN.test(message)) {
559
+ return EXPRESSION_KIND.exec(message)?.[1]?.toLowerCase() === "condition"
560
+ ? emptyInCondition(sent)
561
+ : undefined;
562
+ }
563
+ if (EXPRESSION_LENGTH.test(message) ||
564
+ EXPRESSION_OPERATORS.test(message) ||
565
+ EXPRESSION_IN.test(message)) {
566
+ return oversizedExpression(message, sent);
567
+ }
568
+ if (ITEM_SIZE_UPDATE.test(message)) {
569
+ if (sent._tag === "Item") {
570
+ return sent.item.Update === undefined && sent.item.Put === undefined
571
+ ? undefined
572
+ : { _tag: "ItemTooLarge", target: sent.target(), bytes: putBytes(sent.item), likely: false };
573
+ }
574
+ return growingUpdate(sent);
575
+ }
576
+ if (ITEM_SIZE.test(message)) {
577
+ if (sent._tag === "Item" && sent.item.Update !== undefined) {
578
+ return { _tag: "ItemTooLarge", target: sent.target(), bytes: undefined, likely: false };
579
+ }
580
+ return oversizedPut(sent);
581
+ }
582
+ return undefined;
583
+ };
584
+ const diagnoseCancellation = (reasons, sent, keys) => {
585
+ // A request that repeats an item can never commit, whatever DynamoDB
586
+ // reported for it: DynamoDB Local applies the operations in order, and a
587
+ // repeat whose condition then fails comes back as a conditional failure
588
+ // indistinguishable from a lost race.
589
+ const repeated = repeatedItem(sent);
590
+ if (repeated !== undefined)
591
+ return repeated;
592
+ // Any other conditional failure is the site's to explain, never this module's.
593
+ if (reasons.some((reason) => reason?.code === "ConditionalCheckFailed"))
594
+ return undefined;
595
+ const entries = entriesOf(sent);
596
+ for (const [index, reason] of reasons.entries()) {
597
+ if (reason?.code !== "ValidationError")
598
+ continue;
599
+ const message = reason.message ?? "";
600
+ const item = entries[index];
601
+ if (item === undefined)
602
+ continue;
603
+ if (KEY_SIZE.test(message)) {
604
+ const found = keyAt(sent, index, keys, keySizeKinds(message));
605
+ if (found !== undefined)
606
+ return found;
607
+ continue;
608
+ }
609
+ if ((ITEM_SIZE_UPDATE.test(message) || ITEM_SIZE.test(message)) &&
610
+ (item.Update !== undefined || item.Put !== undefined)) {
611
+ const target = targetAt(sent, index);
612
+ if (target !== undefined) {
613
+ return { _tag: "ItemTooLarge", target, bytes: putBytes(item), likely: false };
614
+ }
615
+ }
616
+ }
617
+ return undefined;
618
+ };
619
+ // ---------------------------------------------------------------------------
620
+ // Confirm and attribute: reads
621
+ // ---------------------------------------------------------------------------
622
+ const diagnoseRead = (message, sent) => {
623
+ const index = sent.input.IndexName;
624
+ if (CONSISTENT_GSI.test(message)) {
625
+ return sent.input.ConsistentRead === true && index !== undefined
626
+ ? { _tag: "ConsistentReadOnGsi", index, scan: sent.scan }
627
+ : undefined;
628
+ }
629
+ if (EMPTY_IN.test(message)) {
630
+ if (EXPRESSION_KIND.exec(message)?.[1]?.toLowerCase() !== "filter")
631
+ return undefined;
632
+ const attribute = emptyInOperand(sent.input.FilterExpression, sent.input.ExpressionAttributeNames);
633
+ return attribute === undefined
634
+ ? undefined
635
+ : { _tag: "EmptyIn", expression: "FilterExpression", attribute, target: undefined };
636
+ }
637
+ if (sent.scan)
638
+ return undefined;
639
+ if (BETWEEN_REVERSED.test(message)) {
640
+ for (const comparison of keyComparisons(sent.input)) {
641
+ const [low, high] = comparison.values;
642
+ if (comparison.operator !== "between" || low === undefined || high === undefined)
643
+ continue;
644
+ if (!sortsAfter(low, high))
645
+ continue;
646
+ return {
647
+ _tag: "BetweenReversed",
648
+ attribute: comparison.attribute,
649
+ index,
650
+ low: keyValueText(low),
651
+ high: keyValueText(high),
652
+ };
653
+ }
654
+ return undefined;
655
+ }
656
+ if (EMPTY_KEY.test(message)) {
657
+ const attribute = EMPTY_KEY_INDEX.exec(message)?.[2] ?? EMPTY_KEY_TABLE.exec(message)?.[1] ?? undefined;
658
+ if (attribute === undefined)
659
+ return undefined;
660
+ const empty = keyComparisons(sent.input).some((comparison) => comparison.attribute === attribute && comparison.values.some((v) => isEmptyKeyValue(v)));
661
+ return empty ? { _tag: "EmptyKeyBound", attribute, index } : undefined;
662
+ }
663
+ return undefined;
664
+ };
665
+ // ---------------------------------------------------------------------------
666
+ // Confirm and attribute: table management
667
+ // ---------------------------------------------------------------------------
668
+ const inlineFilters = (index) => (index.SearchSchema ?? [])
669
+ .filter((element) => element.SearchSchemaElementType === "INLINE_FILTER")
670
+ .map((element) => element.AttributeName ?? "");
671
+ const outOfDimensionRange = (index) => index.Dimensions !== undefined &&
672
+ (index.Dimensions < MIN_DIMENSIONS || index.Dimensions > MAX_DIMENSIONS);
673
+ const diagnoseTable = (message, sent) => {
674
+ if (sent.vectorIndexes.length === 0)
675
+ return undefined;
676
+ if (VECTOR_DIMENSIONS.test(message)) {
677
+ // The index DynamoDB named, if it named one of the request's; else the
678
+ // first outside the range.
679
+ const over = sent.vectorIndexes.filter(outOfDimensionRange);
680
+ const culprit = over.find((index) => index.IndexName !== undefined && message.includes(index.IndexName)) ??
681
+ over[0];
682
+ if (culprit === undefined)
683
+ return undefined;
684
+ const name = culprit.IndexName ?? "";
685
+ return {
686
+ _tag: "VectorIndexDimensions",
687
+ operation: sent.operation,
688
+ index: name,
689
+ dimensions: culprit.Dimensions,
690
+ sharers: sent.sharers(name),
691
+ };
692
+ }
693
+ if (!VECTOR.test(message))
694
+ return undefined;
695
+ if (VECTOR_FILTERS.test(message)) {
696
+ // The index DynamoDB named, if it named one of the request's; else the
697
+ // first over the limit.
698
+ const over = sent.vectorIndexes.filter((index) => inlineFilters(index).length > MAX_INLINE_FILTERS);
699
+ const culprit = over.find((index) => index.IndexName !== undefined && message.includes(index.IndexName)) ??
700
+ over[0];
701
+ if (culprit === undefined)
702
+ return undefined;
703
+ const name = culprit.IndexName ?? "";
704
+ return {
705
+ _tag: "VectorIndexFilters",
706
+ operation: sent.operation,
707
+ index: name,
708
+ filters: inlineFilters(culprit),
709
+ sharers: sent.sharers(name),
710
+ limit: MAX_INLINE_FILTERS,
711
+ };
712
+ }
713
+ if (!VECTOR_COUNT.test(message) || VECTOR_NOT_COUNT.test(message))
714
+ return undefined;
715
+ const statedText = STATED_LIMIT.exec(message)?.[1];
716
+ const stated = statedText === undefined ? undefined : Number(statedText);
717
+ const limit = stated ?? MAX_VECTOR_INDEXES_PER_TABLE;
718
+ const indexes = sent.vectorIndexes.map((index) => ({
719
+ index: index.IndexName ?? "",
720
+ sharers: sent.sharers(index.IndexName ?? ""),
721
+ }));
722
+ // Confirmed by counting against the limit DynamoDB stated, or its default
723
+ // quota. `CreateTable` carries every vector index; `UpdateTable` adds to
724
+ // the ones the table already has, read back after the rejection.
725
+ const existing = sent.operation === "UpdateTable" ? sent.existing : 0;
726
+ if (existing === undefined || existing + indexes.length <= limit)
727
+ return undefined;
728
+ return {
729
+ _tag: "VectorIndexCount",
730
+ operation: sent.operation,
731
+ indexes,
732
+ declared: sent.declared,
733
+ existing: sent.operation === "UpdateTable" ? existing : undefined,
734
+ stated,
735
+ };
736
+ };
737
+ /**
738
+ * A search rejected for its `TopK`, confirmed: the `TopK` sent is not an
739
+ * integer in DynamoDB's range.
740
+ */
741
+ const diagnoseSearch = (message, sent) => TOP_K.test(message) &&
742
+ (!Number.isInteger(sent.topK) || sent.topK < MIN_TOP_K || sent.topK > MAX_TOP_K)
743
+ ? { _tag: "TopKOutOfRange", index: sent.index, topK: sent.topK }
744
+ : undefined;
745
+ /**
746
+ * Read DynamoDB's rejection of `sent`. `keys` names the key attributes a write
747
+ * writes (the table's and its indexes', by kind), for the key-size shape.
748
+ * `undefined`: unrecognised or unconfirmed — the site fails with `error`
749
+ * unchanged.
750
+ */
751
+ export const diagnoseRejection = (error, sent, keys = NO_KEYS) => {
752
+ const failure = readFailure(error);
753
+ if (failure === undefined)
754
+ return undefined;
755
+ switch (sent._tag) {
756
+ case "Read":
757
+ return failure._tag === "Request" ? diagnoseRead(failure.message, sent) : undefined;
758
+ case "Table":
759
+ return failure._tag === "Request" ? diagnoseTable(failure.message, sent) : undefined;
760
+ case "Search":
761
+ return failure._tag === "Request" ? diagnoseSearch(failure.message, sent) : undefined;
762
+ default:
763
+ return failure._tag === "Request"
764
+ ? diagnoseRequest(failure.message, sent, keys)
765
+ : diagnoseCancellation(failure.reasons, sent, keys);
766
+ }
767
+ };
768
+ // ---------------------------------------------------------------------------
769
+ // Explain
770
+ // ---------------------------------------------------------------------------
771
+ const named = (target) => `${target.source}, at ${target.key}`;
772
+ const list = (items) => items.join(", ");
773
+ /**
774
+ * The explanation every site puts in its error, so the wording is identical
775
+ * library-wide. See {@link ExplainOptions} for a multi-request operation.
776
+ */
777
+ export const explainDiagnosis = (diagnosis, operation, options) => {
778
+ const outcome = options?.committed !== undefined
779
+ ? `This request wrote nothing; ${options.committed}.`
780
+ : "Nothing was written.";
781
+ switch (diagnosis._tag) {
782
+ case "ItemTooLarge":
783
+ return (`${operation}: DynamoDB rejected the write because an item exceeds its limit of ` +
784
+ `${ITEM_MAX_BYTES} bytes (400 KB) for one item — ` +
785
+ (diagnosis.likely ? "most likely the item of " : "the item of ") +
786
+ named(diagnosis.target) +
787
+ (diagnosis.bytes === undefined
788
+ ? " (the item it would produce is not in the request, so it cannot be measured)"
789
+ : `, ${diagnosis.bytes} bytes by DynamoDB's item-size rules`) +
790
+ "." +
791
+ (diagnosis.likely
792
+ ? " No item measures over the limit by the library's size model (measured against " +
793
+ "DynamoDB Local; AWS documents its sizes as approximate), so the largest is named."
794
+ : "") +
795
+ ` ${outcome}`);
796
+ case "TooManyItems":
797
+ return (`${operation}: the transaction holds ${diagnosis.count} items, over DynamoDB's limit ` +
798
+ `of ${diagnosis.limit} items for one transaction. ${outcome}`);
799
+ case "TransactionTooLarge": {
800
+ const disagrees = diagnosis.measured <= TRANSACT_WRITE_MAX_BYTES;
801
+ return (`${operation}: DynamoDB rejected the transaction for exceeding its limit of ` +
802
+ `${TRANSACT_WRITE_MAX_BYTES} bytes (4 MB) for one transaction` +
803
+ (diagnosis.reported === undefined ? "" : ` (it reported ${diagnosis.reported} bytes)`) +
804
+ `; the library measures the request at ${diagnosis.measured} bytes ` +
805
+ "(its items, or an update's or delete's key, plus every expression with its names and values)." +
806
+ (diagnosis.reported === undefined
807
+ ? ""
808
+ : " DynamoDB's figure is its running total in request order, up to the action that " +
809
+ "crossed the limit.") +
810
+ (disagrees
811
+ ? " That is within the limit: the library's size model disagrees with DynamoDB's for " +
812
+ "this request, so it was not split to fit."
813
+ : "") +
814
+ " The largest: " +
815
+ diagnosis.largest
816
+ .map(({ target, bytes }) => `${named(target)} (${bytes} bytes)`)
817
+ .join("; ") +
818
+ ". A put of a retain entity counts twice: its item and its version snapshot. " +
819
+ `Split the operations into smaller transactions. ${outcome}`);
820
+ }
821
+ case "KeyTooLong": {
822
+ const [kind, limit] = diagnosis.kind === "partition"
823
+ ? ["partition key", PARTITION_KEY_MAX_BYTES]
824
+ : ["sort key", SORT_KEY_MAX_BYTES];
825
+ return (`${operation}: the ${kind} "${diagnosis.attribute}" of ${named(diagnosis.target)} is ` +
826
+ `${diagnosis.bytes} bytes, longer than DynamoDB's ${limit}-byte limit for ` +
827
+ `a ${kind}. ${outcome}`);
828
+ }
829
+ case "RepeatedItem":
830
+ return diagnosis.batch
831
+ ? `${operation}: the batch request touches one item more than once — ` +
832
+ `${diagnosis.first.source} and ${diagnosis.second.source} both target the item at ` +
833
+ `${diagnosis.second.key}. DynamoDB allows one request per item in a batch request, ` +
834
+ `and rejected it. Write the item once. ${outcome}`
835
+ : `${operation}: the transaction touches one item more than once — ` +
836
+ `${diagnosis.first.source} and ${diagnosis.second.source} both target the item at ` +
837
+ `${diagnosis.second.key}. DynamoDB allows one operation per item in a transaction, ` +
838
+ "counting the uniqueness sentinels and version snapshots a put writes, and rejected " +
839
+ `it. Split the operations into separate writes. ${outcome}`;
840
+ case "ExpressionTooLarge": {
841
+ const subject = `the ${diagnosis.expression} of ${named(diagnosis.target)}`;
842
+ const over = diagnosis.limit === "length"
843
+ ? `it is ${diagnosis.measured} characters, over DynamoDB's limit of ` +
844
+ `${EXPRESSION_MAX_LENGTH} characters for one expression`
845
+ : diagnosis.limit === "operators"
846
+ ? `it holds ${diagnosis.measured} operators and functions, over DynamoDB's limit of ` +
847
+ `${EXPRESSION_MAX_OPERATORS} for one expression`
848
+ : `an IN in it holds ${diagnosis.measured} operands, over DynamoDB's limit of ` +
849
+ `${IN_MAX_OPERANDS}`;
850
+ const caller = diagnosis.caller;
851
+ // The library added a guard beside the caller's condition: say so.
852
+ const shared = caller !== undefined &&
853
+ (diagnosis.limit === "length"
854
+ ? caller.length
855
+ : diagnosis.limit === "operators"
856
+ ? caller.operators
857
+ : caller.inOperands) < diagnosis.measured;
858
+ const advice = caller !== undefined && shared
859
+ ? ` The .condition() alone is ${caller.length} characters / ${caller.operators} ` +
860
+ "operators, and the library's own guard needs room beside it: simplify the condition."
861
+ : diagnosis.expression === "UpdateExpression"
862
+ ? " Split the update into smaller ones."
863
+ : " Simplify the condition.";
864
+ return `${operation}: DynamoDB rejected ${subject}: ${over}.${advice} ${outcome}`;
865
+ }
866
+ case "EmptyKey":
867
+ return (`${operation}: DynamoDB rejected ${named(diagnosis.target)}: it writes an empty value ` +
868
+ `to the key attribute "${diagnosis.attribute}"` +
869
+ (diagnosis.index === undefined ? "" : ` of index "${diagnosis.index}"`) +
870
+ `, and DynamoDB does not accept an empty string as a key value. ${outcome}`);
871
+ case "EmptyIn":
872
+ return diagnosis.target === undefined
873
+ ? `${operation}: DynamoDB rejected the filter: isIn() on "${diagnosis.attribute}" has no ` +
874
+ "values, and DynamoDB rejects the `IN ()` it compiles to as a syntax error. Give " +
875
+ "isIn() at least one value, or leave the filter out."
876
+ : `${operation}: DynamoDB rejected ${named(diagnosis.target)}: isIn() on ` +
877
+ `"${diagnosis.attribute}" in its condition has no values, and DynamoDB rejects the ` +
878
+ "`IN ()` it compiles to as a syntax error. Give isIn() at least one value, or " +
879
+ `leave the condition out. ${outcome}`;
880
+ case "EmptyKeyBound":
881
+ return (`${operation}: DynamoDB rejected the query: its key condition compares the key ` +
882
+ `attribute "${diagnosis.attribute}"` +
883
+ (diagnosis.index === undefined ? "" : ` of index "${diagnosis.index}"`) +
884
+ " with an empty string, which DynamoDB does not accept as a key value.");
885
+ case "BetweenReversed":
886
+ return (`${operation}: DynamoDB rejected the query: between's lower bound ` +
887
+ `${JSON.stringify(diagnosis.low)} sorts after its upper bound ` +
888
+ `${JSON.stringify(diagnosis.high)} on "${diagnosis.attribute}"` +
889
+ (diagnosis.index === undefined ? "" : ` of index "${diagnosis.index}"`) +
890
+ " (DynamoDB orders strings by their UTF-8 bytes).");
891
+ case "ConsistentReadOnGsi":
892
+ return (`consistentRead: index "${diagnosis.index}" is a global secondary index, which ` +
893
+ "DynamoDB reads only eventually consistently, so DynamoDB rejected the consistent " +
894
+ `${diagnosis.scan ? "scan" : "query"}.`);
895
+ case "VectorIndexFilters":
896
+ return (`${operation}: DynamoDB rejected vector index "${diagnosis.index}": it declares ` +
897
+ `${diagnosis.filters.length} INLINE_FILTER attributes (${list(diagnosis.filters)}), over ` +
898
+ `DynamoDB's limit of ${diagnosis.limit} per vector index. They are the union of the ` +
899
+ `filters of the entities sharing it (${list(diagnosis.sharers)}). ` +
900
+ (diagnosis.operation === "CreateTable"
901
+ ? "The table was not created."
902
+ : "Nothing was changed."));
903
+ case "VectorIndexDimensions":
904
+ return (`${operation}: DynamoDB rejected vector index "${diagnosis.index}": its dimensions ` +
905
+ `(${diagnosis.dimensions}) are outside the range DynamoDB accepts, ` +
906
+ `${MIN_DIMENSIONS}..${MAX_DIMENSIONS}. The entities sharing it ` +
907
+ `(${list(diagnosis.sharers)}) declare it. ` +
908
+ (diagnosis.operation === "CreateTable"
909
+ ? "The table was not created."
910
+ : "Nothing was changed."));
911
+ case "TopKOutOfRange":
912
+ return (`${operation}: DynamoDB rejected the search of vector index "${diagnosis.index}": ` +
913
+ `topK(${diagnosis.topK}) is outside the range DynamoDB accepts, an integer in ` +
914
+ `${MIN_TOP_K}..${MAX_TOP_K}. ` +
915
+ `A vector search returns at most ${MAX_TOP_K} results, with no further pages.`);
916
+ case "VectorIndexCount": {
917
+ const indexes = diagnosis.indexes
918
+ .map(({ index, sharers }) => `"${index}" (${list(sharers)})`)
919
+ .join(", ");
920
+ const quota = `its limit on vector indexes per table (${diagnosis.stated === undefined
921
+ ? `${MAX_VECTOR_INDEXES_PER_TABLE} by default`
922
+ : diagnosis.stated}; the quota is adjustable through AWS Service Quotas)`;
923
+ return diagnosis.operation === "CreateTable"
924
+ ? `${operation}: DynamoDB rejected the table's ${diagnosis.indexes.length} vector ` +
925
+ `indexes, ${indexes}, for exceeding ${quota}. The table was not created.`
926
+ : `${operation}: DynamoDB rejected adding vector index ${indexes} for exceeding ` +
927
+ `${quota}: the table already has ${diagnosis.existing ?? 0} vector ` +
928
+ `${diagnosis.existing === 1 ? "index" : "indexes"}, and its entities declare ` +
929
+ `${diagnosis.declared} in all. Nothing was changed.`;
930
+ }
931
+ }
932
+ };
933
+ /**
934
+ * An `Error` carrying `message`, with DynamoDB's error one step down as its
935
+ * own `cause` — what a diagnosed error stores in its `cause`, so the readable
936
+ * text stays where callers look and the original is kept.
937
+ */
938
+ export const withCause = (message, original) => new Error(message, { cause: original });
939
+ /** The entity (or stream) a diagnosis names, for an error's `entityType`. */
940
+ export const diagnosedEntityType = (diagnosis, fallback) => {
941
+ switch (diagnosis._tag) {
942
+ case "ItemTooLarge":
943
+ case "KeyTooLong":
944
+ case "ExpressionTooLarge":
945
+ case "EmptyKey":
946
+ return diagnosis.target.entityType;
947
+ case "EmptyIn":
948
+ return diagnosis.target?.entityType ?? fallback;
949
+ case "RepeatedItem":
950
+ return diagnosis.first.entityType;
951
+ case "TransactionTooLarge":
952
+ return diagnosis.largest[0]?.target.entityType ?? fallback;
953
+ default:
954
+ return fallback;
955
+ }
956
+ };
957
+ /** The `ValidationError` a diagnosis becomes at most sites. */
958
+ export const diagnosedValidationError = (diagnosis, original, options) => new ValidationError({
959
+ entityType: diagnosedEntityType(diagnosis, options.entityType),
960
+ operation: options.operation,
961
+ cause: withCause(explainDiagnosis(diagnosis, options.operation, options), original),
962
+ });
963
+ // ---------------------------------------------------------------------------
964
+ // Wiring
965
+ // ---------------------------------------------------------------------------
966
+ /**
967
+ * Wrap a site's request so that, on failure only, a diagnosable error is read
968
+ * against what was sent and — when confirmed — rewritten by `rewrite`.
969
+ * Everything else, and every success, passes through untouched.
970
+ *
971
+ * Placed after the site's own conditional-failure mapping, so it only ever
972
+ * sees what that mapping could not explain — except for a repeated item: a
973
+ * site whose mapping reads conditional failures (and may retry them) wraps its
974
+ * request a second time, BEFORE that mapping, with `only: ["RepeatedItem"]`,
975
+ * because DynamoDB can report a repeat as a conditional failure that reads as
976
+ * a lost race. A diagnosis is terminal: no retry loop sees it.
977
+ *
978
+ * `only` limits the rewrite to those diagnoses; any other passes through.
979
+ */
980
+ export const diagnoseFailure = (options) => (self) => Effect.catch(self, (error) => {
981
+ if (!isDiagnosable(error))
982
+ return Effect.fail(error);
983
+ const diagnosis = diagnoseRejection(error, options.sent(), options.keys?.() ?? NO_KEYS);
984
+ if (diagnosis === undefined)
985
+ return Effect.fail(error);
986
+ if (options.only !== undefined && !options.only.includes(diagnosis._tag)) {
987
+ return Effect.fail(error);
988
+ }
989
+ return Effect.fail(options.rewrite(diagnosis, error));
990
+ });
991
+ // ---------------------------------------------------------------------------
992
+ // Targets
993
+ // ---------------------------------------------------------------------------
994
+ /**
995
+ * Targets for entries that all share one primary key schema (`keyFields`),
996
+ * named by `source(index, item)`. Each entry's own `TableName` is used.
997
+ */
998
+ export const targetsFor = (items, keyFields, entityType, source) => items.map((item, index) => transactItemTarget(item, item.Put?.TableName ??
999
+ item.Update?.TableName ??
1000
+ item.Delete?.TableName ??
1001
+ item.ConditionCheck?.TableName ??
1002
+ "", keyFields, typeof entityType === "string" ? entityType : entityType(index), source(index, item)));
1003
+ /** A `BatchWriteItem` request map, flattened to transact-entry form in request order. */
1004
+ export const batchEntries = (requestItems) => Object.entries(requestItems).flatMap(([tableName, requests]) => requests.flatMap((request) => {
1005
+ if (request.PutRequest !== undefined) {
1006
+ return [{ Put: { TableName: tableName, Item: request.PutRequest.Item ?? {} } }];
1007
+ }
1008
+ if (request.DeleteRequest !== undefined) {
1009
+ return [{ Delete: { TableName: tableName, Key: request.DeleteRequest.Key ?? {} } }];
1010
+ }
1011
+ return [];
1012
+ }));
1013
+ /**
1014
+ * The key attributes an entity writes, by kind: its primary key's and each
1015
+ * index's partition and sort keys. `indexes` is an entity's `indexes` record.
1016
+ */
1017
+ export const keyAttributesOf = (indexes) => {
1018
+ const fields = (half) => [
1019
+ ...new Set(Object.values(indexes).flatMap((index) => index?.[half]?.field === undefined ? [] : [index[half].field])),
1020
+ ];
1021
+ return { partition: fields("pk"), sort: fields("sk") };
1022
+ };
1023
+ /** Key attributes from several sources, each attribute once per kind. */
1024
+ export const mergeKeyAttributes = (...all) => ({
1025
+ partition: [...new Set(all.flatMap((keys) => keys.partition))],
1026
+ sort: [...new Set(all.flatMap((keys) => keys.sort))],
1027
+ });
1028
+ //# sourceMappingURL=WriteDiagnosis.js.map