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