@model-ts/dynamodb 4.1.0 → 4.2.1
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/CHANGELOG.md +12 -0
- package/dist/cjs/__test__/client-env-guard.test.d.ts +1 -0
- package/dist/cjs/__test__/client-env-guard.test.js +28 -0
- package/dist/cjs/__test__/client-env-guard.test.js.map +1 -0
- package/dist/cjs/__test__/conformance.test.d.ts +1 -0
- package/dist/cjs/__test__/conformance.test.js +1835 -0
- package/dist/cjs/__test__/conformance.test.js.map +1 -0
- package/dist/cjs/__test__/in-memory.spec.test.d.ts +1 -0
- package/dist/cjs/__test__/in-memory.spec.test.js +234 -0
- package/dist/cjs/__test__/in-memory.spec.test.js.map +1 -0
- package/dist/cjs/client.d.ts +2 -2
- package/dist/cjs/client.js +14 -6
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/errors.d.ts +13 -1
- package/dist/cjs/errors.js +12 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/in-memory/document-client.d.ts +45 -0
- package/dist/cjs/in-memory/document-client.js +795 -0
- package/dist/cjs/in-memory/document-client.js.map +1 -0
- package/dist/cjs/in-memory/expression.d.ts +42 -0
- package/dist/cjs/in-memory/expression.js +665 -0
- package/dist/cjs/in-memory/expression.js.map +1 -0
- package/dist/cjs/in-memory/index.d.ts +2 -0
- package/dist/cjs/in-memory/index.js +6 -0
- package/dist/cjs/in-memory/index.js.map +1 -0
- package/dist/cjs/in-memory/spec.d.ts +23 -0
- package/dist/cjs/in-memory/spec.js +141 -0
- package/dist/cjs/in-memory/spec.js.map +1 -0
- package/dist/cjs/in-memory/store.d.ts +73 -0
- package/dist/cjs/in-memory/store.js +267 -0
- package/dist/cjs/in-memory/store.js.map +1 -0
- package/dist/cjs/in-memory/treap.d.ts +31 -0
- package/dist/cjs/in-memory/treap.js +187 -0
- package/dist/cjs/in-memory/treap.js.map +1 -0
- package/dist/cjs/in-memory/utils.d.ts +10 -0
- package/dist/cjs/in-memory/utils.js +38 -0
- package/dist/cjs/in-memory/utils.js.map +1 -0
- package/dist/cjs/sandbox.js +44 -0
- package/dist/cjs/sandbox.js.map +1 -1
- package/dist/esm/__test__/client-env-guard.test.d.ts +1 -0
- package/dist/esm/__test__/client-env-guard.test.js +26 -0
- package/dist/esm/__test__/client-env-guard.test.js.map +1 -0
- package/dist/esm/__test__/conformance.test.d.ts +1 -0
- package/dist/esm/__test__/conformance.test.js +1833 -0
- package/dist/esm/__test__/conformance.test.js.map +1 -0
- package/dist/esm/__test__/in-memory.spec.test.d.ts +1 -0
- package/dist/esm/__test__/in-memory.spec.test.js +232 -0
- package/dist/esm/__test__/in-memory.spec.test.js.map +1 -0
- package/dist/esm/client.d.ts +2 -2
- package/dist/esm/client.js +14 -6
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/errors.d.ts +13 -1
- package/dist/esm/errors.js +10 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/in-memory/document-client.d.ts +45 -0
- package/dist/esm/in-memory/document-client.js +791 -0
- package/dist/esm/in-memory/document-client.js.map +1 -0
- package/dist/esm/in-memory/expression.d.ts +42 -0
- package/dist/esm/in-memory/expression.js +657 -0
- package/dist/esm/in-memory/expression.js.map +1 -0
- package/dist/esm/in-memory/index.d.ts +2 -0
- package/dist/esm/in-memory/index.js +3 -0
- package/dist/esm/in-memory/index.js.map +1 -0
- package/dist/esm/in-memory/spec.d.ts +23 -0
- package/dist/esm/in-memory/spec.js +138 -0
- package/dist/esm/in-memory/spec.js.map +1 -0
- package/dist/esm/in-memory/store.d.ts +73 -0
- package/dist/esm/in-memory/store.js +258 -0
- package/dist/esm/in-memory/store.js.map +1 -0
- package/dist/esm/in-memory/treap.d.ts +31 -0
- package/dist/esm/in-memory/treap.js +183 -0
- package/dist/esm/in-memory/treap.js.map +1 -0
- package/dist/esm/in-memory/utils.d.ts +10 -0
- package/dist/esm/in-memory/utils.js +28 -0
- package/dist/esm/in-memory/utils.js.map +1 -0
- package/dist/esm/sandbox.js +44 -0
- package/dist/esm/sandbox.js.map +1 -1
- package/package.json +2 -1
- package/src/__test__/client-env-guard.test.ts +31 -0
- package/src/__test__/conformance.test.ts +2042 -0
- package/src/__test__/in-memory.spec.test.ts +283 -0
- package/src/client.ts +17 -4
- package/src/errors.ts +24 -0
- package/src/in-memory/document-client.ts +1140 -0
- package/src/in-memory/expression.ts +830 -0
- package/src/in-memory/index.ts +2 -0
- package/src/in-memory/spec.ts +159 -0
- package/src/in-memory/store.ts +360 -0
- package/src/in-memory/treap.ts +239 -0
- package/src/in-memory/utils.ts +45 -0
- package/src/sandbox.ts +56 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { NotSupportedError } from "../errors";
|
|
2
|
+
const MISSING = Symbol("missing");
|
|
3
|
+
const KEY_COND_BEGINS_WITH = /^(.+?)\s*=\s*(.+?)\s+and\s+begins_with\((.+?),\s*(.+?)\)$/i;
|
|
4
|
+
const KEY_COND_BETWEEN = /^(.+?)\s*=\s*(.+?)\s+and\s+(.+?)\s+between\s+(.+?)\s+and\s+(.+)$/i;
|
|
5
|
+
const KEY_COND_COMPARE = /^(.+?)\s*=\s*(.+?)\s+and\s+(.+?)\s*(<=|<|>=|>|=)\s*(.+)$/i;
|
|
6
|
+
const KEY_COND_HASH_ONLY = /^(.+?)\s*=\s*(.+)$/i;
|
|
7
|
+
export const parseKeyConditionExpression = (expression, context) => {
|
|
8
|
+
const source = expression.trim();
|
|
9
|
+
const beginsMatch = source.match(KEY_COND_BEGINS_WITH);
|
|
10
|
+
if (beginsMatch) {
|
|
11
|
+
const [, hashAttrToken, hashValueToken, rangeAttrToken, rangeValueToken] = beginsMatch;
|
|
12
|
+
const hashAttribute = resolveAttributeToken(hashAttrToken, context);
|
|
13
|
+
const rangeAttribute = resolveAttributeToken(rangeAttrToken, context);
|
|
14
|
+
const rangeValue = resolveValueToken(rangeValueToken, undefined, context);
|
|
15
|
+
if (typeof rangeValue !== "string") {
|
|
16
|
+
throw new NotSupportedError({
|
|
17
|
+
method: context.method,
|
|
18
|
+
featurePath: "KeyConditionExpression.begins_with.value",
|
|
19
|
+
reason: "begins_with currently supports string operands only.",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
hashAttribute,
|
|
24
|
+
hashValue: resolveValueToken(hashValueToken, undefined, context),
|
|
25
|
+
range: {
|
|
26
|
+
attribute: rangeAttribute,
|
|
27
|
+
condition: { type: "begins_with", value: rangeValue },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const betweenMatch = source.match(KEY_COND_BETWEEN);
|
|
32
|
+
if (betweenMatch) {
|
|
33
|
+
const [, hashAttrToken, hashValueToken, rangeAttrToken, lowerToken, upperToken] = betweenMatch;
|
|
34
|
+
return {
|
|
35
|
+
hashAttribute: resolveAttributeToken(hashAttrToken, context),
|
|
36
|
+
hashValue: resolveValueToken(hashValueToken, undefined, context),
|
|
37
|
+
range: {
|
|
38
|
+
attribute: resolveAttributeToken(rangeAttrToken, context),
|
|
39
|
+
condition: {
|
|
40
|
+
type: "between",
|
|
41
|
+
lower: resolveValueToken(lowerToken, undefined, context),
|
|
42
|
+
upper: resolveValueToken(upperToken, undefined, context),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const compareMatch = source.match(KEY_COND_COMPARE);
|
|
48
|
+
if (compareMatch) {
|
|
49
|
+
const [, hashAttrToken, hashValueToken, rangeAttrToken, operator, rangeValueToken] = compareMatch;
|
|
50
|
+
return {
|
|
51
|
+
hashAttribute: resolveAttributeToken(hashAttrToken, context),
|
|
52
|
+
hashValue: resolveValueToken(hashValueToken, undefined, context),
|
|
53
|
+
range: {
|
|
54
|
+
attribute: resolveAttributeToken(rangeAttrToken, context),
|
|
55
|
+
condition: {
|
|
56
|
+
type: operator,
|
|
57
|
+
value: resolveValueToken(rangeValueToken, undefined, context),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const hashOnlyMatch = source.match(KEY_COND_HASH_ONLY);
|
|
63
|
+
if (hashOnlyMatch) {
|
|
64
|
+
const [, hashAttrToken, hashValueToken] = hashOnlyMatch;
|
|
65
|
+
return {
|
|
66
|
+
hashAttribute: resolveAttributeToken(hashAttrToken, context),
|
|
67
|
+
hashValue: resolveValueToken(hashValueToken, undefined, context),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
throw new NotSupportedError({
|
|
71
|
+
method: context.method,
|
|
72
|
+
featurePath: "KeyConditionExpression",
|
|
73
|
+
reason: "Unsupported key condition expression grammar.",
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
export const evaluateConditionExpression = (expression, item, context) => {
|
|
77
|
+
const orGroups = splitTopLevelByKeyword(expression, "or");
|
|
78
|
+
if (orGroups.length === 0) {
|
|
79
|
+
throw new NotSupportedError({
|
|
80
|
+
method: context.method,
|
|
81
|
+
featurePath: "ConditionExpression",
|
|
82
|
+
reason: "Empty condition expressions are not supported.",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return orGroups.some((group) => {
|
|
86
|
+
const andClauses = splitTopLevelByKeyword(group, "and");
|
|
87
|
+
return andClauses.every((clause) => evaluateSingleClause(clause, item, context));
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
export const parseUpdateExpression = (expression, context) => {
|
|
91
|
+
var _a, _b;
|
|
92
|
+
const normalized = expression.trim();
|
|
93
|
+
if (!normalized) {
|
|
94
|
+
throw new NotSupportedError({
|
|
95
|
+
method: context.method,
|
|
96
|
+
featurePath: "UpdateExpression",
|
|
97
|
+
reason: "UpdateExpression must not be empty.",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const setMatch = normalized.match(/\bSET\b/i);
|
|
101
|
+
const removeMatch = normalized.match(/\bREMOVE\b/i);
|
|
102
|
+
if (!setMatch && !removeMatch) {
|
|
103
|
+
throw new NotSupportedError({
|
|
104
|
+
method: context.method,
|
|
105
|
+
featurePath: "UpdateExpression",
|
|
106
|
+
reason: "Only SET and REMOVE update operators are supported.",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const setStart = (_a = setMatch === null || setMatch === void 0 ? void 0 : setMatch.index) !== null && _a !== void 0 ? _a : -1;
|
|
110
|
+
const removeStart = (_b = removeMatch === null || removeMatch === void 0 ? void 0 : removeMatch.index) !== null && _b !== void 0 ? _b : -1;
|
|
111
|
+
let setClause = "";
|
|
112
|
+
let removeClause = "";
|
|
113
|
+
if (setStart >= 0) {
|
|
114
|
+
const setBodyStart = setStart + setMatch[0].length;
|
|
115
|
+
const setBodyEnd = removeStart >= 0 ? removeStart : normalized.length;
|
|
116
|
+
setClause = normalized.slice(setBodyStart, setBodyEnd).trim();
|
|
117
|
+
if (!setClause) {
|
|
118
|
+
throw new NotSupportedError({
|
|
119
|
+
method: context.method,
|
|
120
|
+
featurePath: "UpdateExpression.SET",
|
|
121
|
+
reason: "Malformed SET assignment.",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (removeStart >= 0) {
|
|
126
|
+
const removeBodyStart = removeStart + removeMatch[0].length;
|
|
127
|
+
removeClause = normalized.slice(removeBodyStart).trim();
|
|
128
|
+
if (!removeClause) {
|
|
129
|
+
throw new NotSupportedError({
|
|
130
|
+
method: context.method,
|
|
131
|
+
featurePath: "UpdateExpression.REMOVE",
|
|
132
|
+
reason: "Malformed REMOVE assignment.",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const set = setClause
|
|
137
|
+
? splitTopLevelByDelimiter(setClause, ",")
|
|
138
|
+
.map((segment) => segment.trim())
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.map((assignment) => {
|
|
141
|
+
const split = splitTopLevelAssignment(assignment);
|
|
142
|
+
if (!split) {
|
|
143
|
+
throw new NotSupportedError({
|
|
144
|
+
method: context.method,
|
|
145
|
+
featurePath: "UpdateExpression.SET",
|
|
146
|
+
reason: "Malformed SET assignment.",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const attribute = resolveAttributeToken(split.left, context);
|
|
150
|
+
const value = resolveUpdateSetValueToken(split.right, context.item, context);
|
|
151
|
+
return { attribute, value };
|
|
152
|
+
})
|
|
153
|
+
: [];
|
|
154
|
+
const remove = removeClause
|
|
155
|
+
? splitTopLevelByDelimiter(removeClause, ",")
|
|
156
|
+
.map((segment) => segment.trim())
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.map((token) => resolveAttributeToken(token, context))
|
|
159
|
+
: [];
|
|
160
|
+
return { set, remove };
|
|
161
|
+
};
|
|
162
|
+
export const matchesRangeCondition = (value, condition) => {
|
|
163
|
+
if (value === undefined || value === null)
|
|
164
|
+
return false;
|
|
165
|
+
switch (condition.type) {
|
|
166
|
+
case "begins_with":
|
|
167
|
+
return String(value).startsWith(condition.value);
|
|
168
|
+
case "between":
|
|
169
|
+
return compareValues(value, condition.lower) >= 0 && compareValues(value, condition.upper) <= 0;
|
|
170
|
+
case "=":
|
|
171
|
+
return compareValues(value, condition.value) === 0;
|
|
172
|
+
case "<":
|
|
173
|
+
return compareValues(value, condition.value) < 0;
|
|
174
|
+
case "<=":
|
|
175
|
+
return compareValues(value, condition.value) <= 0;
|
|
176
|
+
case ">":
|
|
177
|
+
return compareValues(value, condition.value) > 0;
|
|
178
|
+
case ">=":
|
|
179
|
+
return compareValues(value, condition.value) >= 0;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
export const compareValues = (left, right) => {
|
|
183
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
184
|
+
return left - right;
|
|
185
|
+
}
|
|
186
|
+
const leftString = String(left);
|
|
187
|
+
const rightString = String(right);
|
|
188
|
+
if (leftString < rightString)
|
|
189
|
+
return -1;
|
|
190
|
+
if (leftString > rightString)
|
|
191
|
+
return 1;
|
|
192
|
+
return 0;
|
|
193
|
+
};
|
|
194
|
+
function evaluateSingleClause(clause, item, context) {
|
|
195
|
+
const source = clause.trim();
|
|
196
|
+
const existsMatch = source.match(/^attribute_exists\((.+)\)$/i);
|
|
197
|
+
if (existsMatch) {
|
|
198
|
+
const value = resolveAttributeValue(existsMatch[1].trim(), item, context);
|
|
199
|
+
return value !== MISSING;
|
|
200
|
+
}
|
|
201
|
+
const notExistsMatch = source.match(/^attribute_not_exists\((.+)\)$/i);
|
|
202
|
+
if (notExistsMatch) {
|
|
203
|
+
const value = resolveAttributeValue(notExistsMatch[1].trim(), item, context);
|
|
204
|
+
return value === MISSING;
|
|
205
|
+
}
|
|
206
|
+
const beginsWithMatch = source.match(/^begins_with\((.+?),\s*(.+)\)$/i);
|
|
207
|
+
if (beginsWithMatch) {
|
|
208
|
+
const [, attrToken, valueToken] = beginsWithMatch;
|
|
209
|
+
const current = resolveAttributeValue(attrToken.trim(), item, context);
|
|
210
|
+
const expected = resolveValueToken(valueToken.trim(), item, context);
|
|
211
|
+
if (current === MISSING || expected === MISSING)
|
|
212
|
+
return false;
|
|
213
|
+
return String(current).startsWith(String(expected));
|
|
214
|
+
}
|
|
215
|
+
const containsMatch = source.match(/^contains\((.+?),\s*(.+)\)$/i);
|
|
216
|
+
if (containsMatch) {
|
|
217
|
+
const [, attrToken, valueToken] = containsMatch;
|
|
218
|
+
const current = resolveAttributeValue(attrToken.trim(), item, context);
|
|
219
|
+
const expected = resolveValueToken(valueToken.trim(), item, context);
|
|
220
|
+
if (current === MISSING || expected === MISSING)
|
|
221
|
+
return false;
|
|
222
|
+
return containsValue(current, expected);
|
|
223
|
+
}
|
|
224
|
+
const attributeTypeMatch = source.match(/^attribute_type\((.+?),\s*(.+)\)$/i);
|
|
225
|
+
if (attributeTypeMatch) {
|
|
226
|
+
const [, attrToken, typeToken] = attributeTypeMatch;
|
|
227
|
+
const current = resolveAttributeValue(attrToken.trim(), item, context);
|
|
228
|
+
const expectedType = resolveValueToken(typeToken.trim(), item, context);
|
|
229
|
+
if (current === MISSING)
|
|
230
|
+
return false;
|
|
231
|
+
if (typeof expectedType !== "string") {
|
|
232
|
+
throw new NotSupportedError({
|
|
233
|
+
method: context.method,
|
|
234
|
+
featurePath: "ConditionExpression.attribute_type",
|
|
235
|
+
reason: "attribute_type expects a DynamoDB type string.",
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return attributeMatchesType(current, expectedType);
|
|
239
|
+
}
|
|
240
|
+
const betweenMatch = source.match(/^(.+?)\s+between\s+(.+?)\s+and\s+(.+)$/i);
|
|
241
|
+
if (betweenMatch) {
|
|
242
|
+
const [, attrToken, lowerToken, upperToken] = betweenMatch;
|
|
243
|
+
const current = resolveAttributeValue(attrToken.trim(), item, context);
|
|
244
|
+
const lower = resolveValueToken(lowerToken.trim(), item, context);
|
|
245
|
+
const upper = resolveValueToken(upperToken.trim(), item, context);
|
|
246
|
+
if (current === MISSING || lower === MISSING || upper === MISSING)
|
|
247
|
+
return false;
|
|
248
|
+
return compareValues(current, lower) >= 0 && compareValues(current, upper) <= 0;
|
|
249
|
+
}
|
|
250
|
+
const compareMatch = source.match(/^(.+?)\s*(=|<>|<=|<|>=|>)\s*(.+)$/);
|
|
251
|
+
if (compareMatch) {
|
|
252
|
+
const [, leftToken, operator, rightToken] = compareMatch;
|
|
253
|
+
const left = resolveValueToken(leftToken.trim(), item, context);
|
|
254
|
+
const right = resolveValueToken(rightToken.trim(), item, context);
|
|
255
|
+
if (left === MISSING || right === MISSING)
|
|
256
|
+
return false;
|
|
257
|
+
const result = compareValues(left, right);
|
|
258
|
+
switch (operator) {
|
|
259
|
+
case "=":
|
|
260
|
+
return result === 0;
|
|
261
|
+
case "<>":
|
|
262
|
+
return result !== 0;
|
|
263
|
+
case "<":
|
|
264
|
+
return result < 0;
|
|
265
|
+
case "<=":
|
|
266
|
+
return result <= 0;
|
|
267
|
+
case ">":
|
|
268
|
+
return result > 0;
|
|
269
|
+
case ">=":
|
|
270
|
+
return result >= 0;
|
|
271
|
+
default:
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
throw new NotSupportedError({
|
|
276
|
+
method: context.method,
|
|
277
|
+
featurePath: "ConditionExpression",
|
|
278
|
+
reason: `Unsupported clause: ${source}`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const PLACEHOLDER_VALUE = /^:[A-Za-z_][A-Za-z0-9_]*$/;
|
|
282
|
+
const PLACEHOLDER_NAME = /^#[A-Za-z_][A-Za-z0-9_]*$/;
|
|
283
|
+
const NUMBER_LITERAL = /^-?\d+(?:\.\d+)?$/;
|
|
284
|
+
const STRING_LITERAL = /^".*"$|^'.*'$/;
|
|
285
|
+
const ATTRIBUTE_NAME = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
|
|
286
|
+
const DOCUMENT_PATH_TOKEN = /^(?:#[A-Za-z_][A-Za-z0-9_]*|[A-Za-z_][A-Za-z0-9_-]*)(?:\[\d+\])*(?:\.(?:#[A-Za-z_][A-Za-z0-9_]*|[A-Za-z_][A-Za-z0-9_-]*)(?:\[\d+\])*)*$/;
|
|
287
|
+
const ATTRIBUTE_SEGMENT = /^[A-Za-z_][A-Za-z0-9_-]*/;
|
|
288
|
+
const PLACEHOLDER_SEGMENT = /^#[A-Za-z_][A-Za-z0-9_]*/;
|
|
289
|
+
function resolveAttributeToken(token, context) {
|
|
290
|
+
var _a;
|
|
291
|
+
const trimmed = token.trim();
|
|
292
|
+
if (PLACEHOLDER_NAME.test(trimmed)) {
|
|
293
|
+
const resolved = (_a = context.expressionAttributeNames) === null || _a === void 0 ? void 0 : _a[trimmed];
|
|
294
|
+
if (!resolved) {
|
|
295
|
+
throw new NotSupportedError({
|
|
296
|
+
method: context.method,
|
|
297
|
+
featurePath: `ExpressionAttributeNames.${trimmed}`,
|
|
298
|
+
reason: "Missing expression attribute name placeholder.",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return resolved;
|
|
302
|
+
}
|
|
303
|
+
if (!ATTRIBUTE_NAME.test(trimmed)) {
|
|
304
|
+
throw new NotSupportedError({
|
|
305
|
+
method: context.method,
|
|
306
|
+
featurePath: "ExpressionAttributeNames",
|
|
307
|
+
reason: `Unsupported attribute token: ${trimmed}`,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return trimmed;
|
|
311
|
+
}
|
|
312
|
+
function resolveValueToken(token, item, context) {
|
|
313
|
+
const trimmed = token.trim();
|
|
314
|
+
if (PLACEHOLDER_VALUE.test(trimmed)) {
|
|
315
|
+
if (!context.expressionAttributeValues) {
|
|
316
|
+
throw new NotSupportedError({
|
|
317
|
+
method: context.method,
|
|
318
|
+
featurePath: "ExpressionAttributeValues",
|
|
319
|
+
reason: "ExpressionAttributeValues are required for value placeholders.",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
if (!(trimmed in context.expressionAttributeValues)) {
|
|
323
|
+
throw new NotSupportedError({
|
|
324
|
+
method: context.method,
|
|
325
|
+
featurePath: `ExpressionAttributeValues.${trimmed}`,
|
|
326
|
+
reason: "Missing expression attribute value placeholder.",
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return context.expressionAttributeValues[trimmed];
|
|
330
|
+
}
|
|
331
|
+
if (PLACEHOLDER_NAME.test(trimmed)) {
|
|
332
|
+
return resolveAttributeValue(trimmed, item, context);
|
|
333
|
+
}
|
|
334
|
+
if (STRING_LITERAL.test(trimmed)) {
|
|
335
|
+
return trimmed.slice(1, -1);
|
|
336
|
+
}
|
|
337
|
+
const sizeMatch = trimmed.match(/^size\((.+)\)$/i);
|
|
338
|
+
if (sizeMatch) {
|
|
339
|
+
const value = resolveAttributeValue(sizeMatch[1].trim(), item, context);
|
|
340
|
+
if (value === MISSING)
|
|
341
|
+
return MISSING;
|
|
342
|
+
return sizeOfValue(value, context);
|
|
343
|
+
}
|
|
344
|
+
if (NUMBER_LITERAL.test(trimmed)) {
|
|
345
|
+
return Number(trimmed);
|
|
346
|
+
}
|
|
347
|
+
if (trimmed === "true")
|
|
348
|
+
return true;
|
|
349
|
+
if (trimmed === "false")
|
|
350
|
+
return false;
|
|
351
|
+
if (trimmed === "null")
|
|
352
|
+
return null;
|
|
353
|
+
if (DOCUMENT_PATH_TOKEN.test(trimmed) || ATTRIBUTE_NAME.test(trimmed)) {
|
|
354
|
+
return resolveAttributeValue(trimmed, item, context);
|
|
355
|
+
}
|
|
356
|
+
throw new NotSupportedError({
|
|
357
|
+
method: context.method,
|
|
358
|
+
featurePath: "ExpressionValue",
|
|
359
|
+
reason: `Unsupported value token: ${trimmed}`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function resolveAttributeValue(token, item, context) {
|
|
363
|
+
const path = parseDocumentPath(token, context);
|
|
364
|
+
if (!item)
|
|
365
|
+
return MISSING;
|
|
366
|
+
let current = item;
|
|
367
|
+
for (const part of path) {
|
|
368
|
+
if (part.type === "attribute") {
|
|
369
|
+
if (!current || typeof current !== "object")
|
|
370
|
+
return MISSING;
|
|
371
|
+
if (!(part.value in current))
|
|
372
|
+
return MISSING;
|
|
373
|
+
current = current[part.value];
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (!Array.isArray(current))
|
|
377
|
+
return MISSING;
|
|
378
|
+
if (!(part.value in current))
|
|
379
|
+
return MISSING;
|
|
380
|
+
current = current[part.value];
|
|
381
|
+
}
|
|
382
|
+
return current;
|
|
383
|
+
}
|
|
384
|
+
function parseDocumentPath(token, context) {
|
|
385
|
+
var _a, _b;
|
|
386
|
+
const source = token.trim();
|
|
387
|
+
if (!source) {
|
|
388
|
+
throwUnsupportedAttributeToken(token, context);
|
|
389
|
+
}
|
|
390
|
+
const parts = [];
|
|
391
|
+
let cursor = 0;
|
|
392
|
+
while (cursor < source.length) {
|
|
393
|
+
const remaining = source.slice(cursor);
|
|
394
|
+
const placeholderMatch = remaining.match(PLACEHOLDER_SEGMENT);
|
|
395
|
+
const segmentMatch = remaining.match(ATTRIBUTE_SEGMENT);
|
|
396
|
+
const segmentToken = (_a = placeholderMatch === null || placeholderMatch === void 0 ? void 0 : placeholderMatch[0]) !== null && _a !== void 0 ? _a : segmentMatch === null || segmentMatch === void 0 ? void 0 : segmentMatch[0];
|
|
397
|
+
if (!segmentToken) {
|
|
398
|
+
throwUnsupportedAttributeToken(token, context);
|
|
399
|
+
}
|
|
400
|
+
if (segmentToken.startsWith("#")) {
|
|
401
|
+
const resolved = (_b = context.expressionAttributeNames) === null || _b === void 0 ? void 0 : _b[segmentToken];
|
|
402
|
+
if (!resolved) {
|
|
403
|
+
throw new NotSupportedError({
|
|
404
|
+
method: context.method,
|
|
405
|
+
featurePath: `ExpressionAttributeNames.${segmentToken}`,
|
|
406
|
+
reason: "Missing expression attribute name placeholder.",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
parts.push({ type: "attribute", value: resolved });
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
parts.push({ type: "attribute", value: segmentToken });
|
|
413
|
+
}
|
|
414
|
+
cursor += segmentToken.length;
|
|
415
|
+
while (source[cursor] === "[") {
|
|
416
|
+
const start = cursor + 1;
|
|
417
|
+
let end = start;
|
|
418
|
+
while (end < source.length && /\d/.test(source[end]))
|
|
419
|
+
end += 1;
|
|
420
|
+
if (start === end || source[end] !== "]") {
|
|
421
|
+
throwUnsupportedAttributeToken(token, context);
|
|
422
|
+
}
|
|
423
|
+
parts.push({
|
|
424
|
+
type: "index",
|
|
425
|
+
value: Number(source.slice(start, end)),
|
|
426
|
+
});
|
|
427
|
+
cursor = end + 1;
|
|
428
|
+
}
|
|
429
|
+
if (cursor >= source.length)
|
|
430
|
+
break;
|
|
431
|
+
if (source[cursor] !== ".") {
|
|
432
|
+
throwUnsupportedAttributeToken(token, context);
|
|
433
|
+
}
|
|
434
|
+
cursor += 1;
|
|
435
|
+
if (cursor >= source.length) {
|
|
436
|
+
throwUnsupportedAttributeToken(token, context);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return parts;
|
|
440
|
+
}
|
|
441
|
+
function throwUnsupportedAttributeToken(token, context) {
|
|
442
|
+
throw new NotSupportedError({
|
|
443
|
+
method: context.method,
|
|
444
|
+
featurePath: "ExpressionAttributeNames",
|
|
445
|
+
reason: `Unsupported attribute token: ${token.trim()}`,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function splitTopLevelByKeyword(expression, keyword) {
|
|
449
|
+
const clauses = [];
|
|
450
|
+
let current = "";
|
|
451
|
+
let depth = 0;
|
|
452
|
+
const marker = ` ${keyword} `;
|
|
453
|
+
for (let i = 0; i < expression.length; i += 1) {
|
|
454
|
+
const char = expression[i];
|
|
455
|
+
if (char === "(")
|
|
456
|
+
depth += 1;
|
|
457
|
+
if (char === ")")
|
|
458
|
+
depth = Math.max(0, depth - 1);
|
|
459
|
+
if (depth === 0 &&
|
|
460
|
+
expression.slice(i, i + marker.length).toLowerCase() === marker) {
|
|
461
|
+
clauses.push(current.trim());
|
|
462
|
+
current = "";
|
|
463
|
+
i += marker.length - 1;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
current += char;
|
|
467
|
+
}
|
|
468
|
+
if (current.trim())
|
|
469
|
+
clauses.push(current.trim());
|
|
470
|
+
return clauses;
|
|
471
|
+
}
|
|
472
|
+
function splitTopLevelByDelimiter(expression, delimiter) {
|
|
473
|
+
const segments = [];
|
|
474
|
+
let current = "";
|
|
475
|
+
let depth = 0;
|
|
476
|
+
for (let i = 0; i < expression.length; i += 1) {
|
|
477
|
+
const char = expression[i];
|
|
478
|
+
if (char === "(")
|
|
479
|
+
depth += 1;
|
|
480
|
+
if (char === ")")
|
|
481
|
+
depth = Math.max(0, depth - 1);
|
|
482
|
+
if (depth === 0 && char === delimiter) {
|
|
483
|
+
segments.push(current);
|
|
484
|
+
current = "";
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
current += char;
|
|
488
|
+
}
|
|
489
|
+
if (current.length > 0)
|
|
490
|
+
segments.push(current);
|
|
491
|
+
return segments;
|
|
492
|
+
}
|
|
493
|
+
function splitTopLevelAssignment(assignment) {
|
|
494
|
+
let depth = 0;
|
|
495
|
+
for (let i = 0; i < assignment.length; i += 1) {
|
|
496
|
+
const char = assignment[i];
|
|
497
|
+
if (char === "(")
|
|
498
|
+
depth += 1;
|
|
499
|
+
if (char === ")")
|
|
500
|
+
depth = Math.max(0, depth - 1);
|
|
501
|
+
if (depth === 0 && char === "=") {
|
|
502
|
+
const left = assignment.slice(0, i).trim();
|
|
503
|
+
const right = assignment.slice(i + 1).trim();
|
|
504
|
+
if (!left || !right)
|
|
505
|
+
return null;
|
|
506
|
+
return { left, right };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
function resolveUpdateSetValueToken(token, item, context) {
|
|
512
|
+
const trimmed = token.trim();
|
|
513
|
+
const arithmetic = splitTopLevelArithmetic(trimmed);
|
|
514
|
+
if (arithmetic) {
|
|
515
|
+
const left = resolveUpdateSetValueToken(arithmetic.left, item, context);
|
|
516
|
+
const right = resolveUpdateSetValueToken(arithmetic.right, item, context);
|
|
517
|
+
if (typeof left !== "number" || typeof right !== "number") {
|
|
518
|
+
throw new NotSupportedError({
|
|
519
|
+
method: context.method,
|
|
520
|
+
featurePath: "UpdateExpression.SET",
|
|
521
|
+
reason: "Arithmetic update operands must be numbers.",
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return arithmetic.operator === "+" ? left + right : left - right;
|
|
525
|
+
}
|
|
526
|
+
const ifNotExists = parseFunctionCall(trimmed, "if_not_exists");
|
|
527
|
+
if (ifNotExists) {
|
|
528
|
+
if (ifNotExists.length !== 2) {
|
|
529
|
+
throw new NotSupportedError({
|
|
530
|
+
method: context.method,
|
|
531
|
+
featurePath: "UpdateExpression.SET",
|
|
532
|
+
reason: "if_not_exists expects exactly two arguments.",
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const existing = resolveAttributeValue(ifNotExists[0], item, context);
|
|
536
|
+
if (existing !== MISSING)
|
|
537
|
+
return existing;
|
|
538
|
+
return resolveUpdateSetValueToken(ifNotExists[1], item, context);
|
|
539
|
+
}
|
|
540
|
+
const listAppend = parseFunctionCall(trimmed, "list_append");
|
|
541
|
+
if (listAppend) {
|
|
542
|
+
if (listAppend.length !== 2) {
|
|
543
|
+
throw new NotSupportedError({
|
|
544
|
+
method: context.method,
|
|
545
|
+
featurePath: "UpdateExpression.SET",
|
|
546
|
+
reason: "list_append expects exactly two arguments.",
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
const left = resolveUpdateSetValueToken(listAppend[0], item, context);
|
|
550
|
+
const right = resolveUpdateSetValueToken(listAppend[1], item, context);
|
|
551
|
+
if (!Array.isArray(left) || !Array.isArray(right)) {
|
|
552
|
+
throw new NotSupportedError({
|
|
553
|
+
method: context.method,
|
|
554
|
+
featurePath: "UpdateExpression.SET",
|
|
555
|
+
reason: "list_append expects list operands.",
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
return [...left, ...right];
|
|
559
|
+
}
|
|
560
|
+
const value = resolveValueToken(trimmed, item, context);
|
|
561
|
+
if (value === MISSING) {
|
|
562
|
+
throw new NotSupportedError({
|
|
563
|
+
method: context.method,
|
|
564
|
+
featurePath: "UpdateExpression.SET",
|
|
565
|
+
reason: "The provided expression refers to an attribute that does not exist in the item.",
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
return value;
|
|
569
|
+
}
|
|
570
|
+
function splitTopLevelArithmetic(source) {
|
|
571
|
+
let depth = 0;
|
|
572
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
573
|
+
const char = source[i];
|
|
574
|
+
if (char === "(")
|
|
575
|
+
depth += 1;
|
|
576
|
+
if (char === ")")
|
|
577
|
+
depth = Math.max(0, depth - 1);
|
|
578
|
+
if (depth !== 0)
|
|
579
|
+
continue;
|
|
580
|
+
if (char === "+" || char === "-") {
|
|
581
|
+
const left = source.slice(0, i).trim();
|
|
582
|
+
const right = source.slice(i + 1).trim();
|
|
583
|
+
if (!left || !right)
|
|
584
|
+
continue;
|
|
585
|
+
return {
|
|
586
|
+
left,
|
|
587
|
+
right,
|
|
588
|
+
operator: char,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
function parseFunctionCall(source, fnName) {
|
|
595
|
+
const regex = new RegExp(`^${fnName}\\((.*)\\)$`, "i");
|
|
596
|
+
const match = source.match(regex);
|
|
597
|
+
if (!match)
|
|
598
|
+
return null;
|
|
599
|
+
return splitTopLevelByDelimiter(match[1], ",")
|
|
600
|
+
.map((segment) => segment.trim())
|
|
601
|
+
.filter(Boolean);
|
|
602
|
+
}
|
|
603
|
+
function sizeOfValue(value, context) {
|
|
604
|
+
if (typeof value === "string" || Array.isArray(value))
|
|
605
|
+
return value.length;
|
|
606
|
+
if (value instanceof Uint8Array || Buffer.isBuffer(value))
|
|
607
|
+
return value.length;
|
|
608
|
+
if (value instanceof Set)
|
|
609
|
+
return value.size;
|
|
610
|
+
if (value && typeof value === "object")
|
|
611
|
+
return Object.keys(value).length;
|
|
612
|
+
throw new NotSupportedError({
|
|
613
|
+
method: context.method,
|
|
614
|
+
featurePath: "ConditionExpression.size",
|
|
615
|
+
reason: "size supports string, binary, list, set, and map values only.",
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function containsValue(container, expected) {
|
|
619
|
+
if (typeof container === "string")
|
|
620
|
+
return container.includes(String(expected));
|
|
621
|
+
if (Array.isArray(container))
|
|
622
|
+
return container.some((entry) => entry === expected);
|
|
623
|
+
if (container instanceof Set)
|
|
624
|
+
return container.has(expected);
|
|
625
|
+
if (container && typeof container === "object") {
|
|
626
|
+
return Object.prototype.hasOwnProperty.call(container, String(expected));
|
|
627
|
+
}
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
function attributeMatchesType(value, expectedType) {
|
|
631
|
+
const t = expectedType.toUpperCase();
|
|
632
|
+
if (t === "S")
|
|
633
|
+
return typeof value === "string";
|
|
634
|
+
if (t === "N")
|
|
635
|
+
return typeof value === "number";
|
|
636
|
+
if (t === "BOOL")
|
|
637
|
+
return typeof value === "boolean";
|
|
638
|
+
if (t === "NULL")
|
|
639
|
+
return value === null;
|
|
640
|
+
if (t === "L")
|
|
641
|
+
return Array.isArray(value);
|
|
642
|
+
if (t === "M")
|
|
643
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && !Buffer.isBuffer(value) && !(value instanceof Set);
|
|
644
|
+
if (t === "B")
|
|
645
|
+
return Buffer.isBuffer(value) || value instanceof Uint8Array;
|
|
646
|
+
if (value instanceof Set) {
|
|
647
|
+
const values = [...value.values()];
|
|
648
|
+
if (t === "SS")
|
|
649
|
+
return values.every((entry) => typeof entry === "string");
|
|
650
|
+
if (t === "NS")
|
|
651
|
+
return values.every((entry) => typeof entry === "number");
|
|
652
|
+
if (t === "BS")
|
|
653
|
+
return values.every((entry) => Buffer.isBuffer(entry) || entry instanceof Uint8Array);
|
|
654
|
+
}
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
//# sourceMappingURL=expression.js.map
|