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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/index.cjs +1908 -0
- package/dist/index.d.cts +639 -0
- package/dist/index.d.ts +639 -0
- package/dist/index.js +1871 -0
- package/package.json +75 -0
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 };
|