dynamodb-expression-builder 0.1.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/index.cjs ADDED
@@ -0,0 +1,1908 @@
1
+
2
+ //#region src/types.ts
3
+ /** The set-typed tags, which carry members in `values` rather than `value`. */
4
+ const SET_TYPES = [
5
+ "SS",
6
+ "NS",
7
+ "BS"
8
+ ];
9
+ function isSetType(type) {
10
+ return type === "SS" || type === "NS" || type === "BS";
11
+ }
12
+ /**
13
+ * The scalar element type of a set tag (SS→S, NS→N, BS→B); identity for every
14
+ * scalar. `contains(path, operand)` takes a SCALAR operand even when `path` is a
15
+ * set (it tests membership), so the set-typed tag must marshal its ELEMENT type
16
+ * there — never `{SS:[…]}`, which DynamoDB rejects.
17
+ */
18
+ function elementType(type) {
19
+ switch (type) {
20
+ case "SS": return "S";
21
+ case "NS": return "N";
22
+ case "BS": return "B";
23
+ default: return type;
24
+ }
25
+ }
26
+ /**
27
+ * Build a {@link TypedValue} from a tag + raw entry. Set types take an explicit
28
+ * member array (falling back to the single `value` as one member); scalars take
29
+ * the string; NULL ignores the payload.
30
+ */
31
+ function makeTypedValue(type, value, values) {
32
+ if (isSetType(type)) return {
33
+ type,
34
+ value: "",
35
+ values: values ?? (value ? [value] : [])
36
+ };
37
+ if (type === "NULL") return {
38
+ type,
39
+ value: ""
40
+ };
41
+ return {
42
+ type,
43
+ value
44
+ };
45
+ }
46
+
47
+ //#endregion
48
+ //#region src/operators.ts
49
+ const ALL_SCAN_TYPES = [
50
+ "S",
51
+ "N",
52
+ "B",
53
+ "SS",
54
+ "NS",
55
+ "BS",
56
+ "BOOL",
57
+ "NULL",
58
+ "L",
59
+ "M"
60
+ ];
61
+ const COLLECTION_TYPES = [
62
+ "SS",
63
+ "NS",
64
+ "BS",
65
+ "L",
66
+ "M"
67
+ ];
68
+ const FILTER_OPERATORS = [
69
+ {
70
+ value: "=",
71
+ label: "Equals (=)",
72
+ symbol: "=",
73
+ wireForm: "EQ",
74
+ requiresValue: true,
75
+ requiresValue2: false,
76
+ typeOptional: false,
77
+ keyAllowedTypes: [
78
+ "S",
79
+ "N",
80
+ "B"
81
+ ],
82
+ scanAllowedTypes: ALL_SCAN_TYPES
83
+ },
84
+ {
85
+ value: "<>",
86
+ label: "Not Equals (≠)",
87
+ symbol: "≠",
88
+ wireForm: "NE",
89
+ requiresValue: true,
90
+ requiresValue2: false,
91
+ typeOptional: false,
92
+ keyAllowedTypes: [],
93
+ scanAllowedTypes: ALL_SCAN_TYPES
94
+ },
95
+ {
96
+ value: "<",
97
+ label: "Less Than (<)",
98
+ symbol: "<",
99
+ wireForm: "LT",
100
+ requiresValue: true,
101
+ requiresValue2: false,
102
+ typeOptional: false,
103
+ keyAllowedTypes: ["N"],
104
+ scanAllowedTypes: ["N"]
105
+ },
106
+ {
107
+ value: "<=",
108
+ label: "Less Than or Equal (≤)",
109
+ symbol: "≤",
110
+ wireForm: "LE",
111
+ requiresValue: true,
112
+ requiresValue2: false,
113
+ typeOptional: false,
114
+ keyAllowedTypes: ["N"],
115
+ scanAllowedTypes: ["N"]
116
+ },
117
+ {
118
+ value: ">",
119
+ label: "Greater Than (>)",
120
+ symbol: ">",
121
+ wireForm: "GT",
122
+ requiresValue: true,
123
+ requiresValue2: false,
124
+ typeOptional: false,
125
+ keyAllowedTypes: ["N"],
126
+ scanAllowedTypes: ["N"]
127
+ },
128
+ {
129
+ value: ">=",
130
+ label: "Greater Than or Equal (≥)",
131
+ symbol: "≥",
132
+ wireForm: "GE",
133
+ requiresValue: true,
134
+ requiresValue2: false,
135
+ typeOptional: false,
136
+ keyAllowedTypes: ["N"],
137
+ scanAllowedTypes: ["N"]
138
+ },
139
+ {
140
+ value: "contains",
141
+ label: "Contains",
142
+ symbol: "∋",
143
+ wireForm: "CONTAINS",
144
+ requiresValue: true,
145
+ requiresValue2: false,
146
+ typeOptional: false,
147
+ keyAllowedTypes: [],
148
+ scanAllowedTypes: [
149
+ "S",
150
+ "B",
151
+ "SS",
152
+ "NS",
153
+ "BS",
154
+ "L"
155
+ ]
156
+ },
157
+ {
158
+ value: "begins_with",
159
+ label: "Begins With",
160
+ symbol: "^",
161
+ wireForm: "BEGINS_WITH",
162
+ requiresValue: true,
163
+ requiresValue2: false,
164
+ typeOptional: false,
165
+ keyAllowedTypes: ["S", "B"],
166
+ scanAllowedTypes: ["S", "B"]
167
+ },
168
+ {
169
+ value: "between",
170
+ label: "Between",
171
+ symbol: "↔",
172
+ wireForm: "BETWEEN",
173
+ requiresValue: true,
174
+ requiresValue2: true,
175
+ typeOptional: false,
176
+ keyAllowedTypes: ["N"],
177
+ scanAllowedTypes: ["N"]
178
+ },
179
+ {
180
+ value: "in",
181
+ label: "In",
182
+ symbol: "∈",
183
+ wireForm: "IN",
184
+ requiresValue: true,
185
+ requiresValue2: false,
186
+ typeOptional: false,
187
+ keyAllowedTypes: [],
188
+ scanAllowedTypes: ["S", "N"]
189
+ },
190
+ {
191
+ value: "exists",
192
+ label: "Attribute Exists",
193
+ symbol: "∃",
194
+ wireForm: "EXISTS",
195
+ requiresValue: false,
196
+ requiresValue2: false,
197
+ typeOptional: true,
198
+ keyAllowedTypes: [],
199
+ scanAllowedTypes: ALL_SCAN_TYPES
200
+ },
201
+ {
202
+ value: "not_exists",
203
+ label: "Attribute Not Exists",
204
+ symbol: "∄",
205
+ wireForm: "NOT_EXISTS",
206
+ requiresValue: false,
207
+ requiresValue2: false,
208
+ typeOptional: true,
209
+ keyAllowedTypes: [],
210
+ scanAllowedTypes: ALL_SCAN_TYPES
211
+ },
212
+ {
213
+ value: "size_eq",
214
+ label: "Size Equals",
215
+ symbol: "size =",
216
+ wireForm: "SIZE_EQ",
217
+ requiresValue: true,
218
+ requiresValue2: false,
219
+ typeOptional: true,
220
+ keyAllowedTypes: [],
221
+ scanAllowedTypes: COLLECTION_TYPES
222
+ },
223
+ {
224
+ value: "size_ne",
225
+ label: "Size Not Equals",
226
+ symbol: "size ≠",
227
+ wireForm: "SIZE_NE",
228
+ requiresValue: true,
229
+ requiresValue2: false,
230
+ typeOptional: true,
231
+ keyAllowedTypes: [],
232
+ scanAllowedTypes: COLLECTION_TYPES
233
+ },
234
+ {
235
+ value: "size_lt",
236
+ label: "Size Less Than",
237
+ symbol: "size <",
238
+ wireForm: "SIZE_LT",
239
+ requiresValue: true,
240
+ requiresValue2: false,
241
+ typeOptional: true,
242
+ keyAllowedTypes: [],
243
+ scanAllowedTypes: COLLECTION_TYPES
244
+ },
245
+ {
246
+ value: "size_le",
247
+ label: "Size Less Than or Equal",
248
+ symbol: "size ≤",
249
+ wireForm: "SIZE_LE",
250
+ requiresValue: true,
251
+ requiresValue2: false,
252
+ typeOptional: true,
253
+ keyAllowedTypes: [],
254
+ scanAllowedTypes: COLLECTION_TYPES
255
+ },
256
+ {
257
+ value: "size_gt",
258
+ label: "Size Greater Than",
259
+ symbol: "size >",
260
+ wireForm: "SIZE_GT",
261
+ requiresValue: true,
262
+ requiresValue2: false,
263
+ typeOptional: true,
264
+ keyAllowedTypes: [],
265
+ scanAllowedTypes: COLLECTION_TYPES
266
+ },
267
+ {
268
+ value: "size_ge",
269
+ label: "Size Greater Than or Equal",
270
+ symbol: "size ≥",
271
+ wireForm: "SIZE_GE",
272
+ requiresValue: true,
273
+ requiresValue2: false,
274
+ typeOptional: true,
275
+ keyAllowedTypes: [],
276
+ scanAllowedTypes: COLLECTION_TYPES
277
+ },
278
+ {
279
+ value: "type_eq",
280
+ label: "Type Equals",
281
+ symbol: "type =",
282
+ wireForm: "TYPE_EQ",
283
+ requiresValue: true,
284
+ requiresValue2: false,
285
+ typeOptional: false,
286
+ keyAllowedTypes: [],
287
+ scanAllowedTypes: ALL_SCAN_TYPES
288
+ },
289
+ {
290
+ value: "type_ne",
291
+ label: "Type Not Equals",
292
+ symbol: "type ≠",
293
+ wireForm: "TYPE_NE",
294
+ requiresValue: true,
295
+ requiresValue2: false,
296
+ typeOptional: false,
297
+ keyAllowedTypes: [],
298
+ scanAllowedTypes: ALL_SCAN_TYPES
299
+ }
300
+ ];
301
+ /**
302
+ * O(1) registry lookup. Loose-keyed (`Record<string, OperatorDef>`) so boundary
303
+ * callers must guard `if (!def)` — the runtime value is `undefined` for unknown
304
+ * keys.
305
+ */
306
+ const OPERATOR_BY_VALUE = Object.fromEntries(FILTER_OPERATORS.map((op) => [op.value, op]));
307
+ /** Key-eligible operators (KeyConditionExpression compatible). */
308
+ const KEY_OPERATORS = FILTER_OPERATORS.filter((op) => op.keyAllowedTypes.length > 0);
309
+ /**
310
+ * Operators applicable to a scan value of the given type. Undefined type → full
311
+ * list. Filters/conditions use this.
312
+ */
313
+ function getCompatibleFilterOperators(type) {
314
+ if (!type) return FILTER_OPERATORS;
315
+ return FILTER_OPERATORS.filter((op) => op.scanAllowedTypes.includes(type));
316
+ }
317
+ /**
318
+ * Key-eligible operators compatible with the given key type. Undefined → full
319
+ * key list. Range-key UI uses this (type-gated: N gets `<`/`between`, S/B get
320
+ * `begins_with`, all keys get `=`).
321
+ */
322
+ function getCompatibleComparisonOperators(type) {
323
+ if (!type) return KEY_OPERATORS;
324
+ return KEY_OPERATORS.filter((op) => op.keyAllowedTypes.includes(type));
325
+ }
326
+
327
+ //#endregion
328
+ //#region src/filter-expressions.ts
329
+ /**
330
+ * Compile a list of predicate rows into one `… AND …` expression plus the
331
+ * fresh names/typedValues maps it references. Returns null for an empty list.
332
+ * Callers (buildRequest) merge the maps; this function never mutates shared
333
+ * state.
334
+ */
335
+ function buildFilterExpressions(rows, prefix) {
336
+ if (rows.length === 0) return null;
337
+ const names = {};
338
+ const typedValues = {};
339
+ return {
340
+ expression: rows.map((row, index) => buildOne(row, index, prefix, names, typedValues)).join(" AND "),
341
+ names,
342
+ typedValues
343
+ };
344
+ }
345
+ function buildOne(row, index, prefix, names, typedValues) {
346
+ const wire = OPERATOR_BY_VALUE[row.operator].wireForm;
347
+ const nameRef = `#${prefix}${index}`;
348
+ names[nameRef] = row.field;
349
+ const valueRef = `:${prefix}Value${index}`;
350
+ const valueRef2 = `:${prefix}Value${index}_2`;
351
+ switch (wire) {
352
+ case "EQ":
353
+ typedValues[valueRef] = single$1(row);
354
+ return `${nameRef} = ${valueRef}`;
355
+ case "NE":
356
+ typedValues[valueRef] = single$1(row);
357
+ return `${nameRef} <> ${valueRef}`;
358
+ case "GT":
359
+ typedValues[valueRef] = single$1(row);
360
+ return `${nameRef} > ${valueRef}`;
361
+ case "GE":
362
+ typedValues[valueRef] = single$1(row);
363
+ return `${nameRef} >= ${valueRef}`;
364
+ case "LT":
365
+ typedValues[valueRef] = single$1(row);
366
+ return `${nameRef} < ${valueRef}`;
367
+ case "LE":
368
+ typedValues[valueRef] = single$1(row);
369
+ return `${nameRef} <= ${valueRef}`;
370
+ case "CONTAINS":
371
+ typedValues[valueRef] = makeTypedValue(elementType(row.type), row.value);
372
+ return `contains(${nameRef}, ${valueRef})`;
373
+ case "BEGINS_WITH":
374
+ typedValues[valueRef] = single$1(row);
375
+ return `begins_with(${nameRef}, ${valueRef})`;
376
+ case "BETWEEN":
377
+ typedValues[valueRef] = makeTypedValue(row.type, row.value);
378
+ typedValues[valueRef2] = makeTypedValue(row.type, row.value2 ?? "");
379
+ return `${nameRef} BETWEEN ${valueRef} AND ${valueRef2}`;
380
+ case "IN": {
381
+ const members = row.values ?? (row.value ? [row.value] : []);
382
+ if (members.length === 0) throw new Error(`IN on "${row.field}" requires at least one value`);
383
+ return `${nameRef} IN (${members.map((member, i) => {
384
+ const ref = `:${prefix}Value${index}_${i}`;
385
+ typedValues[ref] = makeTypedValue(row.type, member);
386
+ return ref;
387
+ }).join(", ")})`;
388
+ }
389
+ case "EXISTS": return `attribute_exists(${nameRef})`;
390
+ case "NOT_EXISTS": return `attribute_not_exists(${nameRef})`;
391
+ case "SIZE_EQ":
392
+ typedValues[valueRef] = single$1(row);
393
+ return `size(${nameRef}) = ${valueRef}`;
394
+ case "SIZE_NE":
395
+ typedValues[valueRef] = single$1(row);
396
+ return `size(${nameRef}) <> ${valueRef}`;
397
+ case "SIZE_LT":
398
+ typedValues[valueRef] = single$1(row);
399
+ return `size(${nameRef}) < ${valueRef}`;
400
+ case "SIZE_LE":
401
+ typedValues[valueRef] = single$1(row);
402
+ return `size(${nameRef}) <= ${valueRef}`;
403
+ case "SIZE_GT":
404
+ typedValues[valueRef] = single$1(row);
405
+ return `size(${nameRef}) > ${valueRef}`;
406
+ case "SIZE_GE":
407
+ typedValues[valueRef] = single$1(row);
408
+ return `size(${nameRef}) >= ${valueRef}`;
409
+ case "TYPE_EQ":
410
+ typedValues[valueRef] = single$1(row);
411
+ return `attribute_type(${nameRef}, ${valueRef})`;
412
+ case "TYPE_NE":
413
+ typedValues[valueRef] = single$1(row);
414
+ return `NOT attribute_type(${nameRef}, ${valueRef})`;
415
+ }
416
+ }
417
+ /** Capture the row's single RHS value as a typed value (set- or scalar-aware). */
418
+ function single$1(row) {
419
+ return makeTypedValue(row.type, row.value, row.values);
420
+ }
421
+
422
+ //#endregion
423
+ //#region src/key-command.ts
424
+ /**
425
+ * Build a Query KeyConditionExpression from a hash key (always EQ) plus an
426
+ * optional range-key condition. Captures value types.
427
+ */
428
+ function buildKeyConditionExpression(hashKey, rangeKey) {
429
+ const names = { "#hashKey": hashKey.field };
430
+ const typedValues = { ":hashKeyValue": makeTypedValue(hashKey.type, hashKey.value) };
431
+ let expression = "#hashKey = :hashKeyValue";
432
+ if (rangeKey) {
433
+ names["#rangeKey"] = rangeKey.field;
434
+ const wire = OPERATOR_BY_VALUE[rangeKey.operator].wireForm;
435
+ if (wire === "BETWEEN") {
436
+ typedValues[":rangeKeyValue"] = makeTypedValue(rangeKey.type, rangeKey.value);
437
+ typedValues[":rangeKeyValue2"] = makeTypedValue(rangeKey.type, rangeKey.value2 ?? "");
438
+ expression += " AND #rangeKey BETWEEN :rangeKeyValue AND :rangeKeyValue2";
439
+ } else if (wire === "BEGINS_WITH") {
440
+ typedValues[":rangeKeyValue"] = makeTypedValue(rangeKey.type, rangeKey.value);
441
+ expression += " AND begins_with(#rangeKey, :rangeKeyValue)";
442
+ } else {
443
+ const symbol = SYMBOL_BY_WIRE[wire];
444
+ if (!symbol) throw new Error(`Operator ${rangeKey.operator} is not key-eligible`);
445
+ typedValues[":rangeKeyValue"] = makeTypedValue(rangeKey.type, rangeKey.value);
446
+ expression += ` AND #rangeKey ${symbol} :rangeKeyValue`;
447
+ }
448
+ }
449
+ return {
450
+ expression,
451
+ names,
452
+ typedValues
453
+ };
454
+ }
455
+ /**
456
+ * Comparison symbols for the simple key operators. Partial on purpose: a
457
+ * range-key row carries an `OperatorValue` from the full set, so a non-key
458
+ * operator (e.g. `contains`) can reach here at runtime — the lookup returns
459
+ * undefined and the caller throws rather than emitting a malformed expression.
460
+ */
461
+ const SYMBOL_BY_WIRE = {
462
+ EQ: "=",
463
+ GT: ">",
464
+ GE: ">=",
465
+ LT: "<",
466
+ LE: "<="
467
+ };
468
+ /**
469
+ * Build the plain typed Key map (GetItem/Update/Delete) — real attribute names
470
+ * mapped to typed values, no expression placeholders.
471
+ */
472
+ function buildKeyMap(keys) {
473
+ const map = {};
474
+ for (const k of keys) map[k.field] = makeTypedValue(k.type, k.value);
475
+ return map;
476
+ }
477
+
478
+ //#endregion
479
+ //#region src/build-update-expression.ts
480
+ /** DynamoDB clause order. SET, REMOVE, ADD, DELETE — each emitted at most once. */
481
+ const CLAUSE_ORDER = [
482
+ "SET",
483
+ "REMOVE",
484
+ "ADD",
485
+ "DELETE"
486
+ ];
487
+ /**
488
+ * Compile update actions into one UpdateExpression. Returns null for an empty
489
+ * list. Callers (buildRequest) merge the returned maps; this never mutates
490
+ * shared state. Actions are grouped by kind into the canonical
491
+ * `SET … REMOVE … ADD … DELETE …` order while preserving each action's input
492
+ * order within its clause.
493
+ */
494
+ function buildUpdateExpression(actions) {
495
+ if (actions.length === 0) return null;
496
+ const names = {};
497
+ const typedValues = {};
498
+ const clauses = {
499
+ SET: [],
500
+ REMOVE: [],
501
+ ADD: [],
502
+ DELETE: []
503
+ };
504
+ actions.forEach((action, index) => {
505
+ const nameRef = `#upd${index}`;
506
+ names[nameRef] = action.field;
507
+ const valueRef = `:updValue${index}`;
508
+ switch (action.kind) {
509
+ case "SET":
510
+ clauses.SET.push(compileSet(action, nameRef, valueRef, typedValues));
511
+ break;
512
+ case "REMOVE":
513
+ clauses.REMOVE.push(action.index === void 0 ? nameRef : `${nameRef}[${action.index}]`);
514
+ break;
515
+ case "ADD":
516
+ typedValues[valueRef] = requireValue(action);
517
+ clauses.ADD.push(`${nameRef} ${valueRef}`);
518
+ break;
519
+ case "DELETE":
520
+ typedValues[valueRef] = requireValue(action);
521
+ clauses.DELETE.push(`${nameRef} ${valueRef}`);
522
+ break;
523
+ }
524
+ });
525
+ return {
526
+ expression: CLAUSE_ORDER.filter((kind) => clauses[kind].length > 0).map((kind) => `${kind} ${clauses[kind].join(", ")}`).join(" "),
527
+ names,
528
+ typedValues
529
+ };
530
+ }
531
+ /** Compile one SET action's RHS per its idiom, capturing the typed value. */
532
+ function compileSet(action, nameRef, valueRef, typedValues) {
533
+ typedValues[valueRef] = requireValue(action);
534
+ switch (action.setOp ?? "assign") {
535
+ case "assign": return `${nameRef} = ${valueRef}`;
536
+ case "if_not_exists": return `${nameRef} = if_not_exists(${nameRef}, ${valueRef})`;
537
+ case "add": return `${nameRef} = ${nameRef} + ${valueRef}`;
538
+ case "subtract": return `${nameRef} = ${nameRef} - ${valueRef}`;
539
+ case "list_append": return `${nameRef} = list_append(${nameRef}, ${valueRef})`;
540
+ case "list_prepend": return `${nameRef} = list_append(${valueRef}, ${nameRef})`;
541
+ }
542
+ }
543
+ /** Fail loud: a value-bearing action (SET/ADD/DELETE) must carry a value. */
544
+ function requireValue(action) {
545
+ if (!action.value) throw new Error(`Update action ${action.kind} on "${action.field}" requires a value`);
546
+ return action.value;
547
+ }
548
+
549
+ //#endregion
550
+ //#region src/build-request.ts
551
+ /**
552
+ * Assemble a {@link CanonicalRequest} from a builder config. Pure: reads only the
553
+ * config sub-parts the operation uses, merges the sub-builders' maps, and omits
554
+ * any map that ended up empty. Throws (fail loud) when a required part is missing
555
+ * (a Query without a hash key) rather than emitting a malformed request.
556
+ */
557
+ function buildRequest(config) {
558
+ const names = {};
559
+ const typedValues = {};
560
+ const request = {
561
+ operation: config.operation,
562
+ tableName: config.tableName,
563
+ config
564
+ };
565
+ if (config.indexName) request.indexName = config.indexName;
566
+ switch (config.operation) {
567
+ case "GetItem":
568
+ assignKey(request, config);
569
+ applyProjection(request, config, names);
570
+ if (config.consistentRead) request.consistentRead = true;
571
+ break;
572
+ case "Query": {
573
+ if (!config.hashKey) throw new Error("Query requires a hash key");
574
+ const kc = buildKeyConditionExpression(config.hashKey, config.rangeKey);
575
+ request.keyConditionExpression = kc.expression;
576
+ Object.assign(names, kc.names);
577
+ Object.assign(typedValues, kc.typedValues);
578
+ applyFilter(request, config, names, typedValues);
579
+ applyProjection(request, config, names);
580
+ applyReadOptions(request, config);
581
+ if (config.scanIndexForward === false) request.scanIndexForward = false;
582
+ break;
583
+ }
584
+ case "Scan":
585
+ applyFilter(request, config, names, typedValues);
586
+ applyProjection(request, config, names);
587
+ applyReadOptions(request, config);
588
+ break;
589
+ case "Update": {
590
+ assignKey(request, config);
591
+ const update = buildUpdateExpression(config.updates ?? []);
592
+ if (update) {
593
+ request.updateExpression = update.expression;
594
+ Object.assign(names, update.names);
595
+ Object.assign(typedValues, update.typedValues);
596
+ }
597
+ applyCondition(request, config, names, typedValues);
598
+ break;
599
+ }
600
+ case "Put": {
601
+ const item = buildItemMap(config.item ?? []);
602
+ if (Object.keys(item).length > 0) request.item = item;
603
+ applyCondition(request, config, names, typedValues);
604
+ break;
605
+ }
606
+ case "Delete":
607
+ assignKey(request, config);
608
+ applyCondition(request, config, names, typedValues);
609
+ break;
610
+ }
611
+ if (Object.keys(names).length > 0) request.names = names;
612
+ if (Object.keys(typedValues).length > 0) request.typedValues = typedValues;
613
+ assertNoEmptySets(request);
614
+ return request;
615
+ }
616
+ /**
617
+ * An empty set (`{SS:[]}`/`{NS:[]}`/`{BS:[]}`) is rejected by DynamoDB at runtime.
618
+ * Catch it at build time — mirroring the empty-`IN` guard in filter-expressions —
619
+ * so the tool never hands out a snippet that fails when run. PartiQL is emitted
620
+ * from the same request (which `emit()` builds first), so this covers every format.
621
+ */
622
+ function assertNoEmptySets(request) {
623
+ for (const map of [
624
+ request.key,
625
+ request.item,
626
+ request.typedValues,
627
+ request.exclusiveStartKey
628
+ ]) {
629
+ if (!map) continue;
630
+ for (const tv of Object.values(map)) if (isSetType(tv.type) && (tv.values?.length ?? 0) === 0) throw new Error("a set value (SS/NS/BS) requires at least one member");
631
+ }
632
+ }
633
+ /**
634
+ * Attach the Query/Scan request-level read options: `Limit` (validated — a
635
+ * non-positive/non-integer limit fails loud rather than emitting `Limit: 0`),
636
+ * `ConsistentRead` (only `true` carried), and `ExclusiveStartKey` (typed key
637
+ * map, omitted when empty). `ScanIndexForward` is handled at the Query call
638
+ * site — it's Query-only.
639
+ */
640
+ function applyReadOptions(request, config) {
641
+ if (config.limit !== void 0) {
642
+ if (!Number.isInteger(config.limit) || config.limit <= 0) throw new Error("Limit must be a positive integer");
643
+ request.limit = config.limit;
644
+ }
645
+ if (config.consistentRead) request.consistentRead = true;
646
+ const startKey = buildKeyMap(config.exclusiveStartKey ?? []);
647
+ if (Object.keys(startKey).length > 0) request.exclusiveStartKey = startKey;
648
+ }
649
+ /** Attach the typed Key map (GetItem/Update/Delete), omitting it when empty. */
650
+ function assignKey(request, config) {
651
+ const key = buildKeyMap(config.key ?? []);
652
+ if (Object.keys(key).length > 0) request.key = key;
653
+ }
654
+ /** Compile filters into a FilterExpression + merge its maps (Query/Scan). */
655
+ function applyFilter(request, config, names, typedValues) {
656
+ const filter = buildFilterExpressions(config.filters ?? [], "filter");
657
+ if (!filter) return;
658
+ request.filterExpression = filter.expression;
659
+ Object.assign(names, filter.names);
660
+ Object.assign(typedValues, filter.typedValues);
661
+ }
662
+ /** Compile conditions into a ConditionExpression + merge its maps (write ops). */
663
+ function applyCondition(request, config, names, typedValues) {
664
+ const condition = buildFilterExpressions(config.conditions ?? [], "cond");
665
+ if (!condition) return;
666
+ request.conditionExpression = condition.expression;
667
+ Object.assign(names, condition.names);
668
+ Object.assign(typedValues, condition.typedValues);
669
+ }
670
+ /**
671
+ * Build a `#`-aliased ProjectionExpression (read ops only). Each projected
672
+ * attribute gets a fresh `#proj{i}` alias so reserved words are always safe and
673
+ * the aliases never collide with key/filter prefixes.
674
+ */
675
+ function applyProjection(request, config, names) {
676
+ const attrs = config.projection;
677
+ if (!attrs || attrs.length === 0) return;
678
+ request.projectionExpression = attrs.map((attr, index) => {
679
+ const ref = `#proj${index}`;
680
+ names[ref] = attr;
681
+ return ref;
682
+ }).join(", ");
683
+ }
684
+ /** Build the typed Put item map — real attribute names → typed values. */
685
+ function buildItemMap(items) {
686
+ const map = {};
687
+ for (const attr of items) map[attr.field] = makeTypedValue(attr.type, attr.value, attr.values);
688
+ return map;
689
+ }
690
+
691
+ //#endregion
692
+ //#region src/emit/marshal.ts
693
+ /** Build an AttributeValue from a value's type tag (not its runtime type). */
694
+ function typedValueToAv(tv) {
695
+ switch (tv.type) {
696
+ case "S": return { S: tv.value };
697
+ case "N": return { N: tv.value };
698
+ case "B": return { B: tv.value };
699
+ case "BOOL": return { BOOL: tv.value === "true" };
700
+ case "SS": return { SS: tv.values ?? [] };
701
+ case "NS": return { NS: tv.values ?? [] };
702
+ case "BS": return { BS: tv.values ?? [] };
703
+ case "NULL": return { NULL: true };
704
+ }
705
+ }
706
+ /** Marshal a whole placeholder/attr map (`:v`→typed, or name→typed) to AVs. */
707
+ function typedMapToAvMap(map) {
708
+ const out = {};
709
+ for (const [key, tv] of Object.entries(map)) out[key] = typedValueToAv(tv);
710
+ return out;
711
+ }
712
+ /**
713
+ * Does this request carry any binary (`B`/`BS`) value? The SDK v3 / boto3
714
+ * emitters use this to decide whether to emit binary-decoding (a `Uint8Array`
715
+ * expression / an `import base64`).
716
+ */
717
+ function hasBinaryValue(request) {
718
+ return [
719
+ request.key,
720
+ request.item,
721
+ request.typedValues,
722
+ request.exclusiveStartKey
723
+ ].some((map) => map !== void 0 && Object.values(map).some((tv) => tv.type === "B" || tv.type === "BS"));
724
+ }
725
+
726
+ //#endregion
727
+ //#region src/emit/sdk-v3.ts
728
+ /** Operation → the `@aws-sdk/client-dynamodb` command class name. */
729
+ const COMMAND_BY_OP = {
730
+ GetItem: "GetItemCommand",
731
+ Query: "QueryCommand",
732
+ Scan: "ScanCommand",
733
+ Update: "UpdateItemCommand",
734
+ Put: "PutItemCommand",
735
+ Delete: "DeleteItemCommand"
736
+ };
737
+ /** Build the command params object (key order matches typical SDK usage). */
738
+ function buildSdkV3Params(request) {
739
+ const p = { TableName: request.tableName };
740
+ if (request.indexName) p.IndexName = request.indexName;
741
+ if (request.key) p.Key = typedMapToAvMap(request.key);
742
+ if (request.item) p.Item = typedMapToAvMap(request.item);
743
+ if (request.keyConditionExpression) p.KeyConditionExpression = request.keyConditionExpression;
744
+ if (request.updateExpression) p.UpdateExpression = request.updateExpression;
745
+ if (request.conditionExpression) p.ConditionExpression = request.conditionExpression;
746
+ if (request.filterExpression) p.FilterExpression = request.filterExpression;
747
+ if (request.projectionExpression) p.ProjectionExpression = request.projectionExpression;
748
+ if (request.names) p.ExpressionAttributeNames = request.names;
749
+ if (request.typedValues) p.ExpressionAttributeValues = typedMapToAvMap(request.typedValues);
750
+ if (request.limit !== void 0) p.Limit = request.limit;
751
+ if (request.consistentRead) p.ConsistentRead = true;
752
+ if (request.scanIndexForward === false) p.ScanIndexForward = false;
753
+ if (request.exclusiveStartKey) p.ExclusiveStartKey = typedMapToAvMap(request.exclusiveStartKey);
754
+ return p;
755
+ }
756
+ /** JS expression decoding a base64 `B` member to the `Uint8Array` the client needs. */
757
+ function jsBinary$1(base64) {
758
+ return `Uint8Array.from(atob(${JSON.stringify(base64)}), (c) => c.charCodeAt(0))`;
759
+ }
760
+ /**
761
+ * Render a value as a pretty-printed (2-space) JS object literal, mirroring
762
+ * `JSON.stringify(v, null, 2)` except that a `B`/`BS` AttributeValue member is
763
+ * emitted as a `Uint8Array` expression rather than a base64 string.
764
+ */
765
+ function jsLiteral(value, indent) {
766
+ const pad = " ".repeat(indent);
767
+ const inner = " ".repeat(indent + 1);
768
+ if (Array.isArray(value)) {
769
+ if (value.length === 0) return "[]";
770
+ return `[\n${value.map((v) => inner + jsLiteral(v, indent + 1)).join(",\n")}\n${pad}]`;
771
+ }
772
+ if (value && typeof value === "object") {
773
+ const entries = Object.entries(value);
774
+ if (entries.length === 0) return "{}";
775
+ return `{\n${entries.map(([k, v]) => {
776
+ const key = JSON.stringify(k);
777
+ if (k === "B" && typeof v === "string") return `${inner}${key}: ${jsBinary$1(v)}`;
778
+ if (k === "BS" && Array.isArray(v)) return `${inner}${key}: [${v.map(jsBinary$1).join(", ")}]`;
779
+ return `${inner}${key}: ${jsLiteral(v, indent + 1)}`;
780
+ }).join(",\n")}\n${pad}}`;
781
+ }
782
+ if (typeof value === "string") return JSON.stringify(value);
783
+ return String(value);
784
+ }
785
+ /** Emit the `new <Op>Command({...})` source snippet for a canonical request. */
786
+ function emitSdkV3(request) {
787
+ return `new ${COMMAND_BY_OP[request.operation]}(${jsLiteral(buildSdkV3Params(request), 0)})`;
788
+ }
789
+ /** The `@aws-sdk/client-dynamodb` command class name for an operation. */
790
+ function sdkV3CommandName(operation) {
791
+ return COMMAND_BY_OP[operation];
792
+ }
793
+ /**
794
+ * Render an arbitrary params value as the same pretty-printed JS literal
795
+ * `emitSdkV3` embeds (B/BS members as `Uint8Array` expressions). Exported for
796
+ * the query-builder program emitter, which composes its own statements around
797
+ * the literal (client init + pagination loop) instead of a bare `new Command`.
798
+ */
799
+ function renderJsValue(value, indent = 0) {
800
+ return jsLiteral(value, indent);
801
+ }
802
+
803
+ //#endregion
804
+ //#region src/emit/cli.ts
805
+ /** Operation → the `aws dynamodb` subcommand. */
806
+ const SUBCOMMAND_BY_OP = {
807
+ GetItem: "get-item",
808
+ Query: "query",
809
+ Scan: "scan",
810
+ Update: "update-item",
811
+ Put: "put-item",
812
+ Delete: "delete-item"
813
+ };
814
+ /** Wrap a value in a POSIX single-quoted string, escaping inner single quotes. */
815
+ function shellQuote(value) {
816
+ return `'${value.replace(/'/g, `'\\''`)}'`;
817
+ }
818
+ /** Compact DynamoDB-JSON for a typed map (no whitespace → a tight shell arg). */
819
+ function avJson(map) {
820
+ return JSON.stringify(map);
821
+ }
822
+ /** Emit the multi-line `aws dynamodb <op>` command for a canonical request. */
823
+ function emitCli(request) {
824
+ const flags = [["--table-name", request.tableName]];
825
+ if (request.indexName) flags.push(["--index-name", request.indexName]);
826
+ if (request.key) flags.push(["--key", avJson(typedMapToAvMap(request.key))]);
827
+ if (request.item) flags.push(["--item", avJson(typedMapToAvMap(request.item))]);
828
+ if (request.keyConditionExpression) flags.push(["--key-condition-expression", request.keyConditionExpression]);
829
+ if (request.updateExpression) flags.push(["--update-expression", request.updateExpression]);
830
+ if (request.conditionExpression) flags.push(["--condition-expression", request.conditionExpression]);
831
+ if (request.filterExpression) flags.push(["--filter-expression", request.filterExpression]);
832
+ if (request.projectionExpression) flags.push(["--projection-expression", request.projectionExpression]);
833
+ if (request.names) flags.push(["--expression-attribute-names", avJson(request.names)]);
834
+ if (request.typedValues) flags.push(["--expression-attribute-values", avJson(typedMapToAvMap(request.typedValues))]);
835
+ if (request.consistentRead) flags.push(["--consistent-read", null]);
836
+ if (request.scanIndexForward === false) flags.push(["--no-scan-index-forward", null]);
837
+ if (request.limit !== void 0) flags.push(["--page-size", String(request.limit)]);
838
+ const comments = request.exclusiveStartKey ? ["# The AWS CLI has no --exclusive-start-key: resume a paginated run with", "# --starting-token <NextToken from the previous run's output> instead."] : [];
839
+ const command = [`aws dynamodb ${SUBCOMMAND_BY_OP[request.operation]}`, ...flags.map(([flag, value]) => value === null ? ` ${flag}` : ` ${flag} ${shellQuote(value)}`)].join(" \\\n");
840
+ return comments.length > 0 ? [...comments, command].join("\n") : command;
841
+ }
842
+
843
+ //#endregion
844
+ //#region src/emit/boto3.ts
845
+ /** Operation → the low-level `boto3.client("dynamodb")` method. */
846
+ const METHOD_BY_OP = {
847
+ GetItem: "get_item",
848
+ Query: "query",
849
+ Scan: "scan",
850
+ Update: "update_item",
851
+ Put: "put_item",
852
+ Delete: "delete_item"
853
+ };
854
+ /** Python expression decoding a base64 `B` member to the `bytes` the client needs. */
855
+ function pyBinary(base64) {
856
+ return `base64.b64decode(${JSON.stringify(base64)})`;
857
+ }
858
+ /**
859
+ * Render a JS value (string / boolean / array / object — the shapes a param
860
+ * object and its AttributeValue maps contain) as a Python literal. Strings reuse
861
+ * `JSON.stringify`, whose escapes (`\"`, `\\`, `\n`, `\uXXXX`) are all valid
862
+ * Python; booleans become `True`/`False`. A `B`/`BS` AttributeValue member is
863
+ * rendered as `bytes` (base64-decoded), not a str.
864
+ */
865
+ function pyLiteral(value) {
866
+ if (typeof value === "string") return JSON.stringify(value);
867
+ if (typeof value === "boolean") return value ? "True" : "False";
868
+ if (typeof value === "number") return String(value);
869
+ if (Array.isArray(value)) return `[${value.map(pyLiteral).join(", ")}]`;
870
+ if (value && typeof value === "object") return `{${Object.entries(value).map(([k, v]) => {
871
+ if (k === "B" && typeof v === "string") return `${JSON.stringify(k)}: ${pyBinary(v)}`;
872
+ if (k === "BS" && Array.isArray(v)) return `${JSON.stringify(k)}: [${v.map(pyBinary).join(", ")}]`;
873
+ return `${JSON.stringify(k)}: ${pyLiteral(v)}`;
874
+ }).join(", ")}}`;
875
+ return "None";
876
+ }
877
+ /**
878
+ * Render a params value as the same Python literal `emitBoto3` embeds
879
+ * (`True`/`False`/`None`, B/BS as `base64.b64decode(...)`). Exported for the
880
+ * query-builder program emitter, which renders a `params` dict for its
881
+ * LastEvaluatedKey pagination loop.
882
+ */
883
+ function renderPyValue(value) {
884
+ return pyLiteral(value);
885
+ }
886
+ /** The low-level boto3 client method name for an operation. */
887
+ function boto3MethodName(operation) {
888
+ return METHOD_BY_OP[operation];
889
+ }
890
+ /** Emit the runnable boto3 snippet (client + method call) for a canonical request. */
891
+ function emitBoto3(request) {
892
+ const params = buildSdkV3Params(request);
893
+ const method = METHOD_BY_OP[request.operation];
894
+ const kwargs = Object.entries(params).map(([key, value]) => ` ${key}=${pyLiteral(value)},`);
895
+ return [
896
+ ...hasBinaryValue(request) ? ["import base64", "import boto3"] : ["import boto3"],
897
+ "",
898
+ "client = boto3.client(\"dynamodb\")",
899
+ "",
900
+ `response = client.${method}(`,
901
+ ...kwargs,
902
+ ")"
903
+ ].join("\n");
904
+ }
905
+
906
+ //#endregion
907
+ //#region src/emit/partiql.ts
908
+ /** Thrown internally to short-circuit out of a partly-built statement. */
909
+ var NotExpressibleError = class extends Error {};
910
+ /** Bail out: this request has no faithful PartiQL form. */
911
+ function notExpressible(reason) {
912
+ throw new NotExpressibleError(reason);
913
+ }
914
+ /** Double-quote an identifier (reserved-word-safe), doubling any inner `"`. */
915
+ function quoteIdent(name) {
916
+ return `"${name.replace(/"/g, "\"\"")}"`;
917
+ }
918
+ /** Single-quote a string literal, doubling any inner `'` (PartiQL escaping). */
919
+ function quoteString(value) {
920
+ return `'${value.replace(/'/g, "''")}'`;
921
+ }
922
+ const NUMERIC = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
923
+ /** A typed value as a PartiQL literal — built from the tag, never the runtime. */
924
+ function literal(tv) {
925
+ switch (tv.type) {
926
+ case "S": return quoteString(tv.value);
927
+ case "N":
928
+ if (!NUMERIC.test(tv.value.trim())) return notExpressible(`non-numeric N value "${tv.value}" has no PartiQL literal form`);
929
+ return tv.value;
930
+ case "BOOL": return tv.value === "true" ? "true" : "false";
931
+ case "NULL": return "NULL";
932
+ case "B": return notExpressible("binary (B) values have no PartiQL literal form — use the SDK/CLI emitter");
933
+ case "SS":
934
+ case "NS":
935
+ case "BS": return notExpressible("set-typed values have no PartiQL literal form — use the SDK/CLI emitter");
936
+ }
937
+ }
938
+ /** `"Table"` or `"Table"."Index"`. */
939
+ function fromClause(config) {
940
+ const table = quoteIdent(config.tableName);
941
+ return config.indexName ? `${table}.${quoteIdent(config.indexName)}` : table;
942
+ }
943
+ /** Projected attributes → `"a", "b"`, or `*` when no projection is set. */
944
+ function selectList(config) {
945
+ const proj = config.projection;
946
+ if (proj && proj.length > 0) return proj.map(quoteIdent).join(", ");
947
+ return "*";
948
+ }
949
+ /** A FilterRow's single RHS value as a typed value (set-/scalar-aware). */
950
+ function single(row) {
951
+ return makeTypedValue(row.type, row.value, row.values);
952
+ }
953
+ /**
954
+ * Compile one predicate row (filter, condition, or — structurally compatible —
955
+ * a range-key condition) into a PartiQL boolean expression. Operators PartiQL
956
+ * can't express (`size_*`, `type_*`) degrade the whole statement.
957
+ */
958
+ function predicate(row) {
959
+ const id = quoteIdent(row.field);
960
+ switch (OPERATOR_BY_VALUE[row.operator].wireForm) {
961
+ case "EQ": return `${id} = ${literal(single(row))}`;
962
+ case "NE": return `${id} <> ${literal(single(row))}`;
963
+ case "LT": return `${id} < ${literal(single(row))}`;
964
+ case "LE": return `${id} <= ${literal(single(row))}`;
965
+ case "GT": return `${id} > ${literal(single(row))}`;
966
+ case "GE": return `${id} >= ${literal(single(row))}`;
967
+ case "BETWEEN": return `${id} BETWEEN ${literal(makeTypedValue(row.type, row.value))} AND ${literal(makeTypedValue(row.type, row.value2 ?? ""))}`;
968
+ case "BEGINS_WITH": return `begins_with(${id}, ${literal(single(row))})`;
969
+ case "CONTAINS": return `contains(${id}, ${literal(makeTypedValue(elementType(row.type), row.value))})`;
970
+ case "IN": return `${id} IN (${(row.values ?? (row.value ? [row.value] : [])).map((m) => literal(makeTypedValue(row.type, m))).join(", ")})`;
971
+ case "EXISTS": return `${id} IS NOT MISSING`;
972
+ case "NOT_EXISTS": return `${id} IS MISSING`;
973
+ case "SIZE_EQ":
974
+ case "SIZE_NE":
975
+ case "SIZE_LT":
976
+ case "SIZE_LE":
977
+ case "SIZE_GT":
978
+ case "SIZE_GE": return notExpressible("size() comparisons are not expressible in PartiQL");
979
+ case "TYPE_EQ":
980
+ case "TYPE_NE": return notExpressible("attribute_type() checks are not expressible in PartiQL");
981
+ }
982
+ }
983
+ /** Equality predicates for an exact primary key (GetItem/Update/Delete). */
984
+ function keyPredicates(keys) {
985
+ return keys.map((k) => `${quoteIdent(k.field)} = ${literal(makeTypedValue(k.type, k.value))}`);
986
+ }
987
+ /** One SET action → a PartiQL `SET` body, degrading on the inexpressible idioms. */
988
+ function setClause(action) {
989
+ const id = quoteIdent(action.field);
990
+ const value = action.value;
991
+ if (!value) throw new Error(`SET on "${action.field}" requires a value`);
992
+ switch (action.setOp ?? "assign") {
993
+ case "assign": return `${id} = ${literal(value)}`;
994
+ case "add":
995
+ case "subtract": return notExpressible("atomic counters (SET x = x ± n) are not expressible in PartiQL — use the SDK/CLI emitter");
996
+ case "if_not_exists": return notExpressible("if_not_exists() is not expressible in PartiQL");
997
+ case "list_append":
998
+ case "list_prepend": return notExpressible("list_append() is not expressible in PartiQL");
999
+ }
1000
+ }
1001
+ function buildSelect(config) {
1002
+ if (config.limit !== void 0) notExpressible("Limit is an ExecuteStatement API parameter, not part of the PartiQL statement — use the SDK/CLI emitter");
1003
+ if (config.consistentRead) notExpressible("ConsistentRead is an ExecuteStatement API parameter, not part of the PartiQL statement — use the SDK/CLI emitter");
1004
+ if (config.exclusiveStartKey && config.exclusiveStartKey.length > 0) notExpressible("PartiQL paginates with an opaque NextToken, not ExclusiveStartKey — use the SDK/CLI emitter");
1005
+ const head = `SELECT ${selectList(config)}\nFROM ${fromClause(config)}`;
1006
+ if (config.operation === "GetItem") {
1007
+ if (!config.key || config.key.length === 0) notExpressible("GetItem requires the full primary key in WHERE");
1008
+ return `${head}\nWHERE ${keyPredicates(config.key).join("\n AND ")}`;
1009
+ }
1010
+ if (config.operation === "Query") {
1011
+ if (!config.hashKey) notExpressible("Query requires a hash key");
1012
+ const preds$1 = [predicate({
1013
+ field: config.hashKey.field,
1014
+ operator: "=",
1015
+ type: config.hashKey.type,
1016
+ value: config.hashKey.value
1017
+ })];
1018
+ if (config.rangeKey) preds$1.push(predicate(config.rangeKey));
1019
+ for (const filter of config.filters ?? []) preds$1.push(predicate(filter));
1020
+ const select = `${head}\nWHERE ${preds$1.join("\n AND ")}`;
1021
+ if (config.scanIndexForward === false) {
1022
+ if (!config.rangeKey) notExpressible("descending order needs ORDER BY on the sort key — add a sort key condition, or use the SDK/CLI emitter");
1023
+ return `${select}\nORDER BY ${quoteIdent(config.rangeKey.field)} DESC`;
1024
+ }
1025
+ return select;
1026
+ }
1027
+ const preds = (config.filters ?? []).map(predicate);
1028
+ return preds.length ? `${head}\nWHERE ${preds.join("\n AND ")}` : head;
1029
+ }
1030
+ function buildUpdate(config) {
1031
+ if (!config.key || config.key.length === 0) notExpressible("UPDATE requires the full primary key in WHERE");
1032
+ const parts = [`UPDATE ${fromClause(config)}`];
1033
+ for (const action of config.updates ?? []) switch (action.kind) {
1034
+ case "SET":
1035
+ parts.push(`SET ${setClause(action)}`);
1036
+ break;
1037
+ case "REMOVE":
1038
+ parts.push(`REMOVE ${action.index === void 0 ? quoteIdent(action.field) : `${quoteIdent(action.field)}[${action.index}]`}`);
1039
+ break;
1040
+ case "ADD":
1041
+ case "DELETE": notExpressible(action.kind === "ADD" ? "ADD is not expressible in PartiQL" : "set DELETE is not expressible in PartiQL");
1042
+ }
1043
+ const where = [...keyPredicates(config.key), ...(config.conditions ?? []).map(predicate)];
1044
+ parts.push(`WHERE ${where.join("\n AND ")}`);
1045
+ return parts.join("\n");
1046
+ }
1047
+ function buildInsert(config) {
1048
+ if (config.conditions && config.conditions.length > 0) notExpressible("a conditional Put has no condition-less PartiQL INSERT — use the SDK/CLI emitter");
1049
+ const entries = (config.item ?? []).map((attr) => `${quoteString(attr.field)}: ${literal(makeTypedValue(attr.type, attr.value, attr.values))}`);
1050
+ return `INSERT INTO ${fromClause(config)} VALUE {${entries.join(", ")}}`;
1051
+ }
1052
+ function buildDelete(config) {
1053
+ if (!config.key || config.key.length === 0) notExpressible("DELETE requires the full primary key in WHERE");
1054
+ const where = [...keyPredicates(config.key), ...(config.conditions ?? []).map(predicate)];
1055
+ return `DELETE FROM ${fromClause(config)}\nWHERE ${where.join("\n AND ")}`;
1056
+ }
1057
+ function buildStatement(config) {
1058
+ switch (config.operation) {
1059
+ case "GetItem":
1060
+ case "Query":
1061
+ case "Scan": return buildSelect(config);
1062
+ case "Update": return buildUpdate(config);
1063
+ case "Put": return buildInsert(config);
1064
+ case "Delete": return buildDelete(config);
1065
+ }
1066
+ }
1067
+ /**
1068
+ * Emit a PartiQL statement for a canonical request, or an honest
1069
+ * `{ ok: false, reason }` when the request has no faithful PartiQL form.
1070
+ */
1071
+ function emitPartiql(request) {
1072
+ try {
1073
+ return {
1074
+ ok: true,
1075
+ statement: buildStatement(request.config)
1076
+ };
1077
+ } catch (error) {
1078
+ if (error instanceof NotExpressibleError) return {
1079
+ ok: false,
1080
+ reason: error.message
1081
+ };
1082
+ throw error;
1083
+ }
1084
+ }
1085
+
1086
+ //#endregion
1087
+ //#region src/emit/java.ts
1088
+ /** Operation → the `software.amazon.awssdk.services.dynamodb.model` request class. */
1089
+ const REQUEST_CLASS_BY_OP$1 = {
1090
+ GetItem: "GetItemRequest",
1091
+ Query: "QueryRequest",
1092
+ Scan: "ScanRequest",
1093
+ Update: "UpdateItemRequest",
1094
+ Put: "PutItemRequest",
1095
+ Delete: "DeleteItemRequest"
1096
+ };
1097
+ /** Operation → the `DynamoDbClient` method. */
1098
+ const CLIENT_METHOD_BY_OP$2 = {
1099
+ GetItem: "getItem",
1100
+ Query: "query",
1101
+ Scan: "scan",
1102
+ Update: "updateItem",
1103
+ Put: "putItem",
1104
+ Delete: "deleteItem"
1105
+ };
1106
+ /** The model request class name for an operation (`QueryRequest`, …). */
1107
+ function javaRequestClassName(operation) {
1108
+ return REQUEST_CLASS_BY_OP$1[operation];
1109
+ }
1110
+ /** The `DynamoDbClient` method name for an operation (`query`, …). */
1111
+ function javaClientMethodName(operation) {
1112
+ return CLIENT_METHOD_BY_OP$2[operation];
1113
+ }
1114
+ /** Java string literal — `JSON.stringify` escapes are all valid Java escapes. */
1115
+ function javaString(value) {
1116
+ return JSON.stringify(value);
1117
+ }
1118
+ /** Java expression decoding a base64 `B` member to the `SdkBytes` the client needs. */
1119
+ function javaBinary(base64) {
1120
+ return `SdkBytes.fromByteArray(Base64.getDecoder().decode(${javaString(base64)}))`;
1121
+ }
1122
+ /** Render one wire AttributeValue as an `AttributeValue.builder()…build()` chain. */
1123
+ function renderJavaAv(av) {
1124
+ if ("S" in av) return `AttributeValue.builder().s(${javaString(av.S)}).build()`;
1125
+ if ("N" in av) return `AttributeValue.builder().n(${javaString(av.N)}).build()`;
1126
+ if ("B" in av) return `AttributeValue.builder().b(${javaBinary(av.B)}).build()`;
1127
+ if ("BOOL" in av) return `AttributeValue.builder().bool(${av.BOOL}).build()`;
1128
+ if ("SS" in av) return `AttributeValue.builder().ss(${av.SS.map(javaString).join(", ")}).build()`;
1129
+ if ("NS" in av) return `AttributeValue.builder().ns(${av.NS.map(javaString).join(", ")}).build()`;
1130
+ if ("BS" in av) return `AttributeValue.builder().bs(${av.BS.map(javaBinary).join(", ")}).build()`;
1131
+ return "AttributeValue.builder().nul(true).build()";
1132
+ }
1133
+ /** Render a typed map as `Map.ofEntries(Map.entry(name, <AV builder>), …)`. */
1134
+ function renderJavaAvMap(map, indent) {
1135
+ const inner = `${indent} `;
1136
+ return `Map.ofEntries(\n${Object.entries(typedMapToAvMap(map)).map(([name, av]) => `${inner}Map.entry(${javaString(name)}, ${renderJavaAv(av)})`).join(",\n")}\n${indent})`;
1137
+ }
1138
+ /** Render the plain string name map (`ExpressionAttributeNames`). */
1139
+ function renderJavaNameMap(map, indent) {
1140
+ const inner = `${indent} `;
1141
+ return `Map.ofEntries(\n${Object.entries(map).map(([alias, name]) => `${inner}Map.entry(${javaString(alias)}, ${javaString(name)})`).join(",\n")}\n${indent})`;
1142
+ }
1143
+ /**
1144
+ * Render the `<Op>Request.builder()…build()` expression, each setter on its own
1145
+ * line at `indent + 4` spaces. Exported for the query-builder program emitter,
1146
+ * which wraps the same expression in a runnable class (client init + send +
1147
+ * pagination loop); `extraSetters` lets it append loop-owned setters (e.g.
1148
+ * `.exclusiveStartKey(lastEvaluatedKey)`) before `.build()`.
1149
+ */
1150
+ function renderJavaRequestBuilder(request, indent, extraSetters = []) {
1151
+ const pad = `${indent} `;
1152
+ const setters = [`.tableName(${javaString(request.tableName)})`];
1153
+ if (request.indexName) setters.push(`.indexName(${javaString(request.indexName)})`);
1154
+ if (request.key) setters.push(`.key(${renderJavaAvMap(request.key, pad)})`);
1155
+ if (request.item) setters.push(`.item(${renderJavaAvMap(request.item, pad)})`);
1156
+ if (request.keyConditionExpression) setters.push(`.keyConditionExpression(${javaString(request.keyConditionExpression)})`);
1157
+ if (request.updateExpression) setters.push(`.updateExpression(${javaString(request.updateExpression)})`);
1158
+ if (request.conditionExpression) setters.push(`.conditionExpression(${javaString(request.conditionExpression)})`);
1159
+ if (request.filterExpression) setters.push(`.filterExpression(${javaString(request.filterExpression)})`);
1160
+ if (request.projectionExpression) setters.push(`.projectionExpression(${javaString(request.projectionExpression)})`);
1161
+ if (request.names) setters.push(`.expressionAttributeNames(${renderJavaNameMap(request.names, pad)})`);
1162
+ if (request.typedValues) setters.push(`.expressionAttributeValues(${renderJavaAvMap(request.typedValues, pad)})`);
1163
+ if (request.limit !== void 0) setters.push(`.limit(${request.limit})`);
1164
+ if (request.consistentRead) setters.push(".consistentRead(true)");
1165
+ if (request.scanIndexForward === false) setters.push(".scanIndexForward(false)");
1166
+ if (request.exclusiveStartKey) setters.push(`.exclusiveStartKey(${renderJavaAvMap(request.exclusiveStartKey, pad)})`);
1167
+ const lines = [
1168
+ ...setters,
1169
+ ...extraSetters,
1170
+ ".build()"
1171
+ ].map((s) => `${pad}${s}`);
1172
+ return [`${REQUEST_CLASS_BY_OP$1[request.operation]}.builder()`, ...lines].join("\n");
1173
+ }
1174
+ /** Emit the bare `<Op>Request.builder()…build()` snippet for a canonical request. */
1175
+ function emitJava(request) {
1176
+ return renderJavaRequestBuilder(request, "");
1177
+ }
1178
+
1179
+ //#endregion
1180
+ //#region src/emit/go.ts
1181
+ /** Operation → the `dynamodb.<Op>Input` struct name. */
1182
+ const INPUT_TYPE_BY_OP = {
1183
+ GetItem: "GetItemInput",
1184
+ Query: "QueryInput",
1185
+ Scan: "ScanInput",
1186
+ Update: "UpdateItemInput",
1187
+ Put: "PutItemInput",
1188
+ Delete: "DeleteItemInput"
1189
+ };
1190
+ /** Operation → the `dynamodb.Client` method. */
1191
+ const CLIENT_METHOD_BY_OP$1 = {
1192
+ GetItem: "GetItem",
1193
+ Query: "Query",
1194
+ Scan: "Scan",
1195
+ Update: "UpdateItem",
1196
+ Put: "PutItem",
1197
+ Delete: "DeleteItem"
1198
+ };
1199
+ /** The `dynamodb.<Op>Input` struct name for an operation. */
1200
+ function goInputTypeName(operation) {
1201
+ return INPUT_TYPE_BY_OP[operation];
1202
+ }
1203
+ /** The `dynamodb.Client` method name for an operation. */
1204
+ function goClientMethodName(operation) {
1205
+ return CLIENT_METHOD_BY_OP$1[operation];
1206
+ }
1207
+ /** Go string literal — `JSON.stringify` escapes are all valid Go escapes. */
1208
+ function goString(value) {
1209
+ return JSON.stringify(value);
1210
+ }
1211
+ /** Decode canonical base64 into a Go `[]byte{0x…}` literal (fail-loud on bad input). */
1212
+ function goBytes(base64) {
1213
+ let raw;
1214
+ try {
1215
+ raw = atob(base64);
1216
+ } catch {
1217
+ throw new Error(`invalid base64 in a binary (B/BS) value: ${base64}`);
1218
+ }
1219
+ return `[]byte{${Array.from(raw, (c) => `0x${c.charCodeAt(0).toString(16).padStart(2, "0")}`).join(", ")}}`;
1220
+ }
1221
+ /** Render one wire AttributeValue as a `&types.AttributeValueMember*` literal. */
1222
+ function renderGoAv(av) {
1223
+ if ("S" in av) return `&types.AttributeValueMemberS{Value: ${goString(av.S)}}`;
1224
+ if ("N" in av) return `&types.AttributeValueMemberN{Value: ${goString(av.N)}}`;
1225
+ if ("B" in av) return `&types.AttributeValueMemberB{Value: ${goBytes(av.B)}}`;
1226
+ if ("BOOL" in av) return `&types.AttributeValueMemberBOOL{Value: ${av.BOOL}}`;
1227
+ if ("SS" in av) return `&types.AttributeValueMemberSS{Value: []string{${av.SS.map(goString).join(", ")}}}`;
1228
+ if ("NS" in av) return `&types.AttributeValueMemberNS{Value: []string{${av.NS.map(goString).join(", ")}}}`;
1229
+ if ("BS" in av) return `&types.AttributeValueMemberBS{Value: [][]byte{${av.BS.map(goBytes).join(", ")}}}`;
1230
+ return "&types.AttributeValueMemberNULL{Value: true}";
1231
+ }
1232
+ /** Render a typed map as a `map[string]types.AttributeValue{…}` literal. */
1233
+ function renderGoAvMap(map, indent) {
1234
+ const inner = `${indent}\t`;
1235
+ return `map[string]types.AttributeValue{\n${Object.entries(typedMapToAvMap(map)).map(([name, av]) => `${inner}${goString(name)}: ${renderGoAv(av)},`).join("\n")}\n${indent}}`;
1236
+ }
1237
+ /** Render the plain string name map (`ExpressionAttributeNames`). */
1238
+ function renderGoNameMap(map, indent) {
1239
+ const inner = `${indent}\t`;
1240
+ return `map[string]string{\n${Object.entries(map).map(([alias, name]) => `${inner}${goString(alias)}: ${goString(name)},`).join("\n")}\n${indent}}`;
1241
+ }
1242
+ /**
1243
+ * Render the `&dynamodb.<Op>Input{…}` composite literal, one field per line at
1244
+ * `indent + \t`. Exported for the query-builder program emitter, which wraps
1245
+ * the same literal in a runnable `main.go` (config load + client + paginator).
1246
+ */
1247
+ function renderGoInput(request, indent) {
1248
+ const pad = `${indent}\t`;
1249
+ const fields = [`TableName: aws.String(${goString(request.tableName)}),`];
1250
+ if (request.indexName) fields.push(`IndexName: aws.String(${goString(request.indexName)}),`);
1251
+ if (request.key) fields.push(`Key: ${renderGoAvMap(request.key, pad)},`);
1252
+ if (request.item) fields.push(`Item: ${renderGoAvMap(request.item, pad)},`);
1253
+ if (request.keyConditionExpression) fields.push(`KeyConditionExpression: aws.String(${goString(request.keyConditionExpression)}),`);
1254
+ if (request.updateExpression) fields.push(`UpdateExpression: aws.String(${goString(request.updateExpression)}),`);
1255
+ if (request.conditionExpression) fields.push(`ConditionExpression: aws.String(${goString(request.conditionExpression)}),`);
1256
+ if (request.filterExpression) fields.push(`FilterExpression: aws.String(${goString(request.filterExpression)}),`);
1257
+ if (request.projectionExpression) fields.push(`ProjectionExpression: aws.String(${goString(request.projectionExpression)}),`);
1258
+ if (request.names) fields.push(`ExpressionAttributeNames: ${renderGoNameMap(request.names, pad)},`);
1259
+ if (request.typedValues) fields.push(`ExpressionAttributeValues: ${renderGoAvMap(request.typedValues, pad)},`);
1260
+ if (request.limit !== void 0) fields.push(`Limit: aws.Int32(${request.limit}),`);
1261
+ if (request.consistentRead) fields.push("ConsistentRead: aws.Bool(true),");
1262
+ if (request.scanIndexForward === false) fields.push("ScanIndexForward: aws.Bool(false),");
1263
+ if (request.exclusiveStartKey) fields.push(`ExclusiveStartKey: ${renderGoAvMap(request.exclusiveStartKey, pad)},`);
1264
+ const lines = fields.map((f) => `${pad}${f}`);
1265
+ return [
1266
+ `&dynamodb.${INPUT_TYPE_BY_OP[request.operation]}{`,
1267
+ ...lines,
1268
+ `${indent}}`
1269
+ ].join("\n");
1270
+ }
1271
+ /**
1272
+ * Does this request render any `types.AttributeValue` (AV map)? Drives the
1273
+ * `dynamodb/types` import in the program emitter.
1274
+ */
1275
+ function goUsesTypes(request) {
1276
+ return request.key !== void 0 || request.item !== void 0 || request.typedValues !== void 0 || request.exclusiveStartKey !== void 0;
1277
+ }
1278
+ /** Emit the bare `&dynamodb.<Op>Input{…}` snippet for a canonical request. */
1279
+ function emitGo(request) {
1280
+ return renderGoInput(request, "");
1281
+ }
1282
+
1283
+ //#endregion
1284
+ //#region src/emit/dotnet.ts
1285
+ /** Operation → the `Amazon.DynamoDBv2.Model` request class. */
1286
+ const REQUEST_CLASS_BY_OP = {
1287
+ GetItem: "GetItemRequest",
1288
+ Query: "QueryRequest",
1289
+ Scan: "ScanRequest",
1290
+ Update: "UpdateItemRequest",
1291
+ Put: "PutItemRequest",
1292
+ Delete: "DeleteItemRequest"
1293
+ };
1294
+ /** Operation → the async `AmazonDynamoDBClient` method. */
1295
+ const CLIENT_METHOD_BY_OP = {
1296
+ GetItem: "GetItemAsync",
1297
+ Query: "QueryAsync",
1298
+ Scan: "ScanAsync",
1299
+ Update: "UpdateItemAsync",
1300
+ Put: "PutItemAsync",
1301
+ Delete: "DeleteItemAsync"
1302
+ };
1303
+ /** The model request class name for an operation (`QueryRequest`, …). */
1304
+ function dotnetRequestClassName(operation) {
1305
+ return REQUEST_CLASS_BY_OP[operation];
1306
+ }
1307
+ /** The async client method name for an operation (`QueryAsync`, …). */
1308
+ function dotnetClientMethodName(operation) {
1309
+ return CLIENT_METHOD_BY_OP[operation];
1310
+ }
1311
+ /** C# string literal — `JSON.stringify` escapes are all valid C# escapes. */
1312
+ function csString(value) {
1313
+ return JSON.stringify(value);
1314
+ }
1315
+ /** C# expression decoding a base64 `B` member to the `MemoryStream` the client needs. */
1316
+ function csBinary(base64) {
1317
+ return `new MemoryStream(Convert.FromBase64String(${csString(base64)}))`;
1318
+ }
1319
+ /** Render one wire AttributeValue as a `new AttributeValue { … }` initializer. */
1320
+ function renderCsAv(av) {
1321
+ if ("S" in av) return `new AttributeValue { S = ${csString(av.S)} }`;
1322
+ if ("N" in av) return `new AttributeValue { N = ${csString(av.N)} }`;
1323
+ if ("B" in av) return `new AttributeValue { B = ${csBinary(av.B)} }`;
1324
+ if ("BOOL" in av) return `new AttributeValue { BOOL = ${av.BOOL} }`;
1325
+ if ("SS" in av) return `new AttributeValue { SS = new List<string> { ${av.SS.map(csString).join(", ")} } }`;
1326
+ if ("NS" in av) return `new AttributeValue { NS = new List<string> { ${av.NS.map(csString).join(", ")} } }`;
1327
+ if ("BS" in av) return `new AttributeValue { BS = new List<MemoryStream> { ${av.BS.map(csBinary).join(", ")} } }`;
1328
+ return "new AttributeValue { NULL = true }";
1329
+ }
1330
+ /** Render a typed map as a `Dictionary<string, AttributeValue>` initializer. */
1331
+ function renderCsAvMap(map, indent) {
1332
+ const inner = `${indent} `;
1333
+ return `new Dictionary<string, AttributeValue>\n${indent}{\n${Object.entries(typedMapToAvMap(map)).map(([name, av]) => `${inner}[${csString(name)}] = ${renderCsAv(av)},`).join("\n")}\n${indent}}`;
1334
+ }
1335
+ /** Render the plain string name map (`ExpressionAttributeNames`). */
1336
+ function renderCsNameMap(map, indent) {
1337
+ const inner = `${indent} `;
1338
+ return `new Dictionary<string, string>\n${indent}{\n${Object.entries(map).map(([alias, name]) => `${inner}[${csString(alias)}] = ${csString(name)},`).join("\n")}\n${indent}}`;
1339
+ }
1340
+ /**
1341
+ * Render the `new <Op>Request { … }` object initializer, one property per line
1342
+ * at `indent + 4` spaces. Exported for the query-builder program emitter, which
1343
+ * wraps the same initializer in a runnable program (client init + await send +
1344
+ * pagination loop).
1345
+ */
1346
+ function renderCsRequest(request, indent) {
1347
+ const pad = `${indent} `;
1348
+ const props = [`TableName = ${csString(request.tableName)},`];
1349
+ if (request.indexName) props.push(`IndexName = ${csString(request.indexName)},`);
1350
+ if (request.key) props.push(`Key = ${renderCsAvMap(request.key, pad)},`);
1351
+ if (request.item) props.push(`Item = ${renderCsAvMap(request.item, pad)},`);
1352
+ if (request.keyConditionExpression) props.push(`KeyConditionExpression = ${csString(request.keyConditionExpression)},`);
1353
+ if (request.updateExpression) props.push(`UpdateExpression = ${csString(request.updateExpression)},`);
1354
+ if (request.conditionExpression) props.push(`ConditionExpression = ${csString(request.conditionExpression)},`);
1355
+ if (request.filterExpression) props.push(`FilterExpression = ${csString(request.filterExpression)},`);
1356
+ if (request.projectionExpression) props.push(`ProjectionExpression = ${csString(request.projectionExpression)},`);
1357
+ if (request.names) props.push(`ExpressionAttributeNames = ${renderCsNameMap(request.names, pad)},`);
1358
+ if (request.typedValues) props.push(`ExpressionAttributeValues = ${renderCsAvMap(request.typedValues, pad)},`);
1359
+ if (request.limit !== void 0) props.push(`Limit = ${request.limit},`);
1360
+ if (request.consistentRead) props.push("ConsistentRead = true,");
1361
+ if (request.scanIndexForward === false) props.push("ScanIndexForward = false,");
1362
+ if (request.exclusiveStartKey) props.push(`ExclusiveStartKey = ${renderCsAvMap(request.exclusiveStartKey, pad)},`);
1363
+ const lines = props.map((p) => `${pad}${p}`);
1364
+ return [
1365
+ `new ${REQUEST_CLASS_BY_OP[request.operation]}`,
1366
+ `${indent}{`,
1367
+ ...lines,
1368
+ `${indent}}`
1369
+ ].join("\n");
1370
+ }
1371
+ /** Emit the bare `new <Op>Request { … }` snippet for a canonical request. */
1372
+ function emitDotnet(request) {
1373
+ return renderCsRequest(request, "");
1374
+ }
1375
+
1376
+ //#endregion
1377
+ //#region src/emit/ddbtoolbox.ts
1378
+ /** Key attr DynamoDB type (S/N/B) → the Table `type` string dynamodb-toolbox wants. */
1379
+ function tableKeyType(type) {
1380
+ if (type === "N") return "'number'";
1381
+ if (type === "B") return "'binary'";
1382
+ return "'string'";
1383
+ }
1384
+ /** JS expression decoding a base64 `B` value to the `Uint8Array` the client wants. */
1385
+ function jsBinary(base64) {
1386
+ return `Uint8Array.from(atob(${JSON.stringify(base64)}), (c) => c.charCodeAt(0))`;
1387
+ }
1388
+ /**
1389
+ * Render a {@link TypedValue} as the NATIVE JS literal dynamodb-toolbox expects
1390
+ * (the DocumentClient does the low-level AttributeValue marshalling): a bare
1391
+ * number for N, a `Uint8Array` for B, a real `Set` for SS/NS/BS.
1392
+ */
1393
+ function renderNativeValue(tv) {
1394
+ switch (tv.type) {
1395
+ case "S": return JSON.stringify(tv.value);
1396
+ case "N": return tv.value;
1397
+ case "B": return jsBinary(tv.value);
1398
+ case "BOOL": return tv.value === "true" ? "true" : "false";
1399
+ case "NULL": return "null";
1400
+ case "SS": return `new Set([${(tv.values ?? []).map((v) => JSON.stringify(v)).join(", ")}])`;
1401
+ case "NS": return `new Set([${(tv.values ?? []).join(", ")}])`;
1402
+ case "BS": return `new Set([${(tv.values ?? []).map(jsBinary).join(", ")}])`;
1403
+ }
1404
+ }
1405
+ /** Build a scalar TypedValue from a tag + raw string (for key/range/filter RHS). */
1406
+ function scalar(type, value) {
1407
+ return {
1408
+ type,
1409
+ value
1410
+ };
1411
+ }
1412
+ /** lowercase comparison operator → dynamodb-toolbox condition key. */
1413
+ const COMPARATOR_KEY = {
1414
+ "=": "eq",
1415
+ "<>": "ne",
1416
+ "<": "lt",
1417
+ "<=": "lte",
1418
+ ">": "gt",
1419
+ ">=": "gte"
1420
+ };
1421
+ /** size_* operator → the comparator key its number pairs with. */
1422
+ const SIZE_COMPARATOR = {
1423
+ size_eq: "eq",
1424
+ size_ne: "ne",
1425
+ size_lt: "lt",
1426
+ size_le: "lte",
1427
+ size_gt: "gt",
1428
+ size_ge: "gte"
1429
+ };
1430
+ /**
1431
+ * Render one filter row as a dynamodb-toolbox condition object. `size_*` targets
1432
+ * a path via the `size` key (`{ size: 'attr', gt: 3 }`); `type_ne` has no direct
1433
+ * form so it degrades to `{ not: { attr, type } }`.
1434
+ */
1435
+ function renderCondition(f) {
1436
+ const attr = JSON.stringify(f.field);
1437
+ const val = () => renderNativeValue(scalar(f.type, f.value));
1438
+ if (f.operator === "exists") return `{ attr: ${attr}, exists: true }`;
1439
+ if (f.operator === "not_exists") return `{ attr: ${attr}, exists: false }`;
1440
+ if (f.operator === "contains") return `{ attr: ${attr}, contains: ${val()} }`;
1441
+ if (f.operator === "begins_with") return `{ attr: ${attr}, beginsWith: ${val()} }`;
1442
+ if (f.operator === "between") return `{ attr: ${attr}, between: [${renderNativeValue(scalar(f.type, f.value))}, ${renderNativeValue(scalar(f.type, f.value2 ?? ""))}] }`;
1443
+ if (f.operator === "in") return `{ attr: ${attr}, in: [${(f.values ?? []).map((v) => renderNativeValue(scalar(f.type, v))).join(", ")}] }`;
1444
+ if (f.operator === "type_eq") return `{ attr: ${attr}, type: ${JSON.stringify(f.value)} }`;
1445
+ if (f.operator === "type_ne") return `{ not: { attr: ${attr}, type: ${JSON.stringify(f.value)} } }`;
1446
+ const sizeKey = SIZE_COMPARATOR[f.operator];
1447
+ if (sizeKey) return `{ size: ${attr}, ${sizeKey}: ${f.value} }`;
1448
+ const cmp = COMPARATOR_KEY[f.operator];
1449
+ if (cmp) return `{ attr: ${attr}, ${cmp}: ${val()} }`;
1450
+ throw new Error(`dynamodb-toolbox: unsupported filter operator "${f.operator}"`);
1451
+ }
1452
+ /** Compose the filter option — a single condition, or `{ and: [...] }` for several. */
1453
+ function renderFilter(filters) {
1454
+ const conds = filters.map(renderCondition);
1455
+ if (conds.length === 1) return conds[0];
1456
+ return `{ and: [${conds.map((c) => `\n ${c}`).join(",")}\n ] }`;
1457
+ }
1458
+ /** A `{ name, type }` key-attr line for a Table partition/sort key. */
1459
+ function keyLine(field, type) {
1460
+ return `{ name: ${JSON.stringify(field)}, type: ${tableKeyType(type)} }`;
1461
+ }
1462
+ /**
1463
+ * Render the `new Table({...})` scaffold. A base-table Query knows its real
1464
+ * PK/SK; a Scan or a GSI query does NOT (the request's key belongs to the GSI),
1465
+ * so it emits `config.baseKeySchema` when supplied, else a placeholder base PK
1466
+ * the developer replaces.
1467
+ */
1468
+ function renderTable(config) {
1469
+ const lines = [` name: ${JSON.stringify(config.tableName)},`];
1470
+ if (config.operation === "Query" && !config.indexName && config.hashKey) {
1471
+ lines.push(` partitionKey: ${keyLine(config.hashKey.field, config.hashKey.type)},`);
1472
+ if (config.rangeKey) lines.push(` sortKey: ${keyLine(config.rangeKey.field, config.rangeKey.type)},`);
1473
+ } else if (config.baseKeySchema) {
1474
+ const { hashKey, rangeKey } = config.baseKeySchema;
1475
+ lines.push(` partitionKey: ${keyLine(hashKey.field, hashKey.type)},`);
1476
+ if (rangeKey) lines.push(` sortKey: ${keyLine(rangeKey.field, rangeKey.type)},`);
1477
+ } else lines.push(" // TODO: replace with your table's real key schema (unknown from this query alone)", " partitionKey: { name: 'PK', type: 'string' },");
1478
+ if (config.indexName && config.hashKey) {
1479
+ const idx = [
1480
+ ` ${JSON.stringify(config.indexName)}: {`,
1481
+ ` type: 'global',`,
1482
+ ` partitionKey: ${keyLine(config.hashKey.field, config.hashKey.type)},`,
1483
+ ...config.rangeKey ? [` sortKey: ${keyLine(config.rangeKey.field, config.rangeKey.type)},`] : [],
1484
+ " }"
1485
+ ];
1486
+ lines.push(` indexes: {\n${idx.join("\n")}\n },`);
1487
+ }
1488
+ return [
1489
+ "const table = new Table({",
1490
+ " documentClient,",
1491
+ ...lines,
1492
+ "});"
1493
+ ].join("\n");
1494
+ }
1495
+ /** The `.query({...})` argument for a Query (partition [+ range] [+ index]). */
1496
+ function renderQueryArg(config) {
1497
+ if (!config.hashKey) throw new Error("dynamodb-toolbox: Query requires a partition key");
1498
+ const parts = [`partition: ${renderNativeValue(scalar(config.hashKey.type, config.hashKey.value))}`];
1499
+ if (config.rangeKey) parts.push(`range: ${renderRange(config.rangeKey)}`);
1500
+ if (config.indexName) parts.push(`index: ${JSON.stringify(config.indexName)}`);
1501
+ return `{ ${parts.join(", ")} }`;
1502
+ }
1503
+ /** A sort-key `range` condition: `{ beginsWith: 'x' }` / `{ between: [a, b] }` / … */
1504
+ function renderRange(range) {
1505
+ const v = () => renderNativeValue(scalar(range.type, range.value));
1506
+ if (range.operator === "begins_with") return `{ beginsWith: ${v()} }`;
1507
+ if (range.operator === "between") return `{ between: [${renderNativeValue(scalar(range.type, range.value))}, ${renderNativeValue(scalar(range.type, range.value2 ?? ""))}] }`;
1508
+ const cmp = COMPARATOR_KEY[range.operator];
1509
+ if (cmp) return `{ ${cmp}: ${v()} }`;
1510
+ throw new Error(`dynamodb-toolbox: unsupported range operator "${range.operator}"`);
1511
+ }
1512
+ /** Native key map for `exclusiveStartKey` (typed KeyAttr[] → `{ pk: 'x' }`). */
1513
+ function renderKeyMap(key) {
1514
+ return `{ ${key.map((k) => `${JSON.stringify(k.field)}: ${renderNativeValue(scalar(k.type, k.value))}`).join(", ")} }`;
1515
+ }
1516
+ /**
1517
+ * The `.options({...})` entries. When `paginate`, `exclusiveStartKey` is OWNED
1518
+ * by the loop (seeded from the manual resume point there), so it is omitted here
1519
+ * — the loop appends its own `exclusiveStartKey` shorthand entry instead.
1520
+ */
1521
+ function optionEntries(config, paginate) {
1522
+ const opts = [];
1523
+ if (config.filters && config.filters.length > 0) opts.push(`filter: ${renderFilter(config.filters)}`);
1524
+ if (config.limit !== void 0) opts.push(`limit: ${config.limit}`);
1525
+ if (config.consistentRead) opts.push("consistent: true");
1526
+ if (config.operation === "Query" && config.scanIndexForward === false) opts.push("reverse: true");
1527
+ if (config.projection && config.projection.length > 0) opts.push(`attributes: [${config.projection.map((a) => JSON.stringify(a)).join(", ")}]`);
1528
+ if (!paginate && config.exclusiveStartKey && config.exclusiveStartKey.length > 0) opts.push(`exclusiveStartKey: ${renderKeyMap(config.exclusiveStartKey)}`);
1529
+ return opts;
1530
+ }
1531
+ /** Wrap `optionEntries` into the `.options({...})` object literal. */
1532
+ function renderOptions(config, paginate) {
1533
+ const opts = optionEntries(config, paginate);
1534
+ return opts.length > 0 ? `{ ${opts.join(", ")} }` : "{}";
1535
+ }
1536
+ /**
1537
+ * Emit the runnable dynamodb-toolbox program for a Query/Scan canonical request.
1538
+ * `paginate` wraps the send in a `LastEvaluatedKey` loop (dynamodb-toolbox's
1539
+ * `send()` returns `{ Items, LastEvaluatedKey }` and takes `exclusiveStartKey`).
1540
+ */
1541
+ function emitDdbToolboxProgram(request, paginate) {
1542
+ const config = request.config;
1543
+ const isQuery = config.operation === "Query";
1544
+ const command = isQuery ? "QueryCommand" : "ScanCommand";
1545
+ const header = [
1546
+ `import { DynamoDBClient } from "@aws-sdk/client-dynamodb";`,
1547
+ `import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";`,
1548
+ `import { Table } from "dynamodb-toolbox/table";`,
1549
+ `import { ${command} } from ${isQuery ? "'dynamodb-toolbox/table/actions/query'" : "'dynamodb-toolbox/table/actions/scan'"};`,
1550
+ "",
1551
+ "const documentClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));",
1552
+ "",
1553
+ renderTable(config),
1554
+ ""
1555
+ ];
1556
+ const queryLine = isQuery ? ` .query(${renderQueryArg(config)})\n` : "";
1557
+ if (!paginate) return [
1558
+ ...header,
1559
+ "const response = await table",
1560
+ ` .build(${command})`,
1561
+ `${queryLine} .options(${renderOptions(config, false)})`,
1562
+ " .send();",
1563
+ "",
1564
+ "console.log(response.Items);"
1565
+ ].join("\n");
1566
+ const seed = config.exclusiveStartKey && config.exclusiveStartKey.length > 0 ? ` = ${renderKeyMap(config.exclusiveStartKey)}` : "";
1567
+ const loopOptions = `{ ${[...optionEntries(config, true), "exclusiveStartKey"].join(", ")} }`;
1568
+ return [
1569
+ ...header,
1570
+ "const items = [];",
1571
+ `let exclusiveStartKey${seed};`,
1572
+ "do {",
1573
+ " const response = await table",
1574
+ ` .build(${command})`,
1575
+ isQuery ? ` .query(${renderQueryArg(config)})` : null,
1576
+ ` .options(${loopOptions})`,
1577
+ " .send();",
1578
+ " items.push(...(response.Items ?? []));",
1579
+ " exclusiveStartKey = response.LastEvaluatedKey;",
1580
+ "} while (exclusiveStartKey);",
1581
+ "",
1582
+ "console.log(items);"
1583
+ ].filter((l) => l !== null).join("\n");
1584
+ }
1585
+
1586
+ //#endregion
1587
+ //#region src/program.ts
1588
+ /**
1589
+ * Emit the runnable program for a Query/Scan config in the given format.
1590
+ * Throws on a non-read operation (the widget never produces one — programmer
1591
+ * error, mirroring `buildRequest`'s fail-loud posture) and propagates
1592
+ * `buildRequest`'s build errors (e.g. a Query without a hash key) for the
1593
+ * caller to surface as a note.
1594
+ */
1595
+ function emitQueryProgram(config, format) {
1596
+ if (config.operation !== "Query" && config.operation !== "Scan") throw new Error("the query builder emits Query and Scan programs only");
1597
+ const request = buildRequest(config);
1598
+ switch (format) {
1599
+ case "sdk": return {
1600
+ ok: true,
1601
+ code: emitSdkProgram(request, config.paginate === true)
1602
+ };
1603
+ case "cli": return {
1604
+ ok: true,
1605
+ code: emitCliProgram(request, config.paginate === true)
1606
+ };
1607
+ case "boto3": return {
1608
+ ok: true,
1609
+ code: emitBoto3Program(request, config.paginate === true)
1610
+ };
1611
+ case "partiql": {
1612
+ const result = emitPartiql(request);
1613
+ return result.ok ? {
1614
+ ok: true,
1615
+ code: result.statement
1616
+ } : result;
1617
+ }
1618
+ case "java": return {
1619
+ ok: true,
1620
+ code: emitJavaProgram(request, config.paginate === true)
1621
+ };
1622
+ case "go": return {
1623
+ ok: true,
1624
+ code: emitGoProgram(request, config.paginate === true)
1625
+ };
1626
+ case "dotnet": return {
1627
+ ok: true,
1628
+ code: emitDotnetProgram(request, config.paginate === true)
1629
+ };
1630
+ case "ddbtoolbox": return {
1631
+ ok: true,
1632
+ code: emitDdbToolboxProgram(request, config.paginate === true)
1633
+ };
1634
+ }
1635
+ }
1636
+ function emitSdkProgram(request, paginate) {
1637
+ const command = sdkV3CommandName(request.operation);
1638
+ const header = [
1639
+ `import { DynamoDBClient, ${command} } from "@aws-sdk/client-dynamodb";`,
1640
+ "",
1641
+ "const client = new DynamoDBClient({});",
1642
+ ""
1643
+ ];
1644
+ if (!paginate) {
1645
+ const params$1 = renderJsValue(buildSdkV3Params(request), 1);
1646
+ return [
1647
+ ...header,
1648
+ "const response = await client.send(",
1649
+ ` new ${command}(${params$1})`,
1650
+ ");",
1651
+ "",
1652
+ "console.log(response.Items);"
1653
+ ].join("\n");
1654
+ }
1655
+ const { ExclusiveStartKey: startKey,...params } = buildSdkV3Params(request);
1656
+ const seed = startKey === void 0 ? ";" : ` = ${renderJsValue(startKey)};`;
1657
+ return [
1658
+ ...header,
1659
+ `const params = ${renderJsValue(params)};`,
1660
+ "",
1661
+ "const items = [];",
1662
+ `let lastEvaluatedKey${seed}`,
1663
+ "do {",
1664
+ " const page = await client.send(",
1665
+ ` new ${command}({ ...params, ExclusiveStartKey: lastEvaluatedKey })`,
1666
+ " );",
1667
+ " items.push(...(page.Items ?? []));",
1668
+ " lastEvaluatedKey = page.LastEvaluatedKey;",
1669
+ "} while (lastEvaluatedKey);",
1670
+ "",
1671
+ "console.log(items);"
1672
+ ].join("\n");
1673
+ }
1674
+ function emitCliProgram(request, paginate) {
1675
+ const command = emitCli(request);
1676
+ if (paginate) return ["# The AWS CLI auto-paginates: it follows LastEvaluatedKey and merges all pages.", command].join("\n");
1677
+ return `${command} \\\n --no-paginate`;
1678
+ }
1679
+ function emitBoto3Program(request, paginate) {
1680
+ const method = boto3MethodName(request.operation);
1681
+ const params = buildSdkV3Params(request);
1682
+ const imports = hasBinaryValue(request) ? ["import base64", "import boto3"] : ["import boto3"];
1683
+ const paramLines = Object.entries(params).map(([key, value]) => ` ${JSON.stringify(key)}: ${renderPyValue(value)},`);
1684
+ const header = [
1685
+ ...imports,
1686
+ "",
1687
+ "client = boto3.client(\"dynamodb\")",
1688
+ ""
1689
+ ];
1690
+ if (!paginate) return [
1691
+ ...header,
1692
+ "params = {",
1693
+ ...paramLines,
1694
+ "}",
1695
+ "",
1696
+ `response = client.${method}(**params)`,
1697
+ "",
1698
+ "print(response[\"Items\"])"
1699
+ ].join("\n");
1700
+ return [
1701
+ ...header,
1702
+ "params = {",
1703
+ ...paramLines,
1704
+ "}",
1705
+ "",
1706
+ "items = []",
1707
+ "while True:",
1708
+ ` response = client.${method}(**params)`,
1709
+ " items.extend(response[\"Items\"])",
1710
+ " if \"LastEvaluatedKey\" not in response:",
1711
+ " break",
1712
+ " params[\"ExclusiveStartKey\"] = response[\"LastEvaluatedKey\"]",
1713
+ "",
1714
+ "print(items)"
1715
+ ].join("\n");
1716
+ }
1717
+ function emitJavaProgram(request, paginate) {
1718
+ const requestClass = javaRequestClassName(request.operation);
1719
+ const responseClass = requestClass.replace(/Request$/, "Response");
1720
+ const method = javaClientMethodName(request.operation);
1721
+ const binary = hasBinaryValue(request);
1722
+ const usesAvMap = paginate || request.key !== void 0 || request.typedValues !== void 0 || request.exclusiveStartKey !== void 0;
1723
+ const javaUtil = [
1724
+ ...paginate ? ["java.util.ArrayList", "java.util.List"] : [],
1725
+ ...binary ? ["java.util.Base64"] : [],
1726
+ ...usesAvMap || request.names ? ["java.util.Map"] : []
1727
+ ];
1728
+ const sdk = [
1729
+ ...binary ? ["software.amazon.awssdk.core.SdkBytes"] : [],
1730
+ "software.amazon.awssdk.services.dynamodb.DynamoDbClient",
1731
+ ...usesAvMap ? ["software.amazon.awssdk.services.dynamodb.model.AttributeValue"] : [],
1732
+ `software.amazon.awssdk.services.dynamodb.model.${requestClass}`,
1733
+ `software.amazon.awssdk.services.dynamodb.model.${responseClass}`
1734
+ ];
1735
+ const header = [
1736
+ ...[...javaUtil, ...sdk].map((i) => `import ${i};`),
1737
+ "",
1738
+ "public class Main {",
1739
+ " public static void main(String[] args) {",
1740
+ " DynamoDbClient client = DynamoDbClient.create();",
1741
+ ""
1742
+ ];
1743
+ const footer = [" }", "}"];
1744
+ if (!paginate) return [
1745
+ ...header,
1746
+ ` ${requestClass} request = ${renderJavaRequestBuilder(request, " ")};`,
1747
+ "",
1748
+ ` ${responseClass} response = client.${method}(request);`,
1749
+ " System.out.println(response.items());",
1750
+ ...footer
1751
+ ].join("\n");
1752
+ const { exclusiveStartKey: startKey,...rest } = request;
1753
+ const stripped = {
1754
+ ...rest,
1755
+ config: request.config
1756
+ };
1757
+ const seed = startKey === void 0 ? " Map<String, AttributeValue> lastEvaluatedKey = null;" : ` Map<String, AttributeValue> lastEvaluatedKey = ${renderJavaAvMap(startKey, " ")};`;
1758
+ return [
1759
+ ...header,
1760
+ " List<Map<String, AttributeValue>> items = new ArrayList<>();",
1761
+ seed,
1762
+ " do {",
1763
+ ` ${requestClass} request = ${renderJavaRequestBuilder(stripped, " ", [".exclusiveStartKey(lastEvaluatedKey)"])};`,
1764
+ ` ${responseClass} response = client.${method}(request);`,
1765
+ " items.addAll(response.items());",
1766
+ " lastEvaluatedKey = response.hasLastEvaluatedKey() ? response.lastEvaluatedKey() : null;",
1767
+ " } while (lastEvaluatedKey != null);",
1768
+ "",
1769
+ " System.out.println(items);",
1770
+ ...footer
1771
+ ].join("\n");
1772
+ }
1773
+ function emitGoProgram(request, paginate) {
1774
+ const inputType = goInputTypeName(request.operation);
1775
+ const method = goClientMethodName(request.operation);
1776
+ const header = [
1777
+ "package main",
1778
+ "",
1779
+ "import (",
1780
+ ...[
1781
+ " \"context\"",
1782
+ " \"fmt\"",
1783
+ " \"log\"",
1784
+ "",
1785
+ " \"github.com/aws/aws-sdk-go-v2/aws\"",
1786
+ " \"github.com/aws/aws-sdk-go-v2/config\"",
1787
+ " \"github.com/aws/aws-sdk-go-v2/service/dynamodb\"",
1788
+ ...goUsesTypes(request) || paginate ? [" \"github.com/aws/aws-sdk-go-v2/service/dynamodb/types\""] : []
1789
+ ],
1790
+ ")",
1791
+ "",
1792
+ "func main() {",
1793
+ " cfg, err := config.LoadDefaultConfig(context.TODO())",
1794
+ " if err != nil {",
1795
+ " log.Fatal(err)",
1796
+ " }",
1797
+ " client := dynamodb.NewFromConfig(cfg)",
1798
+ "",
1799
+ `\tinput := ${renderGoInput(request, " ")}`,
1800
+ ""
1801
+ ];
1802
+ if (!paginate) return [
1803
+ ...header,
1804
+ `\tresponse, err := client.${method}(context.TODO(), input)`,
1805
+ " if err != nil {",
1806
+ " log.Fatal(err)",
1807
+ " }",
1808
+ " fmt.Println(response.Items)",
1809
+ "}"
1810
+ ].join("\n");
1811
+ return [
1812
+ ...header,
1813
+ `\tpaginator := dynamodb.New${inputType.replace(/Input$/, "")}Paginator(client, input)`,
1814
+ "",
1815
+ " var items []map[string]types.AttributeValue",
1816
+ " for paginator.HasMorePages() {",
1817
+ " page, err := paginator.NextPage(context.TODO())",
1818
+ " if err != nil {",
1819
+ " log.Fatal(err)",
1820
+ " }",
1821
+ " items = append(items, page.Items...)",
1822
+ " }",
1823
+ " fmt.Println(items)",
1824
+ "}"
1825
+ ].join("\n");
1826
+ }
1827
+ function emitDotnetProgram(request, paginate) {
1828
+ const method = dotnetClientMethodName(request.operation);
1829
+ const binary = hasBinaryValue(request);
1830
+ const header = [
1831
+ "using System;",
1832
+ ...paginate || request.key !== void 0 || request.names !== void 0 || request.typedValues !== void 0 || request.exclusiveStartKey !== void 0 ? ["using System.Collections.Generic;"] : [],
1833
+ ...binary ? ["using System.IO;"] : [],
1834
+ "using Amazon.DynamoDBv2;",
1835
+ "using Amazon.DynamoDBv2.Model;",
1836
+ "",
1837
+ "var client = new AmazonDynamoDBClient();",
1838
+ ""
1839
+ ];
1840
+ if (!paginate) return [
1841
+ ...header,
1842
+ `var request = ${renderCsRequest(request, "")};`,
1843
+ "",
1844
+ `var response = await client.${method}(request);`,
1845
+ "Console.WriteLine(response.Items.Count);"
1846
+ ].join("\n");
1847
+ const { exclusiveStartKey: startKey,...rest } = request;
1848
+ const stripped = {
1849
+ ...rest,
1850
+ config: request.config
1851
+ };
1852
+ const seed = startKey === void 0 ? "Dictionary<string, AttributeValue> lastEvaluatedKey = null;" : `Dictionary<string, AttributeValue> lastEvaluatedKey = ${renderCsAvMap(startKey, "")};`;
1853
+ return [
1854
+ ...header,
1855
+ `var request = ${renderCsRequest(stripped, "")};`,
1856
+ "",
1857
+ "var items = new List<Dictionary<string, AttributeValue>>();",
1858
+ seed,
1859
+ "do",
1860
+ "{",
1861
+ " request.ExclusiveStartKey = lastEvaluatedKey;",
1862
+ ` var response = await client.${method}(request);`,
1863
+ " items.AddRange(response.Items);",
1864
+ " lastEvaluatedKey = response.LastEvaluatedKey;",
1865
+ "} while (lastEvaluatedKey != null && lastEvaluatedKey.Count > 0);",
1866
+ "",
1867
+ "Console.WriteLine(items.Count);"
1868
+ ].join("\n");
1869
+ }
1870
+
1871
+ //#endregion
1872
+ exports.FILTER_OPERATORS = FILTER_OPERATORS;
1873
+ exports.KEY_OPERATORS = KEY_OPERATORS;
1874
+ exports.OPERATOR_BY_VALUE = OPERATOR_BY_VALUE;
1875
+ exports.SET_TYPES = SET_TYPES;
1876
+ exports.boto3MethodName = boto3MethodName;
1877
+ exports.buildFilterExpressions = buildFilterExpressions;
1878
+ exports.buildKeyConditionExpression = buildKeyConditionExpression;
1879
+ exports.buildKeyMap = buildKeyMap;
1880
+ exports.buildRequest = buildRequest;
1881
+ exports.buildSdkV3Params = buildSdkV3Params;
1882
+ exports.buildUpdateExpression = buildUpdateExpression;
1883
+ exports.dotnetClientMethodName = dotnetClientMethodName;
1884
+ exports.dotnetRequestClassName = dotnetRequestClassName;
1885
+ exports.elementType = elementType;
1886
+ exports.emitBoto3 = emitBoto3;
1887
+ exports.emitCli = emitCli;
1888
+ exports.emitDdbToolboxProgram = emitDdbToolboxProgram;
1889
+ exports.emitDotnet = emitDotnet;
1890
+ exports.emitGo = emitGo;
1891
+ exports.emitJava = emitJava;
1892
+ exports.emitPartiql = emitPartiql;
1893
+ exports.emitQueryProgram = emitQueryProgram;
1894
+ exports.emitSdkV3 = emitSdkV3;
1895
+ exports.getCompatibleComparisonOperators = getCompatibleComparisonOperators;
1896
+ exports.getCompatibleFilterOperators = getCompatibleFilterOperators;
1897
+ exports.goClientMethodName = goClientMethodName;
1898
+ exports.goInputTypeName = goInputTypeName;
1899
+ exports.hasBinaryValue = hasBinaryValue;
1900
+ exports.isSetType = isSetType;
1901
+ exports.javaClientMethodName = javaClientMethodName;
1902
+ exports.javaRequestClassName = javaRequestClassName;
1903
+ exports.makeTypedValue = makeTypedValue;
1904
+ exports.renderJsValue = renderJsValue;
1905
+ exports.renderPyValue = renderPyValue;
1906
+ exports.sdkV3CommandName = sdkV3CommandName;
1907
+ exports.typedMapToAvMap = typedMapToAvMap;
1908
+ exports.typedValueToAv = typedValueToAv;