effect-dynamodb 1.25.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 (72) hide show
  1. package/dist/Aggregate.d.ts +7 -0
  2. package/dist/Aggregate.d.ts.map +1 -1
  3. package/dist/Aggregate.js +150 -4
  4. package/dist/Aggregate.js.map +1 -1
  5. package/dist/Collection.d.ts +9 -1
  6. package/dist/Collection.d.ts.map +1 -1
  7. package/dist/Collection.js +3 -0
  8. package/dist/Collection.js.map +1 -1
  9. package/dist/DynamoClient.d.ts +80 -28
  10. package/dist/DynamoClient.d.ts.map +1 -1
  11. package/dist/DynamoClient.js +83 -11
  12. package/dist/DynamoClient.js.map +1 -1
  13. package/dist/Entity.d.ts +80 -27
  14. package/dist/Entity.d.ts.map +1 -1
  15. package/dist/Entity.js +406 -76
  16. package/dist/Entity.js.map +1 -1
  17. package/dist/EventStore.d.ts +182 -2
  18. package/dist/EventStore.d.ts.map +1 -1
  19. package/dist/EventStore.js +328 -41
  20. package/dist/EventStore.js.map +1 -1
  21. package/dist/Query.d.ts +53 -0
  22. package/dist/Query.d.ts.map +1 -1
  23. package/dist/Query.js +88 -6
  24. package/dist/Query.js.map +1 -1
  25. package/dist/StorageEncoding.d.ts +160 -0
  26. package/dist/StorageEncoding.d.ts.map +1 -0
  27. package/dist/StorageEncoding.js +258 -0
  28. package/dist/StorageEncoding.js.map +1 -0
  29. package/dist/Table.d.ts.map +1 -1
  30. package/dist/Table.js +8 -6
  31. package/dist/Table.js.map +1 -1
  32. package/dist/Transaction.d.ts.map +1 -1
  33. package/dist/Transaction.js +5 -0
  34. package/dist/Transaction.js.map +1 -1
  35. package/dist/compression/zlib.d.ts +75 -0
  36. package/dist/compression/zlib.d.ts.map +1 -0
  37. package/dist/compression/zlib.js +109 -0
  38. package/dist/compression/zlib.js.map +1 -0
  39. package/dist/index.d.ts +4 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +3 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal/BoundCrud.d.ts +39 -35
  44. package/dist/internal/BoundCrud.d.ts.map +1 -1
  45. package/dist/internal/BoundCrud.js.map +1 -1
  46. package/dist/internal/BoundQuery.d.ts +3 -3
  47. package/dist/internal/BoundQuery.d.ts.map +1 -1
  48. package/dist/internal/BoundQuery.js.map +1 -1
  49. package/dist/internal/BoundVectorQuery.d.ts +12 -0
  50. package/dist/internal/BoundVectorQuery.d.ts.map +1 -1
  51. package/dist/internal/BoundVectorQuery.js +14 -1
  52. package/dist/internal/BoundVectorQuery.js.map +1 -1
  53. package/dist/internal/EntityCombinators.d.ts.map +1 -1
  54. package/dist/internal/EntityCombinators.js +16 -1
  55. package/dist/internal/EntityCombinators.js.map +1 -1
  56. package/dist/internal/Expr.d.ts +5 -3
  57. package/dist/internal/Expr.d.ts.map +1 -1
  58. package/dist/internal/Expr.js +1 -1
  59. package/dist/internal/Expr.js.map +1 -1
  60. package/dist/internal/Packed.d.ts +289 -0
  61. package/dist/internal/Packed.d.ts.map +1 -0
  62. package/dist/internal/Packed.js +654 -0
  63. package/dist/internal/Packed.js.map +1 -0
  64. package/dist/internal/PathBuilder.d.ts +45 -2
  65. package/dist/internal/PathBuilder.d.ts.map +1 -1
  66. package/dist/internal/PathBuilder.js +4 -0
  67. package/dist/internal/PathBuilder.js.map +1 -1
  68. package/dist/internal/TransactableOps.d.ts +1 -1
  69. package/dist/internal/TransactableOps.d.ts.map +1 -1
  70. package/dist/internal/TransactableOps.js +2 -0
  71. package/dist/internal/TransactableOps.js.map +1 -1
  72. package/package.json +6 -2
@@ -0,0 +1,654 @@
1
+ /**
2
+ * @internal Packed storage at the Marshaller boundary — item-level pack/unpack
3
+ * over the pure codec (`@effect-dynamodb/schema/internal/PackedCodec`), the
4
+ * compressor registry in scope, and DynamoDB Streams image normalisation.
5
+ *
6
+ * Where it runs:
7
+ *
8
+ * - **Write:** after the schema encode, field renames and sparse flattening,
9
+ * before `toAttributeMap` — {@link packItem} replaces each packed attribute's
10
+ * wire value with the JS value that marshals to its stored form (`string` →
11
+ * `S`, `Uint8Array` → `B`, `null` → `NULL`). Everything measured afterwards
12
+ * (item-size diagnosis, `Batch.write` chunking) therefore sees stored bytes.
13
+ * - **Read:** after `fromAttributeMap`, before sparse rebuild and renames —
14
+ * {@link unpackItem} dispatches each dispatch-eligible attribute on its
15
+ * stored form (native → unchanged, `S` → JSON, `B` → header + payload).
16
+ * - **Streams:** {@link normalizeImage} turns a Streams image (DynamoDB JSON,
17
+ * `B`/`BS` as bytes or base64, or an already-unmarshalled item) into an
18
+ * unmarshalled item with real bytes, and {@link unpackImageItem} additionally
19
+ * reads base64 text on a dispatch-eligible attribute as `B`.
20
+ *
21
+ * The registry is read from the fiber's context (as `Effect.serviceOption`
22
+ * would), so nothing here adds a requirement; an absent service is the empty
23
+ * registry. Every per-value step is synchronous — a plain function returning
24
+ * its result or the `ValidationError` it fails with — and is lifted into an
25
+ * Effect once per item (or per operation), not once per value: a read of
26
+ * thousands of rows pays for `JSON.parse` / decompression, not for Effect
27
+ * plumbing around each value.
28
+ */
29
+ import { Compression, empty as emptyRegistry, } from "@effect-dynamodb/schema/Compression.js";
30
+ import { ValidationError } from "@effect-dynamodb/schema/Errors.js";
31
+ import { isBase64PackedText, packValueSync, unpackBytesSync, unpackStoredSync, } from "@effect-dynamodb/schema/internal/PackedCodec.js";
32
+ import { Context, Effect, Result } from "effect";
33
+ import { Base64 } from "effect/encoding";
34
+ import { fromAttributeMap } from "../Marshaller.js";
35
+ // ---------------------------------------------------------------------------
36
+ // Registry
37
+ // ---------------------------------------------------------------------------
38
+ /** The compressor registry in a context — the empty registry when none is provided. */
39
+ export const registryOf = (context) => Context.getOrUndefined(context, Compression) ?? emptyRegistry;
40
+ /** The compressor registry in scope — the empty registry when none is provided. */
41
+ export const currentRegistry = Effect.withFiber((fiber) => Effect.succeed(registryOf(fiber.context)));
42
+ /**
43
+ * Run a synchronous pack/unpack step with the registry in scope, as ONE
44
+ * Effect: `step` returns its result, or the `ValidationError` it fails with.
45
+ * (`step` must never succeed with a `ValidationError` value — none of the
46
+ * steps here can: they return parsed JSON, stored values or `undefined`.)
47
+ */
48
+ export const withRegistry = (step) => Effect.withFiber((fiber) => {
49
+ const result = step(registryOf(fiber.context));
50
+ return result instanceof ValidationError ? Effect.fail(result) : Effect.succeed(result);
51
+ });
52
+ /** Lift a synchronous step's result (or the `ValidationError` it returned) into an Effect. */
53
+ export const fromStep = (result) => result instanceof ValidationError ? Effect.fail(result) : Effect.succeed(result);
54
+ /** The codecs `configs` (stored attribute → packed config) name. */
55
+ export const codecRequirements = (owner, configs) => {
56
+ const out = [];
57
+ for (const [attribute, config] of configs) {
58
+ const codec = config?.compression?.codec;
59
+ if (codec !== undefined)
60
+ out.push({ owner, attribute, codec });
61
+ }
62
+ return out;
63
+ };
64
+ /**
65
+ * The `[EDD-9078]` configuration error for `requirements` the registry in scope
66
+ * (`undefined`: none) cannot meet, or `undefined` when it meets them all.
67
+ * `where` names the binding — `DynamoClient.make`, `EventStore.bind`, ….
68
+ */
69
+ export const unregisteredCodecError = (where, requirements, registry) => {
70
+ const missing = requirements.filter((r) => registry?.get(r.codec) === undefined);
71
+ if (missing.length === 0)
72
+ return undefined;
73
+ const what = missing
74
+ .map((r) => `${r.owner} stores "${r.attribute}" packed with codec "${r.codec}"`)
75
+ .join("; ");
76
+ const codecs = [...new Set(missing.map((r) => `"${r.codec}"`))].join(", ");
77
+ const scope = registry === undefined
78
+ ? "no Compression registry is in scope"
79
+ : registry.codecs.length === 0
80
+ ? "the Compression registry in scope has no compressors"
81
+ : `the Compression registry in scope has ${registry.codecs.map((c) => `"${c}"`).join(", ")}`;
82
+ return new Error(`[EDD-9078] ${where}: ${what}, but ${scope}. Every reader and writer meets values ` +
83
+ `compressed with a configured codec, so register ${codecs} where the client or stream ` +
84
+ "is bound: DynamoClient.make({ compressors }), or Compression.layer([...]) merged into " +
85
+ "the application layer (see the packed storage guide).");
86
+ };
87
+ /**
88
+ * A once-per-registry check of `requirements` for code that resolves its
89
+ * registry at run time (an unbound `EventStream`): the first operation run
90
+ * with a given registry checks it; later ones cost a `WeakSet` lookup. Dies
91
+ * with the `[EDD-9078]` error — a configuration defect, not a typed failure.
92
+ * `undefined` when nothing names a codec.
93
+ */
94
+ export const runtimeCodecCheck = (where, requirements) => {
95
+ if (requirements.length === 0)
96
+ return undefined;
97
+ const passed = new WeakSet();
98
+ return Effect.withFiber((fiber) => {
99
+ const registry = Context.getOrUndefined(fiber.context, Compression);
100
+ if (registry !== undefined && passed.has(registry))
101
+ return Effect.void;
102
+ const error = unregisteredCodecError(where, requirements, registry);
103
+ if (error !== undefined)
104
+ return Effect.die(error);
105
+ passed.add(registry);
106
+ return Effect.void;
107
+ });
108
+ };
109
+ // ---------------------------------------------------------------------------
110
+ // Write
111
+ // ---------------------------------------------------------------------------
112
+ /**
113
+ * Pack, in place, every attribute of `item` (keyed by **stored** name) that
114
+ * `packed` configures. An attribute the item does not carry is left absent; a
115
+ * wire value of `undefined` removes it. Returns the `ValidationError` of the
116
+ * first attribute that fails (the item is then partly packed — discard it), or
117
+ * `undefined`.
118
+ */
119
+ export const packItemSync = (item, packed, registry, ctx) => {
120
+ for (const attribute in packed) {
121
+ if (!(attribute in item))
122
+ continue;
123
+ const stored = packValueSync(packed[attribute], item[attribute], registry, ctx, attribute);
124
+ if (stored instanceof ValidationError)
125
+ return stored;
126
+ if (stored === undefined)
127
+ delete item[attribute];
128
+ else
129
+ item[attribute] = stored;
130
+ }
131
+ return undefined;
132
+ };
133
+ /** {@link packItemSync} as an Effect. */
134
+ export const packItem = (item, packed, registry, ctx) => {
135
+ const failure = packItemSync(item, packed, registry, ctx);
136
+ return failure === undefined ? Effect.void : Effect.fail(failure);
137
+ };
138
+ /** The `AttributeValue` a packed stored value marshals to (`undefined`: no attribute). */
139
+ export const storedToAttributeValue = (stored) => {
140
+ if (stored === undefined)
141
+ return undefined;
142
+ if (stored === null)
143
+ return { NULL: true };
144
+ if (typeof stored === "string")
145
+ return { S: stored };
146
+ return { B: stored };
147
+ };
148
+ /**
149
+ * Pack one wire value straight to its `AttributeValue` — for an `UpdateItem`
150
+ * `SET #f = :v`, where the value never passes through `toAttributeMap`.
151
+ */
152
+ export const packAttributeValue = (config, wire, registry, ctx) => fromStep(packAttributeValueSync(config, wire, registry, ctx, ctx.attribute));
153
+ /** {@link packAttributeValue} without an Effect: the value, or the failure (returned). */
154
+ export const packAttributeValueSync = (config, wire, registry, ctx, attribute) => {
155
+ const stored = packValueSync(config, wire, registry, ctx, attribute);
156
+ return stored instanceof ValidationError ? stored : storedToAttributeValue(stored);
157
+ };
158
+ // ---------------------------------------------------------------------------
159
+ // Read
160
+ // ---------------------------------------------------------------------------
161
+ /**
162
+ * True iff any listed attribute of `raw` holds a string or bytes — a value
163
+ * read by its stored form. A native row (maps, lists, absent) is false, and
164
+ * costs one `typeof` per attribute.
165
+ */
166
+ export const hasStoredForm = (raw, attributes) => {
167
+ for (let i = 0; i < attributes.length; i++) {
168
+ const value = raw[attributes[i]];
169
+ if (typeof value === "string" || value instanceof Uint8Array)
170
+ return true;
171
+ }
172
+ return false;
173
+ };
174
+ /**
175
+ * Unpack, in place, every listed attribute of an unmarshalled item (keyed by
176
+ * **stored** name) by its stored form. Native values cost one `typeof` check.
177
+ * Returns the `ValidationError` of the first value that fails, or `undefined`.
178
+ */
179
+ export const unpackItemSync = (raw, attributes, registry, ctx) => {
180
+ for (const attribute of attributes) {
181
+ const value = raw[attribute];
182
+ if (typeof value !== "string" && !(value instanceof Uint8Array))
183
+ continue;
184
+ const wire = unpackStoredSync(value, registry, ctx, attribute);
185
+ if (wire instanceof ValidationError)
186
+ return wire;
187
+ raw[attribute] = wire;
188
+ }
189
+ return undefined;
190
+ };
191
+ /** {@link unpackItemSync} as an Effect. */
192
+ export const unpackItem = (raw, attributes, registry, ctx) => {
193
+ const failure = unpackItemSync(raw, attributes, registry, ctx);
194
+ return failure === undefined ? Effect.void : Effect.fail(failure);
195
+ };
196
+ /**
197
+ * {@link unpackItemSync} with the registry in scope, as one Effect — and no
198
+ * registry lookup at all for a row with nothing stored packed.
199
+ */
200
+ export const unpackRow = (raw, attributes, ctx) => hasStoredForm(raw, attributes)
201
+ ? withRegistry((registry) => unpackItemSync(raw, attributes, registry, ctx))
202
+ : Effect.void;
203
+ const ATTRIBUTE_VALUE_TAGS = new Set(["S", "N", "B", "BOOL", "NULL", "SS", "NS", "BS", "M", "L"]);
204
+ /** True iff `value` is shaped like one DynamoDB JSON `AttributeValue` (one known tag key). */
205
+ export const isAttributeValueShape = (value) => {
206
+ if (typeof value !== "object" || value === null || Array.isArray(value))
207
+ return false;
208
+ if (value instanceof Uint8Array)
209
+ return false;
210
+ const keys = Object.keys(value);
211
+ return keys.length === 1 && ATTRIBUTE_VALUE_TAGS.has(keys[0]);
212
+ };
213
+ /**
214
+ * True iff `image` is DynamoDB JSON rather than an unmarshalled item: its
215
+ * `probe` attribute (default `"pk"`, a string key attribute on every item the
216
+ * library writes) is an object with a string `S` member — or, without that
217
+ * attribute, every value is shaped like an `AttributeValue`.
218
+ */
219
+ export const isAttributeValueImage = (image, probe = "pk") => {
220
+ const key = image[probe];
221
+ if (key !== undefined) {
222
+ return typeof key === "object" && key !== null && typeof key.S === "string";
223
+ }
224
+ const values = Object.values(image);
225
+ return values.length > 0 && values.every(isAttributeValueShape);
226
+ };
227
+ /**
228
+ * The entity type (`__edd_e__`) a stream image names, read straight off the
229
+ * image — `{ S }` in DynamoDB JSON, a string when unmarshalled — so a decoder
230
+ * can turn away an image of another type **before** unmarshalling it: an
231
+ * attribute the SDK cannot unmarshall (a number beyond what it represents,
232
+ * say) in some other entity's image must not fail the decode of a batch.
233
+ * `undefined` when the image names none.
234
+ */
235
+ export const imageEntityType = (image) => {
236
+ if (typeof image !== "object" || image === null || Array.isArray(image))
237
+ return undefined;
238
+ const value = image.__edd_e__;
239
+ if (typeof value === "string")
240
+ return value;
241
+ if (typeof value === "object" && value !== null) {
242
+ const s = value.S;
243
+ if (typeof s === "string")
244
+ return s;
245
+ }
246
+ return undefined;
247
+ };
248
+ const imageError = (ctx, message, cause) => new ValidationError({
249
+ entityType: ctx.entityType,
250
+ operation: ctx.operation,
251
+ cause: cause === undefined ? new Error(message) : new Error(message, { cause }),
252
+ });
253
+ /** Decode base64 text (or pass bytes through), or the failure naming the attribute. */
254
+ const toBytesSync = (value, ctx, attribute) => {
255
+ if (value instanceof Uint8Array)
256
+ return value;
257
+ if (typeof value === "string") {
258
+ const decoded = Base64.decode(value);
259
+ if (Result.isSuccess(decoded))
260
+ return decoded.success;
261
+ return imageError(ctx, `Attribute "${attribute}": a binary value is not valid base64`, decoded.failure);
262
+ }
263
+ return imageError(ctx, `Attribute "${attribute}": a binary value must be bytes or base64 text, got ${typeof value}`);
264
+ };
265
+ /**
266
+ * One DynamoDB-JSON `AttributeValue` with every `B` / `BS` member (at any
267
+ * depth) as real bytes — the value itself when it holds none as text, so a
268
+ * typical image is not copied.
269
+ */
270
+ const normalizeAttributeValueSync = (value, ctx, path) => {
271
+ if (!isAttributeValueShape(value)) {
272
+ return imageError(ctx, `Attribute "${path}": not a DynamoDB JSON attribute value`);
273
+ }
274
+ const av = value;
275
+ if ("B" in av) {
276
+ if (av.B instanceof Uint8Array)
277
+ return av;
278
+ const B = toBytesSync(av.B, ctx, path);
279
+ return B instanceof ValidationError ? B : { B };
280
+ }
281
+ if ("BS" in av) {
282
+ if (!Array.isArray(av.BS) || av.BS.every((element) => element instanceof Uint8Array)) {
283
+ return av;
284
+ }
285
+ const BS = [];
286
+ for (const element of av.BS) {
287
+ const bytes = toBytesSync(element, ctx, path);
288
+ if (bytes instanceof ValidationError)
289
+ return bytes;
290
+ BS.push(bytes);
291
+ }
292
+ return { BS };
293
+ }
294
+ if ("M" in av && typeof av.M === "object" && av.M !== null) {
295
+ const members = av.M;
296
+ let copy;
297
+ for (const name in members) {
298
+ const nested = members[name];
299
+ const normalized = normalizeAttributeValueSync(nested, ctx, `${path}.${name}`);
300
+ if (normalized instanceof ValidationError)
301
+ return normalized;
302
+ if (normalized !== nested) {
303
+ copy ??= { ...members };
304
+ copy[name] = normalized;
305
+ }
306
+ }
307
+ return copy === undefined ? av : { M: copy };
308
+ }
309
+ if ("L" in av && Array.isArray(av.L)) {
310
+ const elements = av.L;
311
+ let copy;
312
+ for (let i = 0; i < elements.length; i++) {
313
+ const nested = elements[i];
314
+ const normalized = normalizeAttributeValueSync(nested, ctx, `${path}[${i}]`);
315
+ if (normalized instanceof ValidationError)
316
+ return normalized;
317
+ if (normalized !== nested) {
318
+ copy ??= [...elements];
319
+ copy[i] = normalized;
320
+ }
321
+ }
322
+ return copy === undefined ? av : { L: copy };
323
+ }
324
+ return av;
325
+ };
326
+ /** The form of `image` — `options.form`, or detected (see {@link isAttributeValueImage}). */
327
+ export const imageForm = (image, options) => options?.form ??
328
+ (typeof image === "object" &&
329
+ image !== null &&
330
+ !Array.isArray(image) &&
331
+ isAttributeValueImage(image, options?.probe)
332
+ ? "attributeValues"
333
+ : "unmarshalled");
334
+ /**
335
+ * {@link normalizeImage} without an Effect: the unmarshalled item, or the
336
+ * `ValidationError` it fails with (returned, never thrown).
337
+ */
338
+ export const normalizeImageSync = (image, ctx, options) => {
339
+ if (typeof image !== "object" || image === null || Array.isArray(image)) {
340
+ return imageError(ctx, "A stream image must be an object (DynamoDB JSON or an unmarshalled item)");
341
+ }
342
+ if (imageForm(image, options) === "unmarshalled") {
343
+ return { ...image };
344
+ }
345
+ const source = image;
346
+ let normalized;
347
+ for (const attribute in source) {
348
+ const value = source[attribute];
349
+ const av = normalizeAttributeValueSync(value, ctx, attribute);
350
+ if (av instanceof ValidationError)
351
+ return av;
352
+ if (av !== value) {
353
+ normalized ??= { ...source };
354
+ normalized[attribute] = av;
355
+ }
356
+ }
357
+ try {
358
+ return fromAttributeMap(normalized ?? source);
359
+ }
360
+ catch (cause) {
361
+ return new ValidationError({ entityType: ctx.entityType, operation: ctx.operation, cause });
362
+ }
363
+ };
364
+ /**
365
+ * Normalise a DynamoDB Streams image to an unmarshalled item.
366
+ *
367
+ * - DynamoDB JSON (see {@link isAttributeValueImage}): `B` / `BS` members given
368
+ * as base64 text are decoded at any depth (Lambda and EventBridge Pipes
369
+ * deliver them that way; `unmarshall` does not decode them), then the image is
370
+ * unmarshalled.
371
+ * - Already unmarshalled: returned as a shallow copy. A packed attribute's `B`
372
+ * may still be base64 text there — {@link unpackImageItem} handles it.
373
+ */
374
+ export const normalizeImage = (image, ctx, options) => Effect.suspend(() => fromStep(normalizeImageSync(image, ctx, options)));
375
+ /**
376
+ * {@link unpackItemSync} for a normalised **unmarshalled** stream image: on a
377
+ * listed attribute, a string that is base64 of a `B` value (it starts with
378
+ * `I`–`P`, which JSON text never does — see `PackedCodec.isBase64PackedText`)
379
+ * is decoded to bytes and read as `B`; any other string is JSON.
380
+ */
381
+ export const unpackImageItemSync = (raw, attributes, registry, ctx) => {
382
+ for (const attribute of attributes) {
383
+ let value = raw[attribute];
384
+ if (typeof value === "string" && isBase64PackedText(value)) {
385
+ const bytes = toBytesSync(value, ctx, attribute);
386
+ if (bytes instanceof ValidationError)
387
+ return bytes;
388
+ const wire = unpackBytesSync(bytes, registry, ctx, attribute);
389
+ if (wire instanceof ValidationError)
390
+ return wire;
391
+ raw[attribute] = wire;
392
+ continue;
393
+ }
394
+ if (typeof value !== "string" && !(value instanceof Uint8Array))
395
+ continue;
396
+ value = unpackStoredSync(value, registry, ctx, attribute);
397
+ if (value instanceof ValidationError)
398
+ return value;
399
+ raw[attribute] = value;
400
+ }
401
+ return undefined;
402
+ };
403
+ /** {@link unpackImageItemSync} as an Effect. */
404
+ export const unpackImageItem = (raw, attributes, registry, ctx) => {
405
+ const failure = unpackImageItemSync(raw, attributes, registry, ctx);
406
+ return failure === undefined ? Effect.void : Effect.fail(failure);
407
+ };
408
+ /**
409
+ * {@link decodeImage} without an Effect, with an explicit registry: the
410
+ * unmarshalled item, or the `ValidationError` it fails with.
411
+ */
412
+ export const decodeImageSync = (image, attributes, registry, ctx, options) => {
413
+ const form = imageForm(image, options);
414
+ const raw = normalizeImageSync(image, ctx, { ...options, form });
415
+ if (raw instanceof ValidationError)
416
+ return raw;
417
+ // In DynamoDB JSON the S / B tag says what each value is; unmarshalled, a
418
+ // string that is base64 of a `B` value is told apart from JSON text.
419
+ const failure = form === "attributeValues"
420
+ ? unpackItemSync(raw, attributes, registry, ctx)
421
+ : unpackImageItemSync(raw, attributes, registry, ctx);
422
+ return failure ?? raw;
423
+ };
424
+ /**
425
+ * The stream-image decode primitive: normalise `image` and unpack its
426
+ * dispatch-eligible `attributes` (stored names), returning the unmarshalled
427
+ * item with each packed value as its JSON wire form — ready for the same
428
+ * sparse rebuild, renames and schema decode a table read applies. One Effect
429
+ * per image; the work inside is synchronous.
430
+ *
431
+ * In DynamoDB JSON the `S` / `B` tag says what each value is; in an
432
+ * unmarshalled image a string on a listed attribute is read as base64 `B`
433
+ * when it starts with `I`–`P` (see {@link unpackImageItemSync}).
434
+ */
435
+ export const decodeImage = (image, attributes, ctx, options) => withRegistry((registry) => decodeImageSync(image, attributes, registry, ctx, options));
436
+ /** Render path segments as written: `data.items[0].sku`. */
437
+ export const formatPath = (segments) => segments
438
+ .map((segment, i) => typeof segment === "number" ? `[${segment}]` : i === 0 ? segment : `.${segment}`)
439
+ .join("");
440
+ const OPERATOR_NAMES = {
441
+ eq: "=",
442
+ ne: "<>",
443
+ lt: "<",
444
+ lte: "<=",
445
+ gt: ">",
446
+ gte: ">=",
447
+ between: "BETWEEN",
448
+ in: "IN",
449
+ type: "attribute_type",
450
+ beginsWith: "begins_with",
451
+ contains: "contains",
452
+ };
453
+ const refusal = (guard, usage, path, problem) => `[EDD-9076] ${guard.owner}: the ${usage} ${problem}, but \`${guard.attribute}\` is stored packed ` +
454
+ `(format "${guard.storedAs.format}"): DynamoDB sees one opaque string or binary value and ` +
455
+ "cannot evaluate a path inside it, or compare it — only attribute_exists / " +
456
+ `attribute_not_exists apply to it as a whole. ${guard.alternative(path)}`;
457
+ /**
458
+ * Why `expr` cannot be sent against an item with these packed attributes, or
459
+ * `undefined` when it can: it addresses a path inside one, or applies anything
460
+ * but `attribute_exists` / `attribute_not_exists` to one (`size()` included).
461
+ * `usage` names the expression in the message (`"filter"`, `"condition"`).
462
+ */
463
+ export const packedExprRefusal = (expr, guards, usage) => {
464
+ if (guards.length === 0)
465
+ return undefined;
466
+ const guardFor = (segments) => {
467
+ const head = segments[0];
468
+ return typeof head === "string" ? guards.find((g) => g.attribute === head) : undefined;
469
+ };
470
+ const check = (operand, operator) => {
471
+ if (operand._tag === "value")
472
+ return undefined;
473
+ const guard = guardFor(operand.segments);
474
+ if (guard === undefined)
475
+ return undefined;
476
+ const path = formatPath(operand.segments);
477
+ if (operand._tag === "size") {
478
+ return refusal(guard, usage, path, `applies size() to "${path}"`);
479
+ }
480
+ if (operand.segments.length > 1) {
481
+ return refusal(guard, usage, path, `addresses "${path}"`);
482
+ }
483
+ return refusal(guard, usage, path, `applies ${operator} to "${path}"`);
484
+ };
485
+ const walk = (node) => {
486
+ switch (node._tag) {
487
+ case "and":
488
+ case "or": {
489
+ for (const part of node.exprs) {
490
+ const found = walk(part);
491
+ if (found !== undefined)
492
+ return found;
493
+ }
494
+ return undefined;
495
+ }
496
+ case "not":
497
+ return walk(node.expr);
498
+ case "exists":
499
+ case "notExists": {
500
+ const operand = node.operand;
501
+ // The whole attribute: whether it is stored at all is answerable.
502
+ if (operand._tag === "path" && operand.segments.length === 1)
503
+ return undefined;
504
+ return check(operand, node._tag === "exists" ? "attribute_exists" : "attribute_not_exists");
505
+ }
506
+ case "eq":
507
+ case "ne":
508
+ case "lt":
509
+ case "lte":
510
+ case "gt":
511
+ case "gte":
512
+ return (check(node.left, OPERATOR_NAMES[node._tag]) ??
513
+ check(node.right, OPERATOR_NAMES[node._tag]));
514
+ case "between":
515
+ return (check(node.operand, "BETWEEN") ??
516
+ check(node.low, "BETWEEN") ??
517
+ check(node.high, "BETWEEN"));
518
+ case "in": {
519
+ const found = check(node.operand, "IN");
520
+ if (found !== undefined)
521
+ return found;
522
+ for (const value of node.values) {
523
+ const inner = check(value, "IN");
524
+ if (inner !== undefined)
525
+ return inner;
526
+ }
527
+ return undefined;
528
+ }
529
+ case "type":
530
+ return check(node.operand, "attribute_type");
531
+ case "beginsWith":
532
+ return check(node.operand, "begins_with") ?? check(node.prefix, "begins_with");
533
+ case "contains":
534
+ return check(node.operand, "contains") ?? check(node.value, "contains");
535
+ }
536
+ };
537
+ return walk(expr);
538
+ };
539
+ /**
540
+ * Why a projection cannot be sent against an item with these packed
541
+ * attributes, or `undefined` when it can: a packed attribute may be projected
542
+ * only whole. `paths` are segment lists (`selectPaths`); a dotted or indexed
543
+ * name given to `select` (`"data.amount"`, `"data[0]"`) is read as the path it
544
+ * spells.
545
+ */
546
+ export const packedProjectionRefusal = (paths, guards) => {
547
+ if (guards.length === 0)
548
+ return undefined;
549
+ for (const segments of paths) {
550
+ const head = segments[0];
551
+ if (typeof head !== "string")
552
+ continue;
553
+ let guard;
554
+ let nested = segments.length > 1;
555
+ if (segments.length === 1) {
556
+ // `select(["data.amount"])` — the name as a path.
557
+ guard = guards.find((g) => head.startsWith(`${g.attribute}.`) || head.startsWith(`${g.attribute}[`));
558
+ nested = guard !== undefined;
559
+ }
560
+ else {
561
+ guard = guards.find((g) => g.attribute === head);
562
+ }
563
+ if (guard === undefined || !nested)
564
+ continue;
565
+ const path = segments.length === 1 ? head : formatPath(segments);
566
+ return (`[EDD-9076] ${guard.owner}: the projection names "${path}", but \`${guard.attribute}\` is ` +
567
+ `stored packed (format "${guard.storedAs.format}"): DynamoDB holds it as one opaque string or ` +
568
+ `binary value and cannot project a path inside it. Select "${guard.attribute}" whole — a ` +
569
+ "projected packed attribute comes back as its parsed JSON.");
570
+ }
571
+ return undefined;
572
+ };
573
+ /**
574
+ * The raw-row hook of one entity type's projections (`Query.select` /
575
+ * `selectPaths`): reads `attributes` of the type's **own** rows by their stored
576
+ * form, and hands every other row back untouched. `ignoreOwnership()` lets
577
+ * rows of other types through, and their plain strings must never be parsed
578
+ * as packed JSON. The type is read from `__edd_e__`, borrowed into the
579
+ * projection (and stripped again) when the caller did not select it; a row
580
+ * without it is not attributable, so it is left as is. No hook when there is
581
+ * nothing to read, and none on a projection that names none of `attributes`.
582
+ */
583
+ export const ownRowsRawRow = (entityType, attributes, unpack) => attributes !== undefined && attributes.length === 0
584
+ ? undefined
585
+ : Object.assign((raw) => raw.__edd_e__ === entityType ? Effect.as(unpack(raw), raw) : Effect.succeed(raw), { reads: ["__edd_e__"], attributes });
586
+ /** `Query.make` options for the queries of one entity: guards + raw-row unpack. */
587
+ export const entityPackedQueryOptions = (entity) => {
588
+ const unpack = entity._unpackFields;
589
+ return {
590
+ packed: entity._packedGuards ?? [],
591
+ rawRow: unpack === undefined
592
+ ? undefined
593
+ : ownRowsRawRow(entity.entityType, entity._dispatchAttributes, (raw) => unpack(raw)),
594
+ };
595
+ };
596
+ /**
597
+ * `Query.make` options for a query over several entities (a collection): every
598
+ * member's guards (a path packed in any member is refused, naming it), and a
599
+ * raw-row unpack by the member the row's `__edd_e__` names.
600
+ */
601
+ export const collectionPackedQueryOptions = (members) => {
602
+ const byType = new Map(members.map((m) => [m.entityType, m]));
603
+ const packed = members.flatMap((m) => m._packedGuards ?? []);
604
+ const unpacking = members.filter((m) => m._unpackFields !== undefined && m._dispatchAttributes?.length !== 0);
605
+ if (unpacking.length === 0)
606
+ return { packed, rawRow: undefined };
607
+ // A row of a type that is not a member is never read (`ignoreOwnership()`).
608
+ const rawRow = (raw) => {
609
+ const member = byType.get(raw.__edd_e__);
610
+ const unpack = member?._unpackFields;
611
+ return unpack === undefined ? Effect.succeed(raw) : Effect.as(unpack(raw), raw);
612
+ };
613
+ // The member is named by `__edd_e__`, so a projection that does not select
614
+ // it still reads it (borrowed, then stripped — see `Query.QueryRawRow`); a
615
+ // projection naming none of the members' read attributes runs no hook.
616
+ const attributes = unpacking.every((m) => m._dispatchAttributes !== undefined)
617
+ ? [...new Set(unpacking.flatMap((m) => m._dispatchAttributes ?? []))]
618
+ : undefined;
619
+ return { packed, rawRow: Object.assign(rawRow, { reads: ["__edd_e__"], attributes }) };
620
+ };
621
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
622
+ /**
623
+ * {@link packedExprRefusal} for an already-compiled expression
624
+ * (`Transaction.check`'s `ExpressionResult`): every placeholder naming a packed
625
+ * attribute must appear as the whole operand of `attribute_exists(...)` /
626
+ * `attribute_not_exists(...)` — never with a path below it or under another
627
+ * operator. A placeholder used as a nested segment (`#a.#b`) is another
628
+ * attribute's path, and is left alone.
629
+ */
630
+ export const packedCompiledRefusal = (expression, names, guards, usage) => {
631
+ if (guards.length === 0)
632
+ return undefined;
633
+ for (const [placeholder, name] of Object.entries(names)) {
634
+ const guard = guards.find((g) => g.attribute === name);
635
+ if (guard === undefined)
636
+ continue;
637
+ const occurrences = new RegExp(`${escapeRegExp(placeholder)}(?![A-Za-z0-9_])`, "g");
638
+ for (const match of expression.matchAll(occurrences)) {
639
+ const start = match.index;
640
+ const before = expression.slice(0, start);
641
+ if (/[.\]]\s*$/.test(before))
642
+ continue; // a nested segment of another path
643
+ const after = expression.slice(start + placeholder.length);
644
+ if (/^\s*[.[]/.test(after)) {
645
+ return refusal(guard, usage, name, `addresses a path inside "${name}"`);
646
+ }
647
+ if (/attribute_(?:not_)?exists\(\s*$/.test(before) && /^\s*\)/.test(after))
648
+ continue;
649
+ return refusal(guard, usage, name, `applies an operator other than attribute_exists to "${name}"`);
650
+ }
651
+ }
652
+ return undefined;
653
+ };
654
+ //# sourceMappingURL=Packed.js.map