@awsless/dynamodb-server 0.0.12 → 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/README.MD +149 -0
- package/dist/index.d.ts +385 -14
- package/dist/index.js +3102 -75
- package/package.json +21 -9
- package/dist/index.d.mts +0 -24
- package/dist/index.mjs +0 -84
package/dist/index.js
CHANGED
|
@@ -1,88 +1,3033 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
// src/dynamodb-server.ts
|
|
2
|
+
import { DynamoDBClient as DynamoDBClient2 } from "@aws-sdk/client-dynamodb";
|
|
3
|
+
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
4
|
+
|
|
5
|
+
// src/clock.ts
|
|
6
|
+
var VirtualClock = class {
|
|
7
|
+
offset = 0;
|
|
8
|
+
now() {
|
|
9
|
+
return Date.now() + this.offset;
|
|
10
|
+
}
|
|
11
|
+
nowInSeconds() {
|
|
12
|
+
return Math.floor(this.now() / 1e3);
|
|
13
|
+
}
|
|
14
|
+
advance(ms) {
|
|
15
|
+
this.offset += ms;
|
|
16
|
+
}
|
|
17
|
+
set(timestamp) {
|
|
18
|
+
this.offset = timestamp - Date.now();
|
|
19
|
+
}
|
|
20
|
+
reset() {
|
|
21
|
+
this.offset = 0;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/java-server.ts
|
|
26
|
+
import { DynamoDBClient, ListTablesCommand } from "@aws-sdk/client-dynamodb";
|
|
27
|
+
import { spawn } from "dynamo-db-local";
|
|
28
|
+
function sleep(ms) {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
31
|
+
function createJavaServer(port, region) {
|
|
32
|
+
const childProcess = spawn({ port });
|
|
33
|
+
const getClient = () => new DynamoDBClient({
|
|
34
|
+
endpoint: `http://localhost:${port}`,
|
|
35
|
+
region,
|
|
36
|
+
credentials: {
|
|
37
|
+
accessKeyId: "fake",
|
|
38
|
+
secretAccessKey: "fake"
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const ping = async () => {
|
|
42
|
+
const client = getClient();
|
|
43
|
+
try {
|
|
44
|
+
const response = await client.send(new ListTablesCommand({}));
|
|
45
|
+
return Array.isArray(response.TableNames);
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const wait = async (times = 10) => {
|
|
51
|
+
while (times--) {
|
|
52
|
+
if (await ping()) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await sleep(100 * (times + 1));
|
|
56
|
+
}
|
|
57
|
+
throw new Error("DynamoDB server is unavailable");
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
port,
|
|
61
|
+
stop: async () => {
|
|
62
|
+
childProcess.kill();
|
|
63
|
+
},
|
|
64
|
+
ping,
|
|
65
|
+
wait
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/server.ts
|
|
70
|
+
import {
|
|
71
|
+
createServer as createHttpServer
|
|
72
|
+
} from "node:http";
|
|
73
|
+
|
|
74
|
+
// src/errors/index.ts
|
|
75
|
+
var DynamoDBError = class extends Error {
|
|
76
|
+
__type;
|
|
77
|
+
statusCode;
|
|
78
|
+
constructor(type, message, statusCode = 400) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.__type = `com.amazonaws.dynamodb.v20120810#${type}`;
|
|
81
|
+
this.statusCode = statusCode;
|
|
82
|
+
}
|
|
83
|
+
toJSON() {
|
|
84
|
+
return {
|
|
85
|
+
__type: this.__type,
|
|
86
|
+
message: this.message
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var ValidationException = class extends DynamoDBError {
|
|
91
|
+
constructor(message) {
|
|
92
|
+
super("ValidationException", message, 400);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var ResourceNotFoundException = class extends DynamoDBError {
|
|
96
|
+
constructor(message) {
|
|
97
|
+
super("ResourceNotFoundException", message, 400);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var ResourceInUseException = class extends DynamoDBError {
|
|
101
|
+
constructor(message) {
|
|
102
|
+
super("ResourceInUseException", message, 400);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var ConditionalCheckFailedException = class extends DynamoDBError {
|
|
106
|
+
Item;
|
|
107
|
+
constructor(message = "The conditional request failed", item) {
|
|
108
|
+
super("ConditionalCheckFailedException", message, 400);
|
|
109
|
+
this.Item = item;
|
|
110
|
+
}
|
|
111
|
+
toJSON() {
|
|
112
|
+
return {
|
|
113
|
+
__type: this.__type,
|
|
114
|
+
message: this.message,
|
|
115
|
+
...this.Item && { Item: this.Item }
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var TransactionCanceledException = class extends DynamoDBError {
|
|
120
|
+
CancellationReasons;
|
|
121
|
+
constructor(message, reasons) {
|
|
122
|
+
super("TransactionCanceledException", message, 400);
|
|
123
|
+
this.CancellationReasons = reasons;
|
|
124
|
+
}
|
|
125
|
+
toJSON() {
|
|
126
|
+
return {
|
|
127
|
+
__type: this.__type,
|
|
128
|
+
message: this.message,
|
|
129
|
+
CancellationReasons: this.CancellationReasons
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var TransactionConflictException = class extends DynamoDBError {
|
|
134
|
+
constructor(message = "Transaction is ongoing for the item") {
|
|
135
|
+
super("TransactionConflictException", message, 400);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var ProvisionedThroughputExceededException = class extends DynamoDBError {
|
|
139
|
+
constructor(message = "The level of configured provisioned throughput for the table was exceeded") {
|
|
140
|
+
super("ProvisionedThroughputExceededException", message, 400);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var ItemCollectionSizeLimitExceededException = class extends DynamoDBError {
|
|
144
|
+
constructor(message = "Collection size exceeded") {
|
|
145
|
+
super("ItemCollectionSizeLimitExceededException", message, 400);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
var InternalServerError = class extends DynamoDBError {
|
|
149
|
+
constructor(message = "Internal server error") {
|
|
150
|
+
super("InternalServerError", message, 500);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var SerializationException = class extends DynamoDBError {
|
|
154
|
+
constructor(message) {
|
|
155
|
+
super("SerializationException", message, 400);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var IdempotentParameterMismatchException = class extends DynamoDBError {
|
|
159
|
+
constructor(message = "The request uses the same client token as a previous, but non-identical request") {
|
|
160
|
+
super("IdempotentParameterMismatchException", message, 400);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/operations/create-table.ts
|
|
165
|
+
function validateAndCreate(store, input) {
|
|
166
|
+
if (!input.TableName) {
|
|
167
|
+
throw new ValidationException("TableName is required");
|
|
168
|
+
}
|
|
169
|
+
if (!input.KeySchema || input.KeySchema.length === 0) {
|
|
170
|
+
throw new ValidationException("KeySchema is required");
|
|
171
|
+
}
|
|
172
|
+
if (!input.AttributeDefinitions || input.AttributeDefinitions.length === 0) {
|
|
173
|
+
throw new ValidationException("AttributeDefinitions is required");
|
|
174
|
+
}
|
|
175
|
+
const hashKeys = input.KeySchema.filter((k) => k.KeyType === "HASH");
|
|
176
|
+
if (hashKeys.length !== 1) {
|
|
177
|
+
throw new ValidationException("Exactly one hash key is required");
|
|
178
|
+
}
|
|
179
|
+
const rangeKeys = input.KeySchema.filter((k) => k.KeyType === "RANGE");
|
|
180
|
+
if (rangeKeys.length > 1) {
|
|
181
|
+
throw new ValidationException("At most one range key is allowed");
|
|
182
|
+
}
|
|
183
|
+
const definedAttrs = new Set(input.AttributeDefinitions.map((a) => a.AttributeName));
|
|
184
|
+
for (const keyElement of input.KeySchema) {
|
|
185
|
+
if (!definedAttrs.has(keyElement.AttributeName)) {
|
|
186
|
+
throw new ValidationException(
|
|
187
|
+
`Attribute ${keyElement.AttributeName} is specified in KeySchema but not in AttributeDefinitions`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (input.LocalSecondaryIndexes) {
|
|
192
|
+
const tableHashKey = hashKeys[0].AttributeName;
|
|
193
|
+
for (const lsi of input.LocalSecondaryIndexes) {
|
|
194
|
+
const lsiHashKey = lsi.KeySchema.find((k) => k.KeyType === "HASH");
|
|
195
|
+
if (!lsiHashKey || lsiHashKey.AttributeName !== tableHashKey) {
|
|
196
|
+
throw new ValidationException(
|
|
197
|
+
`Local secondary index ${lsi.IndexName} must have the same hash key as the table`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const table = store.createTable({
|
|
203
|
+
TableName: input.TableName,
|
|
204
|
+
KeySchema: input.KeySchema,
|
|
205
|
+
AttributeDefinitions: input.AttributeDefinitions,
|
|
206
|
+
ProvisionedThroughput: input.ProvisionedThroughput,
|
|
207
|
+
BillingMode: input.BillingMode,
|
|
208
|
+
GlobalSecondaryIndexes: input.GlobalSecondaryIndexes,
|
|
209
|
+
LocalSecondaryIndexes: input.LocalSecondaryIndexes,
|
|
210
|
+
StreamSpecification: input.StreamSpecification
|
|
211
|
+
});
|
|
212
|
+
return table.describe();
|
|
213
|
+
}
|
|
214
|
+
function createTable(store, input) {
|
|
215
|
+
const tableDescription = validateAndCreate(store, input);
|
|
216
|
+
return { TableDescription: tableDescription };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/operations/delete-table.ts
|
|
220
|
+
function deleteTable(store, input) {
|
|
221
|
+
if (!input.TableName) {
|
|
222
|
+
throw new ValidationException("TableName is required");
|
|
223
|
+
}
|
|
224
|
+
const table = store.deleteTable(input.TableName);
|
|
225
|
+
const description = table.describe();
|
|
226
|
+
return {
|
|
227
|
+
TableDescription: {
|
|
228
|
+
...description,
|
|
229
|
+
TableStatus: "DELETING"
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/operations/describe-table.ts
|
|
235
|
+
function describeTable(store, input) {
|
|
236
|
+
if (!input.TableName) {
|
|
237
|
+
throw new ValidationException("TableName is required");
|
|
238
|
+
}
|
|
239
|
+
const table = store.getTable(input.TableName);
|
|
240
|
+
return {
|
|
241
|
+
Table: table.describe()
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/operations/list-tables.ts
|
|
246
|
+
function listTables(store, input) {
|
|
247
|
+
const result = store.listTables(input.ExclusiveStartTableName, input.Limit);
|
|
248
|
+
return {
|
|
249
|
+
TableNames: result.tableNames,
|
|
250
|
+
LastEvaluatedTableName: result.lastEvaluatedTableName
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/expressions/path.ts
|
|
255
|
+
function parsePath(path, attributeNames) {
|
|
256
|
+
const segments = [];
|
|
257
|
+
let current = "";
|
|
258
|
+
let i = 0;
|
|
259
|
+
while (i < path.length) {
|
|
260
|
+
const char = path[i];
|
|
261
|
+
if (char === ".") {
|
|
262
|
+
if (current) {
|
|
263
|
+
segments.push({ type: "attribute", value: resolveAttributeName(current, attributeNames) });
|
|
264
|
+
current = "";
|
|
265
|
+
}
|
|
266
|
+
i++;
|
|
267
|
+
} else if (char === "[") {
|
|
268
|
+
if (current) {
|
|
269
|
+
segments.push({ type: "attribute", value: resolveAttributeName(current, attributeNames) });
|
|
270
|
+
current = "";
|
|
271
|
+
}
|
|
272
|
+
i++;
|
|
273
|
+
let indexStr = "";
|
|
274
|
+
while (i < path.length && path[i] !== "]") {
|
|
275
|
+
indexStr += path[i];
|
|
276
|
+
i++;
|
|
277
|
+
}
|
|
278
|
+
i++;
|
|
279
|
+
segments.push({ type: "index", value: parseInt(indexStr, 10) });
|
|
280
|
+
} else {
|
|
281
|
+
current += char;
|
|
282
|
+
i++;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (current) {
|
|
286
|
+
segments.push({ type: "attribute", value: resolveAttributeName(current, attributeNames) });
|
|
287
|
+
}
|
|
288
|
+
return segments;
|
|
289
|
+
}
|
|
290
|
+
function resolveAttributeName(name, attributeNames) {
|
|
291
|
+
if (name.startsWith("#") && attributeNames) {
|
|
292
|
+
const resolved = attributeNames[name];
|
|
293
|
+
if (resolved !== void 0) {
|
|
294
|
+
return resolved;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return name;
|
|
298
|
+
}
|
|
299
|
+
function getValueAtPath(item, segments) {
|
|
300
|
+
let current = { M: item };
|
|
301
|
+
for (const segment of segments) {
|
|
302
|
+
if (current === void 0) {
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
if (segment.type === "attribute") {
|
|
306
|
+
if ("M" in current) {
|
|
307
|
+
const map = current.M;
|
|
308
|
+
current = map[segment.value];
|
|
309
|
+
} else {
|
|
310
|
+
return void 0;
|
|
311
|
+
}
|
|
312
|
+
} else if (segment.type === "index") {
|
|
313
|
+
if ("L" in current) {
|
|
314
|
+
const list = current.L;
|
|
315
|
+
current = list[segment.value];
|
|
316
|
+
} else {
|
|
317
|
+
return void 0;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return current;
|
|
322
|
+
}
|
|
323
|
+
function setValueAtPath(item, segments, value) {
|
|
324
|
+
if (segments.length === 0) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
let current = { M: item };
|
|
328
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
329
|
+
const segment = segments[i];
|
|
330
|
+
const nextSegment = segments[i + 1];
|
|
331
|
+
if (segment.type === "attribute") {
|
|
332
|
+
if (!("M" in current)) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const map = current.M;
|
|
336
|
+
const attrName = segment.value;
|
|
337
|
+
if (!map[attrName]) {
|
|
338
|
+
if (nextSegment.type === "attribute") {
|
|
339
|
+
map[attrName] = { M: {} };
|
|
340
|
+
} else {
|
|
341
|
+
map[attrName] = { L: [] };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
current = map[attrName];
|
|
345
|
+
} else if (segment.type === "index") {
|
|
346
|
+
if (!("L" in current)) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const list = current.L;
|
|
350
|
+
const idx = segment.value;
|
|
351
|
+
if (!list[idx]) {
|
|
352
|
+
if (nextSegment.type === "attribute") {
|
|
353
|
+
list[idx] = { M: {} };
|
|
354
|
+
} else {
|
|
355
|
+
list[idx] = { L: [] };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
current = list[idx];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const lastSegment = segments[segments.length - 1];
|
|
362
|
+
if (lastSegment.type === "attribute") {
|
|
363
|
+
if ("M" in current) {
|
|
364
|
+
const map = current.M;
|
|
365
|
+
map[lastSegment.value] = value;
|
|
366
|
+
}
|
|
367
|
+
} else if (lastSegment.type === "index") {
|
|
368
|
+
if ("L" in current) {
|
|
369
|
+
const list = current.L;
|
|
370
|
+
list[lastSegment.value] = value;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function deleteValueAtPath(item, segments) {
|
|
375
|
+
if (segments.length === 0) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
let current = { M: item };
|
|
379
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
380
|
+
const segment = segments[i];
|
|
381
|
+
if (segment.type === "attribute") {
|
|
382
|
+
if (!("M" in current)) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
const map = current.M;
|
|
386
|
+
if (!map[segment.value]) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
current = map[segment.value];
|
|
390
|
+
} else if (segment.type === "index") {
|
|
391
|
+
if (!("L" in current)) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
const list = current.L;
|
|
395
|
+
if (list[segment.value] === void 0) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
current = list[segment.value];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const lastSegment = segments[segments.length - 1];
|
|
402
|
+
if (lastSegment.type === "attribute") {
|
|
403
|
+
if ("M" in current) {
|
|
404
|
+
const map = current.M;
|
|
405
|
+
delete map[lastSegment.value];
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
} else if (lastSegment.type === "index") {
|
|
409
|
+
if ("L" in current) {
|
|
410
|
+
const list = current.L;
|
|
411
|
+
list.splice(lastSegment.value, 1);
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/expressions/condition.ts
|
|
419
|
+
function tokenize(expression) {
|
|
420
|
+
const tokens = [];
|
|
421
|
+
let i = 0;
|
|
422
|
+
const keywords = {
|
|
423
|
+
AND: "AND",
|
|
424
|
+
OR: "OR",
|
|
425
|
+
NOT: "NOT",
|
|
426
|
+
BETWEEN: "BETWEEN",
|
|
427
|
+
IN: "IN"
|
|
428
|
+
};
|
|
429
|
+
const functions = ["attribute_exists", "attribute_not_exists", "attribute_type", "begins_with", "contains", "size"];
|
|
430
|
+
while (i < expression.length) {
|
|
431
|
+
const char = expression[i];
|
|
432
|
+
if (/\s/.test(char)) {
|
|
433
|
+
i++;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (char === "(") {
|
|
437
|
+
tokens.push({ type: "LPAREN", value: "(" });
|
|
438
|
+
i++;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (char === ")") {
|
|
442
|
+
tokens.push({ type: "RPAREN", value: ")" });
|
|
443
|
+
i++;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (char === ",") {
|
|
447
|
+
tokens.push({ type: "COMMA", value: "," });
|
|
448
|
+
i++;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (char === "=" || char === "<" || char === ">") {
|
|
452
|
+
let op = char;
|
|
453
|
+
if (expression[i + 1] === "=") {
|
|
454
|
+
op += "=";
|
|
455
|
+
i++;
|
|
456
|
+
} else if (char === "<" && expression[i + 1] === ">") {
|
|
457
|
+
op = "<>";
|
|
458
|
+
i++;
|
|
459
|
+
}
|
|
460
|
+
tokens.push({ type: "COMPARATOR", value: op });
|
|
461
|
+
i++;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (char === ":") {
|
|
465
|
+
let value = ":";
|
|
466
|
+
i++;
|
|
467
|
+
while (i < expression.length && /[a-zA-Z0-9_]/.test(expression[i])) {
|
|
468
|
+
value += expression[i];
|
|
469
|
+
i++;
|
|
470
|
+
}
|
|
471
|
+
tokens.push({ type: "VALUE", value });
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (char === "#") {
|
|
475
|
+
let value = "#";
|
|
476
|
+
i++;
|
|
477
|
+
while (i < expression.length && /[a-zA-Z0-9_]/.test(expression[i])) {
|
|
478
|
+
value += expression[i];
|
|
479
|
+
i++;
|
|
480
|
+
}
|
|
481
|
+
tokens.push({ type: "PATH", value });
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (/[a-zA-Z_]/.test(char)) {
|
|
485
|
+
let word = "";
|
|
486
|
+
while (i < expression.length && /[a-zA-Z0-9_]/.test(expression[i])) {
|
|
487
|
+
word += expression[i];
|
|
488
|
+
i++;
|
|
489
|
+
}
|
|
490
|
+
const upper = word.toUpperCase();
|
|
491
|
+
if (keywords[upper]) {
|
|
492
|
+
tokens.push({ type: keywords[upper], value: upper });
|
|
493
|
+
} else if (functions.includes(word)) {
|
|
494
|
+
tokens.push({ type: "FUNCTION", value: word });
|
|
495
|
+
} else {
|
|
496
|
+
tokens.push({ type: "PATH", value: word });
|
|
497
|
+
}
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (char === "[") {
|
|
501
|
+
let value = "";
|
|
502
|
+
while (i < expression.length && expression[i] !== "]") {
|
|
503
|
+
value += expression[i];
|
|
504
|
+
i++;
|
|
505
|
+
}
|
|
506
|
+
value += "]";
|
|
507
|
+
i++;
|
|
508
|
+
if (tokens.length > 0 && tokens[tokens.length - 1].type === "PATH") {
|
|
509
|
+
tokens[tokens.length - 1].value += value;
|
|
510
|
+
}
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (char === ".") {
|
|
514
|
+
if (tokens.length > 0 && tokens[tokens.length - 1].type === "PATH") {
|
|
515
|
+
i++;
|
|
516
|
+
let pathPart = ".";
|
|
517
|
+
while (i < expression.length && /[a-zA-Z0-9_#\[\]]/.test(expression[i])) {
|
|
518
|
+
pathPart += expression[i];
|
|
519
|
+
i++;
|
|
520
|
+
}
|
|
521
|
+
tokens[tokens.length - 1].value += pathPart;
|
|
522
|
+
} else {
|
|
523
|
+
i++;
|
|
524
|
+
}
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
i++;
|
|
528
|
+
}
|
|
529
|
+
return tokens;
|
|
530
|
+
}
|
|
531
|
+
function evaluateCondition(expression, item, context) {
|
|
532
|
+
if (!expression || expression.trim() === "") {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
const tokens = tokenize(expression);
|
|
536
|
+
let pos = 0;
|
|
537
|
+
function current() {
|
|
538
|
+
return tokens[pos];
|
|
539
|
+
}
|
|
540
|
+
function consume(type) {
|
|
541
|
+
const token = tokens[pos];
|
|
542
|
+
if (!token) {
|
|
543
|
+
throw new ValidationException("Unexpected end of expression");
|
|
544
|
+
}
|
|
545
|
+
if (type && token.type !== type) {
|
|
546
|
+
throw new ValidationException(`Expected ${type} but got ${token.type}`);
|
|
547
|
+
}
|
|
548
|
+
pos++;
|
|
549
|
+
return token;
|
|
550
|
+
}
|
|
551
|
+
function parseExpression() {
|
|
552
|
+
return parseOr();
|
|
553
|
+
}
|
|
554
|
+
function parseOr() {
|
|
555
|
+
let left = parseAnd();
|
|
556
|
+
while (current()?.type === "OR") {
|
|
557
|
+
consume("OR");
|
|
558
|
+
const right = parseAnd();
|
|
559
|
+
left = left || right;
|
|
560
|
+
}
|
|
561
|
+
return left;
|
|
562
|
+
}
|
|
563
|
+
function parseAnd() {
|
|
564
|
+
let left = parseNot();
|
|
565
|
+
while (current()?.type === "AND") {
|
|
566
|
+
consume("AND");
|
|
567
|
+
const right = parseNot();
|
|
568
|
+
left = left && right;
|
|
569
|
+
}
|
|
570
|
+
return left;
|
|
571
|
+
}
|
|
572
|
+
function parseNot() {
|
|
573
|
+
if (current()?.type === "NOT") {
|
|
574
|
+
consume("NOT");
|
|
575
|
+
return !parseNot();
|
|
576
|
+
}
|
|
577
|
+
return parsePrimary();
|
|
578
|
+
}
|
|
579
|
+
function parsePrimary() {
|
|
580
|
+
const token = current();
|
|
581
|
+
if (!token) {
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
if (token.type === "LPAREN") {
|
|
585
|
+
consume("LPAREN");
|
|
586
|
+
const result = parseExpression();
|
|
587
|
+
consume("RPAREN");
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
if (token.type === "FUNCTION") {
|
|
591
|
+
return parseFunction();
|
|
592
|
+
}
|
|
593
|
+
if (token.type === "PATH" || token.type === "VALUE") {
|
|
594
|
+
return parseComparison();
|
|
595
|
+
}
|
|
596
|
+
throw new ValidationException(`Unexpected token: ${token.type}`);
|
|
597
|
+
}
|
|
598
|
+
function parseFunction() {
|
|
599
|
+
const funcToken = consume("FUNCTION");
|
|
600
|
+
consume("LPAREN");
|
|
601
|
+
const funcName = funcToken.value;
|
|
602
|
+
if (funcName === "attribute_exists") {
|
|
603
|
+
const pathToken = consume("PATH");
|
|
604
|
+
consume("RPAREN");
|
|
605
|
+
const segments = parsePath(pathToken.value, context.expressionAttributeNames);
|
|
606
|
+
const value = getValueAtPath(item, segments);
|
|
607
|
+
return value !== void 0;
|
|
608
|
+
}
|
|
609
|
+
if (funcName === "attribute_not_exists") {
|
|
610
|
+
const pathToken = consume("PATH");
|
|
611
|
+
consume("RPAREN");
|
|
612
|
+
const segments = parsePath(pathToken.value, context.expressionAttributeNames);
|
|
613
|
+
const value = getValueAtPath(item, segments);
|
|
614
|
+
return value === void 0;
|
|
615
|
+
}
|
|
616
|
+
if (funcName === "attribute_type") {
|
|
617
|
+
const pathToken = consume("PATH");
|
|
618
|
+
consume("COMMA");
|
|
619
|
+
const typeToken = consume("VALUE");
|
|
620
|
+
consume("RPAREN");
|
|
621
|
+
const segments = parsePath(pathToken.value, context.expressionAttributeNames);
|
|
622
|
+
const value = getValueAtPath(item, segments);
|
|
623
|
+
const expectedType = resolveValue(typeToken.value);
|
|
624
|
+
if (!value || !expectedType || !("S" in expectedType)) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
const typeMap = {
|
|
628
|
+
S: "S",
|
|
629
|
+
N: "N",
|
|
630
|
+
B: "B",
|
|
631
|
+
SS: "SS",
|
|
632
|
+
NS: "NS",
|
|
633
|
+
BS: "BS",
|
|
634
|
+
M: "M",
|
|
635
|
+
L: "L",
|
|
636
|
+
NULL: "NULL",
|
|
637
|
+
BOOL: "BOOL"
|
|
638
|
+
};
|
|
639
|
+
const actualType = Object.keys(value)[0];
|
|
640
|
+
return typeMap[expectedType.S] === actualType;
|
|
641
|
+
}
|
|
642
|
+
if (funcName === "begins_with") {
|
|
643
|
+
const pathToken = consume("PATH");
|
|
644
|
+
consume("COMMA");
|
|
645
|
+
const prefixToken = consume("VALUE");
|
|
646
|
+
consume("RPAREN");
|
|
647
|
+
const segments = parsePath(pathToken.value, context.expressionAttributeNames);
|
|
648
|
+
const value = getValueAtPath(item, segments);
|
|
649
|
+
const prefix = resolveValue(prefixToken.value);
|
|
650
|
+
if (!value || !prefix) {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
if ("S" in value && "S" in prefix) {
|
|
654
|
+
return value.S.startsWith(prefix.S);
|
|
655
|
+
}
|
|
656
|
+
if ("B" in value && "B" in prefix) {
|
|
657
|
+
return value.B.startsWith(prefix.B);
|
|
658
|
+
}
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
if (funcName === "contains") {
|
|
662
|
+
const pathToken = consume("PATH");
|
|
663
|
+
consume("COMMA");
|
|
664
|
+
const operandToken = consume("VALUE");
|
|
665
|
+
consume("RPAREN");
|
|
666
|
+
const segments = parsePath(pathToken.value, context.expressionAttributeNames);
|
|
667
|
+
const value = getValueAtPath(item, segments);
|
|
668
|
+
const operand = resolveValue(operandToken.value);
|
|
669
|
+
if (!value || !operand) {
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
if ("S" in value && "S" in operand) {
|
|
673
|
+
return value.S.includes(operand.S);
|
|
674
|
+
}
|
|
675
|
+
if ("SS" in value && "S" in operand) {
|
|
676
|
+
return value.SS.includes(operand.S);
|
|
677
|
+
}
|
|
678
|
+
if ("NS" in value && "N" in operand) {
|
|
679
|
+
return value.NS.includes(operand.N);
|
|
680
|
+
}
|
|
681
|
+
if ("BS" in value && "B" in operand) {
|
|
682
|
+
return value.BS.includes(operand.B);
|
|
683
|
+
}
|
|
684
|
+
if ("L" in value) {
|
|
685
|
+
return value.L.some((v) => compareValues(v, operand) === 0);
|
|
686
|
+
}
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
if (funcName === "size") {
|
|
690
|
+
const pathToken = consume("PATH");
|
|
691
|
+
consume("RPAREN");
|
|
692
|
+
const segments = parsePath(pathToken.value, context.expressionAttributeNames);
|
|
693
|
+
const value = getValueAtPath(item, segments);
|
|
694
|
+
if (!value) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
let size = 0;
|
|
698
|
+
if ("S" in value) size = value.S.length;
|
|
699
|
+
else if ("B" in value) size = value.B.length;
|
|
700
|
+
else if ("SS" in value) size = value.SS.length;
|
|
701
|
+
else if ("NS" in value) size = value.NS.length;
|
|
702
|
+
else if ("BS" in value) size = value.BS.length;
|
|
703
|
+
else if ("L" in value) size = value.L.length;
|
|
704
|
+
else if ("M" in value) size = Object.keys(value.M).length;
|
|
705
|
+
const nextToken = current();
|
|
706
|
+
if (nextToken?.type === "COMPARATOR") {
|
|
707
|
+
consume("COMPARATOR");
|
|
708
|
+
const rightToken = consume("VALUE");
|
|
709
|
+
const rightValue = resolveValue(rightToken.value);
|
|
710
|
+
if (!rightValue || !("N" in rightValue)) {
|
|
711
|
+
throw new ValidationException("Size comparison requires numeric operand");
|
|
712
|
+
}
|
|
713
|
+
return compareNumbers(size, parseFloat(rightValue.N), nextToken.value);
|
|
714
|
+
}
|
|
715
|
+
return size > 0;
|
|
716
|
+
}
|
|
717
|
+
throw new ValidationException(`Unknown function: ${funcName}`);
|
|
718
|
+
}
|
|
719
|
+
function parseComparison() {
|
|
720
|
+
const leftToken = consume();
|
|
721
|
+
const leftValue = resolveOperand2(leftToken);
|
|
722
|
+
const nextToken = current();
|
|
723
|
+
if (nextToken?.type === "COMPARATOR") {
|
|
724
|
+
const op = consume("COMPARATOR").value;
|
|
725
|
+
const rightToken = consume();
|
|
726
|
+
const rightValue = resolveOperand2(rightToken);
|
|
727
|
+
return compare(leftValue, rightValue, op);
|
|
728
|
+
}
|
|
729
|
+
if (nextToken?.type === "BETWEEN") {
|
|
730
|
+
consume("BETWEEN");
|
|
731
|
+
const lowToken = consume();
|
|
732
|
+
const lowValue = resolveOperand2(lowToken);
|
|
733
|
+
consume("AND");
|
|
734
|
+
const highToken = consume();
|
|
735
|
+
const highValue = resolveOperand2(highToken);
|
|
736
|
+
if (!leftValue || !lowValue || !highValue) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
return compareValues(leftValue, lowValue) >= 0 && compareValues(leftValue, highValue) <= 0;
|
|
740
|
+
}
|
|
741
|
+
if (nextToken?.type === "IN") {
|
|
742
|
+
consume("IN");
|
|
743
|
+
consume("LPAREN");
|
|
744
|
+
const values = [];
|
|
745
|
+
values.push(resolveOperand2(consume()));
|
|
746
|
+
while (current()?.type === "COMMA") {
|
|
747
|
+
consume("COMMA");
|
|
748
|
+
values.push(resolveOperand2(consume()));
|
|
749
|
+
}
|
|
750
|
+
consume("RPAREN");
|
|
751
|
+
if (!leftValue) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return values.some((v) => v && compareValues(leftValue, v) === 0);
|
|
755
|
+
}
|
|
756
|
+
return leftValue !== void 0;
|
|
757
|
+
}
|
|
758
|
+
function resolveOperand2(token) {
|
|
759
|
+
if (token.type === "VALUE") {
|
|
760
|
+
return resolveValue(token.value);
|
|
761
|
+
}
|
|
762
|
+
if (token.type === "PATH") {
|
|
763
|
+
const segments = parsePath(token.value, context.expressionAttributeNames);
|
|
764
|
+
return getValueAtPath(item, segments);
|
|
765
|
+
}
|
|
766
|
+
return void 0;
|
|
767
|
+
}
|
|
768
|
+
function resolveValue(ref) {
|
|
769
|
+
if (ref.startsWith(":") && context.expressionAttributeValues) {
|
|
770
|
+
return context.expressionAttributeValues[ref];
|
|
771
|
+
}
|
|
772
|
+
return void 0;
|
|
773
|
+
}
|
|
774
|
+
function compare(left, right, op) {
|
|
775
|
+
if (!left || !right) {
|
|
776
|
+
if (op === "<>") {
|
|
777
|
+
return left !== right;
|
|
778
|
+
}
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
const cmp = compareValues(left, right);
|
|
782
|
+
switch (op) {
|
|
783
|
+
case "=":
|
|
784
|
+
return cmp === 0;
|
|
785
|
+
case "<>":
|
|
786
|
+
return cmp !== 0;
|
|
787
|
+
case "<":
|
|
788
|
+
return cmp < 0;
|
|
789
|
+
case "<=":
|
|
790
|
+
return cmp <= 0;
|
|
791
|
+
case ">":
|
|
792
|
+
return cmp > 0;
|
|
793
|
+
case ">=":
|
|
794
|
+
return cmp >= 0;
|
|
795
|
+
default:
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function compareNumbers(left, right, op) {
|
|
800
|
+
switch (op) {
|
|
801
|
+
case "=":
|
|
802
|
+
return left === right;
|
|
803
|
+
case "<>":
|
|
804
|
+
return left !== right;
|
|
805
|
+
case "<":
|
|
806
|
+
return left < right;
|
|
807
|
+
case "<=":
|
|
808
|
+
return left <= right;
|
|
809
|
+
case ">":
|
|
810
|
+
return left > right;
|
|
811
|
+
case ">=":
|
|
812
|
+
return left >= right;
|
|
813
|
+
default:
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return parseExpression();
|
|
818
|
+
}
|
|
819
|
+
function compareValues(a, b) {
|
|
820
|
+
if ("S" in a && "S" in b) {
|
|
821
|
+
return a.S.localeCompare(b.S);
|
|
822
|
+
}
|
|
823
|
+
if ("N" in a && "N" in b) {
|
|
824
|
+
return parseFloat(a.N) - parseFloat(b.N);
|
|
825
|
+
}
|
|
826
|
+
if ("B" in a && "B" in b) {
|
|
827
|
+
return a.B.localeCompare(b.B);
|
|
828
|
+
}
|
|
829
|
+
if ("BOOL" in a && "BOOL" in b) {
|
|
830
|
+
return Number(a.BOOL) - Number(b.BOOL);
|
|
831
|
+
}
|
|
832
|
+
if ("NULL" in a && "NULL" in b) {
|
|
833
|
+
return 0;
|
|
834
|
+
}
|
|
835
|
+
const aStr = JSON.stringify(a);
|
|
836
|
+
const bStr = JSON.stringify(b);
|
|
837
|
+
return aStr.localeCompare(bStr);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/store/item.ts
|
|
841
|
+
import { createHash } from "crypto";
|
|
842
|
+
function extractKey(item, keySchema) {
|
|
843
|
+
const key = {};
|
|
844
|
+
for (const element of keySchema) {
|
|
845
|
+
const value = item[element.AttributeName];
|
|
846
|
+
if (value) {
|
|
847
|
+
key[element.AttributeName] = value;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return key;
|
|
851
|
+
}
|
|
852
|
+
function serializeKey(key, keySchema) {
|
|
853
|
+
const parts = [];
|
|
854
|
+
for (const element of keySchema) {
|
|
855
|
+
const value = key[element.AttributeName];
|
|
856
|
+
if (value) {
|
|
857
|
+
parts.push(serializeAttributeValue(value));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return parts.join("#");
|
|
861
|
+
}
|
|
862
|
+
function serializeAttributeValue(value) {
|
|
863
|
+
if ("S" in value) return `S:${value.S}`;
|
|
864
|
+
if ("N" in value) return `N:${value.N}`;
|
|
865
|
+
if ("B" in value) return `B:${value.B}`;
|
|
866
|
+
if ("SS" in value) return `SS:${value.SS.sort().join(",")}`;
|
|
867
|
+
if ("NS" in value) return `NS:${value.NS.sort().join(",")}`;
|
|
868
|
+
if ("BS" in value) return `BS:${value.BS.sort().join(",")}`;
|
|
869
|
+
if ("BOOL" in value) return `BOOL:${value.BOOL}`;
|
|
870
|
+
if ("NULL" in value) return "NULL";
|
|
871
|
+
if ("L" in value) return `L:${JSON.stringify(value.L)}`;
|
|
872
|
+
if ("M" in value) return `M:${JSON.stringify(value.M)}`;
|
|
873
|
+
return "";
|
|
874
|
+
}
|
|
875
|
+
function getHashKey(keySchema) {
|
|
876
|
+
const hash = keySchema.find((k) => k.KeyType === "HASH");
|
|
877
|
+
if (!hash) {
|
|
878
|
+
throw new Error("No hash key found");
|
|
879
|
+
}
|
|
880
|
+
return hash.AttributeName;
|
|
881
|
+
}
|
|
882
|
+
function getRangeKey(keySchema) {
|
|
883
|
+
const range = keySchema.find((k) => k.KeyType === "RANGE");
|
|
884
|
+
return range?.AttributeName;
|
|
885
|
+
}
|
|
886
|
+
function deepClone(obj) {
|
|
887
|
+
return JSON.parse(JSON.stringify(obj));
|
|
888
|
+
}
|
|
889
|
+
function estimateItemSize(item) {
|
|
890
|
+
return JSON.stringify(item).length;
|
|
891
|
+
}
|
|
892
|
+
function extractRawValue(value) {
|
|
893
|
+
if ("S" in value) return value.S;
|
|
894
|
+
if ("N" in value) return value.N;
|
|
895
|
+
if ("B" in value) return value.B;
|
|
896
|
+
return serializeAttributeValue(value);
|
|
897
|
+
}
|
|
898
|
+
function hashAttributeValue(value) {
|
|
899
|
+
const raw = extractRawValue(value);
|
|
900
|
+
return createHash("md5").update("Outliers" + raw).digest("hex");
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/expressions/key-condition.ts
|
|
904
|
+
function parseKeyCondition(expression, keySchema, context) {
|
|
905
|
+
const hashKeyName = getHashKey(keySchema);
|
|
906
|
+
const rangeKeyName = getRangeKey(keySchema);
|
|
907
|
+
const resolvedNames = context.expressionAttributeNames || {};
|
|
908
|
+
const resolvedValues = context.expressionAttributeValues || {};
|
|
909
|
+
function resolveName(name) {
|
|
910
|
+
if (name.startsWith("#")) {
|
|
911
|
+
const resolved = resolvedNames[name];
|
|
912
|
+
if (resolved === void 0) {
|
|
913
|
+
throw new ValidationException(`Expression attribute name ${name} is not defined`);
|
|
914
|
+
}
|
|
915
|
+
return resolved;
|
|
916
|
+
}
|
|
917
|
+
return name;
|
|
918
|
+
}
|
|
919
|
+
function resolveValue(ref) {
|
|
920
|
+
if (ref.startsWith(":")) {
|
|
921
|
+
const value = resolvedValues[ref];
|
|
922
|
+
if (value === void 0) {
|
|
923
|
+
throw new ValidationException(`Expression attribute value ${ref} is not defined`);
|
|
924
|
+
}
|
|
925
|
+
return value;
|
|
926
|
+
}
|
|
927
|
+
throw new ValidationException(`Invalid value reference: ${ref}`);
|
|
928
|
+
}
|
|
929
|
+
function stripOuterParens(expr) {
|
|
930
|
+
let s = expr.trim();
|
|
931
|
+
while (s.startsWith("(") && s.endsWith(")")) {
|
|
932
|
+
let depth = 0;
|
|
933
|
+
let balanced = true;
|
|
934
|
+
for (let i = 0; i < s.length - 1; i++) {
|
|
935
|
+
if (s[i] === "(") depth++;
|
|
936
|
+
else if (s[i] === ")") depth--;
|
|
937
|
+
if (depth === 0) {
|
|
938
|
+
balanced = false;
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (balanced) {
|
|
943
|
+
s = s.slice(1, -1).trim();
|
|
944
|
+
} else {
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return s;
|
|
949
|
+
}
|
|
950
|
+
const normalizedExpression = stripOuterParens(expression);
|
|
951
|
+
const parts = normalizedExpression.split(/\s+AND\s+/i);
|
|
952
|
+
let hashValue;
|
|
953
|
+
let rangeCondition;
|
|
954
|
+
for (const part of parts) {
|
|
955
|
+
const trimmed = stripOuterParens(part);
|
|
956
|
+
const beginsWithMatch = trimmed.match(/^begins_with\s*\(\s*([#\w]+)\s*,\s*(:\w+)\s*\)$/i);
|
|
957
|
+
if (beginsWithMatch) {
|
|
958
|
+
const attrName = resolveName(beginsWithMatch[1]);
|
|
959
|
+
const value = resolveValue(beginsWithMatch[2]);
|
|
960
|
+
if (attrName === rangeKeyName) {
|
|
961
|
+
rangeCondition = { operator: "begins_with", value };
|
|
962
|
+
} else {
|
|
963
|
+
throw new ValidationException(`begins_with can only be used on sort key`);
|
|
964
|
+
}
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const betweenMatch = trimmed.match(/^([#\w]+)\s+BETWEEN\s+(:\w+)\s+AND\s+(:\w+)$/i);
|
|
968
|
+
if (betweenMatch) {
|
|
969
|
+
const attrName = resolveName(betweenMatch[1]);
|
|
970
|
+
const value1 = resolveValue(betweenMatch[2]);
|
|
971
|
+
const value2 = resolveValue(betweenMatch[3]);
|
|
972
|
+
if (attrName === rangeKeyName) {
|
|
973
|
+
rangeCondition = { operator: "BETWEEN", value: value1, value2 };
|
|
974
|
+
} else {
|
|
975
|
+
throw new ValidationException(`BETWEEN can only be used on sort key`);
|
|
976
|
+
}
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
const comparisonMatch = trimmed.match(/^([#\w]+)\s*(=|<>|<=|>=|<|>)\s*(:\w+)$/);
|
|
980
|
+
if (comparisonMatch) {
|
|
981
|
+
const attrName = resolveName(comparisonMatch[1]);
|
|
982
|
+
const operator = comparisonMatch[2];
|
|
983
|
+
const value = resolveValue(comparisonMatch[3]);
|
|
984
|
+
if (attrName === hashKeyName) {
|
|
985
|
+
if (operator !== "=") {
|
|
986
|
+
throw new ValidationException(`Hash key condition must use = operator`);
|
|
987
|
+
}
|
|
988
|
+
hashValue = value;
|
|
989
|
+
} else if (attrName === rangeKeyName) {
|
|
990
|
+
rangeCondition = { operator, value };
|
|
991
|
+
} else {
|
|
992
|
+
throw new ValidationException(`Key condition references unknown attribute: ${attrName}`);
|
|
993
|
+
}
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
throw new ValidationException(`Invalid key condition expression: ${trimmed}`);
|
|
997
|
+
}
|
|
998
|
+
if (!hashValue) {
|
|
999
|
+
throw new ValidationException(`Key condition must specify hash key equality`);
|
|
1000
|
+
}
|
|
1001
|
+
return {
|
|
1002
|
+
hashKey: hashKeyName,
|
|
1003
|
+
hashValue,
|
|
1004
|
+
rangeKey: rangeKeyName,
|
|
1005
|
+
rangeCondition
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function matchesKeyCondition(item, condition) {
|
|
1009
|
+
const itemHashValue = item[condition.hashKey];
|
|
1010
|
+
if (!itemHashValue || !attributeEquals(itemHashValue, condition.hashValue)) {
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
if (condition.rangeCondition && condition.rangeKey) {
|
|
1014
|
+
const itemRangeValue = item[condition.rangeKey];
|
|
1015
|
+
if (!itemRangeValue) {
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
return matchesRangeCondition(itemRangeValue, condition.rangeCondition);
|
|
1019
|
+
}
|
|
1020
|
+
return true;
|
|
1021
|
+
}
|
|
1022
|
+
function matchesRangeCondition(value, condition) {
|
|
1023
|
+
const cmp = compareValues2(value, condition.value);
|
|
1024
|
+
switch (condition.operator) {
|
|
1025
|
+
case "=":
|
|
1026
|
+
return cmp === 0;
|
|
1027
|
+
case "<":
|
|
1028
|
+
return cmp < 0;
|
|
1029
|
+
case "<=":
|
|
1030
|
+
return cmp <= 0;
|
|
1031
|
+
case ">":
|
|
1032
|
+
return cmp > 0;
|
|
1033
|
+
case ">=":
|
|
1034
|
+
return cmp >= 0;
|
|
1035
|
+
case "BETWEEN":
|
|
1036
|
+
if (!condition.value2) return false;
|
|
1037
|
+
return cmp >= 0 && compareValues2(value, condition.value2) <= 0;
|
|
1038
|
+
case "begins_with":
|
|
1039
|
+
if ("S" in value && "S" in condition.value) {
|
|
1040
|
+
return value.S.startsWith(condition.value.S);
|
|
1041
|
+
}
|
|
1042
|
+
if ("B" in value && "B" in condition.value) {
|
|
1043
|
+
return value.B.startsWith(condition.value.B);
|
|
1044
|
+
}
|
|
1045
|
+
return false;
|
|
1046
|
+
default:
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function attributeEquals(a, b) {
|
|
1051
|
+
if ("S" in a && "S" in b) return a.S === b.S;
|
|
1052
|
+
if ("N" in a && "N" in b) return a.N === b.N;
|
|
1053
|
+
if ("B" in a && "B" in b) return a.B === b.B;
|
|
1054
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1055
|
+
}
|
|
1056
|
+
function compareValues2(a, b) {
|
|
1057
|
+
if ("S" in a && "S" in b) {
|
|
1058
|
+
return a.S.localeCompare(b.S);
|
|
1059
|
+
}
|
|
1060
|
+
if ("N" in a && "N" in b) {
|
|
1061
|
+
return parseFloat(a.N) - parseFloat(b.N);
|
|
1062
|
+
}
|
|
1063
|
+
if ("B" in a && "B" in b) {
|
|
1064
|
+
return a.B.localeCompare(b.B);
|
|
1065
|
+
}
|
|
1066
|
+
return 0;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/expressions/projection.ts
|
|
1070
|
+
function applyProjection(item, projectionExpression, expressionAttributeNames) {
|
|
1071
|
+
if (!projectionExpression || projectionExpression.trim() === "") {
|
|
1072
|
+
return item;
|
|
1073
|
+
}
|
|
1074
|
+
const paths = projectionExpression.split(",").map((p) => p.trim());
|
|
1075
|
+
const result = {};
|
|
1076
|
+
for (const path of paths) {
|
|
1077
|
+
const segments = parsePath(path, expressionAttributeNames);
|
|
1078
|
+
const value = getValueAtPath(item, segments);
|
|
1079
|
+
if (value !== void 0) {
|
|
1080
|
+
setValueAtPath(result, segments, value);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return result;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/expressions/update.ts
|
|
1087
|
+
function parseUpdateExpression(expression) {
|
|
1088
|
+
const actions = [];
|
|
1089
|
+
let remaining = expression.trim();
|
|
1090
|
+
while (remaining.length > 0) {
|
|
1091
|
+
const setMatch = remaining.match(/^SET\s+/i);
|
|
1092
|
+
const removeMatch = remaining.match(/^REMOVE\s+/i);
|
|
1093
|
+
const addMatch = remaining.match(/^ADD\s+/i);
|
|
1094
|
+
const deleteMatch = remaining.match(/^DELETE\s+/i);
|
|
1095
|
+
if (setMatch) {
|
|
1096
|
+
remaining = remaining.slice(setMatch[0].length);
|
|
1097
|
+
const { items, rest } = parseActionList(remaining);
|
|
1098
|
+
for (const item of items) {
|
|
1099
|
+
const eqIdx = item.indexOf("=");
|
|
1100
|
+
if (eqIdx === -1) {
|
|
1101
|
+
throw new ValidationException(`Invalid SET action: ${item}`);
|
|
1102
|
+
}
|
|
1103
|
+
const path = item.slice(0, eqIdx).trim();
|
|
1104
|
+
const valueExpr = item.slice(eqIdx + 1).trim();
|
|
1105
|
+
const ifNotExistsMatch = valueExpr.match(/^if_not_exists\s*\(\s*([^,]+)\s*,\s*(.+)\s*\)$/i);
|
|
1106
|
+
if (ifNotExistsMatch) {
|
|
1107
|
+
actions.push({
|
|
1108
|
+
type: "SET",
|
|
1109
|
+
path,
|
|
1110
|
+
value: valueExpr,
|
|
1111
|
+
operation: "if_not_exists",
|
|
1112
|
+
operands: [ifNotExistsMatch[1].trim(), ifNotExistsMatch[2].trim()]
|
|
1113
|
+
});
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
const listAppendMatch = valueExpr.match(/^list_append\s*\(\s*([^,]+)\s*,\s*(.+)\s*\)$/i);
|
|
1117
|
+
if (listAppendMatch) {
|
|
1118
|
+
actions.push({
|
|
1119
|
+
type: "SET",
|
|
1120
|
+
path,
|
|
1121
|
+
value: valueExpr,
|
|
1122
|
+
operation: "list_append",
|
|
1123
|
+
operands: [listAppendMatch[1].trim(), listAppendMatch[2].trim()]
|
|
1124
|
+
});
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
const plusMatch = valueExpr.match(/^(.+?)\s*\+\s*(.+)$/);
|
|
1128
|
+
if (plusMatch) {
|
|
1129
|
+
actions.push({
|
|
1130
|
+
type: "SET",
|
|
1131
|
+
path,
|
|
1132
|
+
value: valueExpr,
|
|
1133
|
+
operation: "plus",
|
|
1134
|
+
operands: [plusMatch[1].trim(), plusMatch[2].trim()]
|
|
1135
|
+
});
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
const minusMatch = valueExpr.match(/^(.+?)\s*-\s*(.+)$/);
|
|
1139
|
+
if (minusMatch) {
|
|
1140
|
+
actions.push({
|
|
1141
|
+
type: "SET",
|
|
1142
|
+
path,
|
|
1143
|
+
value: valueExpr,
|
|
1144
|
+
operation: "minus",
|
|
1145
|
+
operands: [minusMatch[1].trim(), minusMatch[2].trim()]
|
|
1146
|
+
});
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
actions.push({ type: "SET", path, value: valueExpr });
|
|
1150
|
+
}
|
|
1151
|
+
remaining = rest;
|
|
1152
|
+
} else if (removeMatch) {
|
|
1153
|
+
remaining = remaining.slice(removeMatch[0].length);
|
|
1154
|
+
const { items, rest } = parseActionList(remaining);
|
|
1155
|
+
for (const item of items) {
|
|
1156
|
+
actions.push({ type: "REMOVE", path: item.trim() });
|
|
1157
|
+
}
|
|
1158
|
+
remaining = rest;
|
|
1159
|
+
} else if (addMatch) {
|
|
1160
|
+
remaining = remaining.slice(addMatch[0].length);
|
|
1161
|
+
const { items, rest } = parseActionList(remaining);
|
|
1162
|
+
for (const item of items) {
|
|
1163
|
+
const parts = item.trim().split(/\s+/);
|
|
1164
|
+
if (parts.length < 2) {
|
|
1165
|
+
throw new ValidationException(`Invalid ADD action: ${item}`);
|
|
1166
|
+
}
|
|
1167
|
+
actions.push({ type: "ADD", path: parts[0], value: parts.slice(1).join(" ") });
|
|
1168
|
+
}
|
|
1169
|
+
remaining = rest;
|
|
1170
|
+
} else if (deleteMatch) {
|
|
1171
|
+
remaining = remaining.slice(deleteMatch[0].length);
|
|
1172
|
+
const { items, rest } = parseActionList(remaining);
|
|
1173
|
+
for (const item of items) {
|
|
1174
|
+
const parts = item.trim().split(/\s+/);
|
|
1175
|
+
if (parts.length < 2) {
|
|
1176
|
+
throw new ValidationException(`Invalid DELETE action: ${item}`);
|
|
1177
|
+
}
|
|
1178
|
+
actions.push({ type: "DELETE", path: parts[0], value: parts.slice(1).join(" ") });
|
|
1179
|
+
}
|
|
1180
|
+
remaining = rest;
|
|
1181
|
+
} else {
|
|
1182
|
+
remaining = remaining.slice(1);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return actions;
|
|
1186
|
+
}
|
|
1187
|
+
function parseActionList(expression) {
|
|
1188
|
+
const items = [];
|
|
1189
|
+
let current = "";
|
|
1190
|
+
let depth = 0;
|
|
1191
|
+
let i = 0;
|
|
1192
|
+
const stopKeywords = ["SET", "REMOVE", "ADD", "DELETE"];
|
|
1193
|
+
while (i < expression.length) {
|
|
1194
|
+
const char = expression[i];
|
|
1195
|
+
for (const keyword of stopKeywords) {
|
|
1196
|
+
if (expression.slice(i).toUpperCase().startsWith(keyword + " ") && depth === 0 && current.trim()) {
|
|
1197
|
+
items.push(current.trim());
|
|
1198
|
+
return { items, rest: expression.slice(i) };
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (char === "(") {
|
|
1202
|
+
depth++;
|
|
1203
|
+
current += char;
|
|
1204
|
+
} else if (char === ")") {
|
|
1205
|
+
depth--;
|
|
1206
|
+
current += char;
|
|
1207
|
+
} else if (char === "," && depth === 0) {
|
|
1208
|
+
if (current.trim()) {
|
|
1209
|
+
items.push(current.trim());
|
|
1210
|
+
}
|
|
1211
|
+
current = "";
|
|
1212
|
+
} else {
|
|
1213
|
+
current += char;
|
|
1214
|
+
}
|
|
1215
|
+
i++;
|
|
1216
|
+
}
|
|
1217
|
+
if (current.trim()) {
|
|
1218
|
+
items.push(current.trim());
|
|
1219
|
+
}
|
|
1220
|
+
return { items, rest: "" };
|
|
1221
|
+
}
|
|
1222
|
+
function applyUpdateExpression(item, expression, context) {
|
|
1223
|
+
if (!expression || expression.trim() === "") {
|
|
1224
|
+
return item;
|
|
1225
|
+
}
|
|
1226
|
+
const result = JSON.parse(JSON.stringify(item));
|
|
1227
|
+
const actions = parseUpdateExpression(expression);
|
|
1228
|
+
const removeActions = [];
|
|
1229
|
+
const otherActions = [];
|
|
1230
|
+
for (const action of actions) {
|
|
1231
|
+
if (action.type === "REMOVE") {
|
|
1232
|
+
removeActions.push(action);
|
|
1233
|
+
} else {
|
|
1234
|
+
otherActions.push(action);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
for (const action of otherActions) {
|
|
1238
|
+
switch (action.type) {
|
|
1239
|
+
case "SET":
|
|
1240
|
+
applySetAction(result, action, context);
|
|
1241
|
+
break;
|
|
1242
|
+
case "ADD":
|
|
1243
|
+
applyAddAction(result, action, context);
|
|
1244
|
+
break;
|
|
1245
|
+
case "DELETE":
|
|
1246
|
+
applyDeleteAction(result, action, context);
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
removeActions.sort((a, b) => {
|
|
1251
|
+
const aSegments = parsePath(a.path, context.expressionAttributeNames);
|
|
1252
|
+
const bSegments = parsePath(b.path, context.expressionAttributeNames);
|
|
1253
|
+
let aLastIdx;
|
|
1254
|
+
let bLastIdx;
|
|
1255
|
+
for (const seg of aSegments) {
|
|
1256
|
+
if (seg.type === "index") {
|
|
1257
|
+
aLastIdx = seg.value;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
for (const seg of bSegments) {
|
|
1261
|
+
if (seg.type === "index") {
|
|
1262
|
+
bLastIdx = seg.value;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (aLastIdx !== void 0 && bLastIdx !== void 0) {
|
|
1266
|
+
return bLastIdx - aLastIdx;
|
|
1267
|
+
}
|
|
1268
|
+
return 0;
|
|
1269
|
+
});
|
|
1270
|
+
for (const action of removeActions) {
|
|
1271
|
+
applyRemoveAction(result, action, context);
|
|
1272
|
+
}
|
|
1273
|
+
return result;
|
|
1274
|
+
}
|
|
1275
|
+
function resolveOperand(item, operand, context) {
|
|
1276
|
+
operand = operand.trim();
|
|
1277
|
+
if (operand.startsWith(":")) {
|
|
1278
|
+
return context.expressionAttributeValues?.[operand];
|
|
1279
|
+
}
|
|
1280
|
+
const segments = parsePath(operand, context.expressionAttributeNames);
|
|
1281
|
+
return getValueAtPath(item, segments);
|
|
1282
|
+
}
|
|
1283
|
+
function applySetAction(item, action, context) {
|
|
1284
|
+
const segments = parsePath(action.path, context.expressionAttributeNames);
|
|
1285
|
+
let value;
|
|
1286
|
+
if (action.operation === "if_not_exists") {
|
|
1287
|
+
const existingValue = resolveOperand(item, action.operands[0], context);
|
|
1288
|
+
if (existingValue !== void 0) {
|
|
1289
|
+
value = existingValue;
|
|
1290
|
+
} else {
|
|
1291
|
+
value = resolveOperand(item, action.operands[1], context);
|
|
1292
|
+
}
|
|
1293
|
+
} else if (action.operation === "list_append") {
|
|
1294
|
+
const list1 = resolveOperand(item, action.operands[0], context);
|
|
1295
|
+
const list2 = resolveOperand(item, action.operands[1], context);
|
|
1296
|
+
if (list1 && "L" in list1 && list2 && "L" in list2) {
|
|
1297
|
+
value = { L: [...list1.L, ...list2.L] };
|
|
1298
|
+
} else if (list1 && "L" in list1) {
|
|
1299
|
+
value = list1;
|
|
1300
|
+
} else if (list2 && "L" in list2) {
|
|
1301
|
+
value = list2;
|
|
1302
|
+
}
|
|
1303
|
+
} else if (action.operation === "plus") {
|
|
1304
|
+
const left = resolveOperand(item, action.operands[0], context);
|
|
1305
|
+
const right = resolveOperand(item, action.operands[1], context);
|
|
1306
|
+
if (left && "N" in left && right && "N" in right) {
|
|
1307
|
+
const result = parseFloat(left.N) + parseFloat(right.N);
|
|
1308
|
+
value = { N: String(result) };
|
|
1309
|
+
}
|
|
1310
|
+
} else if (action.operation === "minus") {
|
|
1311
|
+
const left = resolveOperand(item, action.operands[0], context);
|
|
1312
|
+
const right = resolveOperand(item, action.operands[1], context);
|
|
1313
|
+
if (left && "N" in left && right && "N" in right) {
|
|
1314
|
+
const result = parseFloat(left.N) - parseFloat(right.N);
|
|
1315
|
+
value = { N: String(result) };
|
|
1316
|
+
}
|
|
1317
|
+
} else {
|
|
1318
|
+
value = resolveOperand(item, action.value, context);
|
|
1319
|
+
}
|
|
1320
|
+
if (value !== void 0) {
|
|
1321
|
+
setValueAtPath(item, segments, value);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function applyRemoveAction(item, action, context) {
|
|
1325
|
+
const segments = parsePath(action.path, context.expressionAttributeNames);
|
|
1326
|
+
deleteValueAtPath(item, segments);
|
|
1327
|
+
}
|
|
1328
|
+
function applyAddAction(item, action, context) {
|
|
1329
|
+
const segments = parsePath(action.path, context.expressionAttributeNames);
|
|
1330
|
+
const addValue = resolveOperand(item, action.value, context);
|
|
1331
|
+
const existingValue = getValueAtPath(item, segments);
|
|
1332
|
+
if (!addValue) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if ("N" in addValue) {
|
|
1336
|
+
if (existingValue && "N" in existingValue) {
|
|
1337
|
+
const result = parseFloat(existingValue.N) + parseFloat(addValue.N);
|
|
1338
|
+
setValueAtPath(item, segments, { N: String(result) });
|
|
1339
|
+
} else if (!existingValue) {
|
|
1340
|
+
setValueAtPath(item, segments, addValue);
|
|
1341
|
+
}
|
|
1342
|
+
} else if ("SS" in addValue) {
|
|
1343
|
+
if (existingValue && "SS" in existingValue) {
|
|
1344
|
+
const combined = /* @__PURE__ */ new Set([...existingValue.SS, ...addValue.SS]);
|
|
1345
|
+
setValueAtPath(item, segments, { SS: Array.from(combined) });
|
|
1346
|
+
} else if (!existingValue) {
|
|
1347
|
+
setValueAtPath(item, segments, addValue);
|
|
1348
|
+
}
|
|
1349
|
+
} else if ("NS" in addValue) {
|
|
1350
|
+
if (existingValue && "NS" in existingValue) {
|
|
1351
|
+
const combined = /* @__PURE__ */ new Set([...existingValue.NS, ...addValue.NS]);
|
|
1352
|
+
setValueAtPath(item, segments, { NS: Array.from(combined) });
|
|
1353
|
+
} else if (!existingValue) {
|
|
1354
|
+
setValueAtPath(item, segments, addValue);
|
|
1355
|
+
}
|
|
1356
|
+
} else if ("BS" in addValue) {
|
|
1357
|
+
if (existingValue && "BS" in existingValue) {
|
|
1358
|
+
const combined = /* @__PURE__ */ new Set([...existingValue.BS, ...addValue.BS]);
|
|
1359
|
+
setValueAtPath(item, segments, { BS: Array.from(combined) });
|
|
1360
|
+
} else if (!existingValue) {
|
|
1361
|
+
setValueAtPath(item, segments, addValue);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
function applyDeleteAction(item, action, context) {
|
|
1366
|
+
const segments = parsePath(action.path, context.expressionAttributeNames);
|
|
1367
|
+
const deleteValue = resolveOperand(item, action.value, context);
|
|
1368
|
+
const existingValue = getValueAtPath(item, segments);
|
|
1369
|
+
if (!deleteValue || !existingValue) {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if ("SS" in deleteValue && "SS" in existingValue) {
|
|
1373
|
+
const toDelete = new Set(deleteValue.SS);
|
|
1374
|
+
const remaining = existingValue.SS.filter((s) => !toDelete.has(s));
|
|
1375
|
+
if (remaining.length > 0) {
|
|
1376
|
+
setValueAtPath(item, segments, { SS: remaining });
|
|
1377
|
+
} else {
|
|
1378
|
+
deleteValueAtPath(item, segments);
|
|
1379
|
+
}
|
|
1380
|
+
} else if ("NS" in deleteValue && "NS" in existingValue) {
|
|
1381
|
+
const toDelete = new Set(deleteValue.NS);
|
|
1382
|
+
const remaining = existingValue.NS.filter((n) => !toDelete.has(n));
|
|
1383
|
+
if (remaining.length > 0) {
|
|
1384
|
+
setValueAtPath(item, segments, { NS: remaining });
|
|
1385
|
+
} else {
|
|
1386
|
+
deleteValueAtPath(item, segments);
|
|
1387
|
+
}
|
|
1388
|
+
} else if ("BS" in deleteValue && "BS" in existingValue) {
|
|
1389
|
+
const toDelete = new Set(deleteValue.BS);
|
|
1390
|
+
const remaining = existingValue.BS.filter((b) => !toDelete.has(b));
|
|
1391
|
+
if (remaining.length > 0) {
|
|
1392
|
+
setValueAtPath(item, segments, { BS: remaining });
|
|
1393
|
+
} else {
|
|
1394
|
+
deleteValueAtPath(item, segments);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/operations/put-item.ts
|
|
1400
|
+
function putItem(store, input) {
|
|
1401
|
+
if (!input.TableName) {
|
|
1402
|
+
throw new ValidationException("TableName is required");
|
|
1403
|
+
}
|
|
1404
|
+
if (!input.Item) {
|
|
1405
|
+
throw new ValidationException("Item is required");
|
|
1406
|
+
}
|
|
1407
|
+
const table = store.getTable(input.TableName);
|
|
1408
|
+
const hashKey = table.getHashKeyName();
|
|
1409
|
+
if (!input.Item[hashKey]) {
|
|
1410
|
+
throw new ValidationException(`Missing the key ${hashKey} in the item`);
|
|
1411
|
+
}
|
|
1412
|
+
const rangeKey = table.getRangeKeyName();
|
|
1413
|
+
if (rangeKey && !input.Item[rangeKey]) {
|
|
1414
|
+
throw new ValidationException(`Missing the key ${rangeKey} in the item`);
|
|
1415
|
+
}
|
|
1416
|
+
const existingItem = table.getItem(input.Item);
|
|
1417
|
+
if (input.ConditionExpression) {
|
|
1418
|
+
const conditionMet = evaluateCondition(input.ConditionExpression, existingItem || {}, {
|
|
1419
|
+
expressionAttributeNames: input.ExpressionAttributeNames,
|
|
1420
|
+
expressionAttributeValues: input.ExpressionAttributeValues
|
|
1421
|
+
});
|
|
1422
|
+
if (!conditionMet) {
|
|
1423
|
+
if (input.ReturnValuesOnConditionCheckFailure === "ALL_OLD" && existingItem) {
|
|
1424
|
+
throw new ConditionalCheckFailedException("The conditional request failed", existingItem);
|
|
1425
|
+
}
|
|
1426
|
+
throw new ConditionalCheckFailedException("The conditional request failed");
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
const oldItem = table.putItem(input.Item);
|
|
1430
|
+
const output = {};
|
|
1431
|
+
if (input.ReturnValues === "ALL_OLD" && oldItem) {
|
|
1432
|
+
output.Attributes = oldItem;
|
|
1433
|
+
}
|
|
1434
|
+
if (input.ReturnConsumedCapacity && input.ReturnConsumedCapacity !== "NONE") {
|
|
1435
|
+
output.ConsumedCapacity = {
|
|
1436
|
+
TableName: input.TableName,
|
|
1437
|
+
CapacityUnits: 1,
|
|
1438
|
+
WriteCapacityUnits: 1
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
return output;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// src/operations/get-item.ts
|
|
1445
|
+
function getItem(store, input) {
|
|
1446
|
+
if (!input.TableName) {
|
|
1447
|
+
throw new ValidationException("TableName is required");
|
|
1448
|
+
}
|
|
1449
|
+
if (!input.Key) {
|
|
1450
|
+
throw new ValidationException("Key is required");
|
|
1451
|
+
}
|
|
1452
|
+
const table = store.getTable(input.TableName);
|
|
1453
|
+
const hashKey = table.getHashKeyName();
|
|
1454
|
+
if (!input.Key[hashKey]) {
|
|
1455
|
+
throw new ValidationException(`Missing the key ${hashKey} in the key`);
|
|
1456
|
+
}
|
|
1457
|
+
const rangeKey = table.getRangeKeyName();
|
|
1458
|
+
if (rangeKey && !input.Key[rangeKey]) {
|
|
1459
|
+
throw new ValidationException(`Missing the key ${rangeKey} in the key`);
|
|
1460
|
+
}
|
|
1461
|
+
let item = table.getItem(input.Key);
|
|
1462
|
+
if (item && input.ProjectionExpression) {
|
|
1463
|
+
item = applyProjection(item, input.ProjectionExpression, input.ExpressionAttributeNames);
|
|
1464
|
+
}
|
|
1465
|
+
const output = {};
|
|
1466
|
+
if (item) {
|
|
1467
|
+
output.Item = item;
|
|
1468
|
+
}
|
|
1469
|
+
if (input.ReturnConsumedCapacity && input.ReturnConsumedCapacity !== "NONE") {
|
|
1470
|
+
output.ConsumedCapacity = {
|
|
1471
|
+
TableName: input.TableName,
|
|
1472
|
+
CapacityUnits: 0.5,
|
|
1473
|
+
ReadCapacityUnits: 0.5
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
return output;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// src/operations/delete-item.ts
|
|
1480
|
+
function deleteItem(store, input) {
|
|
1481
|
+
if (!input.TableName) {
|
|
1482
|
+
throw new ValidationException("TableName is required");
|
|
1483
|
+
}
|
|
1484
|
+
if (!input.Key) {
|
|
1485
|
+
throw new ValidationException("Key is required");
|
|
1486
|
+
}
|
|
1487
|
+
const table = store.getTable(input.TableName);
|
|
1488
|
+
const hashKey = table.getHashKeyName();
|
|
1489
|
+
if (!input.Key[hashKey]) {
|
|
1490
|
+
throw new ValidationException(`Missing the key ${hashKey} in the key`);
|
|
1491
|
+
}
|
|
1492
|
+
const rangeKey = table.getRangeKeyName();
|
|
1493
|
+
if (rangeKey && !input.Key[rangeKey]) {
|
|
1494
|
+
throw new ValidationException(`Missing the key ${rangeKey} in the key`);
|
|
1495
|
+
}
|
|
1496
|
+
const existingItem = table.getItem(input.Key);
|
|
1497
|
+
if (input.ConditionExpression) {
|
|
1498
|
+
const conditionMet = evaluateCondition(input.ConditionExpression, existingItem || {}, {
|
|
1499
|
+
expressionAttributeNames: input.ExpressionAttributeNames,
|
|
1500
|
+
expressionAttributeValues: input.ExpressionAttributeValues
|
|
1501
|
+
});
|
|
1502
|
+
if (!conditionMet) {
|
|
1503
|
+
if (input.ReturnValuesOnConditionCheckFailure === "ALL_OLD" && existingItem) {
|
|
1504
|
+
throw new ConditionalCheckFailedException("The conditional request failed", existingItem);
|
|
1505
|
+
}
|
|
1506
|
+
throw new ConditionalCheckFailedException("The conditional request failed");
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
const oldItem = table.deleteItem(input.Key);
|
|
1510
|
+
const output = {};
|
|
1511
|
+
if (input.ReturnValues === "ALL_OLD" && oldItem) {
|
|
1512
|
+
output.Attributes = oldItem;
|
|
1513
|
+
}
|
|
1514
|
+
if (input.ReturnConsumedCapacity && input.ReturnConsumedCapacity !== "NONE") {
|
|
1515
|
+
output.ConsumedCapacity = {
|
|
1516
|
+
TableName: input.TableName,
|
|
1517
|
+
CapacityUnits: 1,
|
|
1518
|
+
WriteCapacityUnits: 1
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
return output;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// src/operations/update-item.ts
|
|
1525
|
+
function updateItem(store, input) {
|
|
1526
|
+
if (!input.TableName) {
|
|
1527
|
+
throw new ValidationException("TableName is required");
|
|
1528
|
+
}
|
|
1529
|
+
if (!input.Key) {
|
|
1530
|
+
throw new ValidationException("Key is required");
|
|
1531
|
+
}
|
|
1532
|
+
const table = store.getTable(input.TableName);
|
|
1533
|
+
const hashKey = table.getHashKeyName();
|
|
1534
|
+
if (!input.Key[hashKey]) {
|
|
1535
|
+
throw new ValidationException(`Missing the key ${hashKey} in the key`);
|
|
1536
|
+
}
|
|
1537
|
+
const rangeKey = table.getRangeKeyName();
|
|
1538
|
+
if (rangeKey && !input.Key[rangeKey]) {
|
|
1539
|
+
throw new ValidationException(`Missing the key ${rangeKey} in the key`);
|
|
1540
|
+
}
|
|
1541
|
+
const existingItem = table.getItem(input.Key);
|
|
1542
|
+
if (input.ConditionExpression) {
|
|
1543
|
+
const conditionMet = evaluateCondition(input.ConditionExpression, existingItem || {}, {
|
|
1544
|
+
expressionAttributeNames: input.ExpressionAttributeNames,
|
|
1545
|
+
expressionAttributeValues: input.ExpressionAttributeValues
|
|
1546
|
+
});
|
|
1547
|
+
if (!conditionMet) {
|
|
1548
|
+
if (input.ReturnValuesOnConditionCheckFailure === "ALL_OLD" && existingItem) {
|
|
1549
|
+
throw new ConditionalCheckFailedException("The conditional request failed", existingItem);
|
|
1550
|
+
}
|
|
1551
|
+
throw new ConditionalCheckFailedException("The conditional request failed");
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
let item = existingItem ? { ...existingItem } : { ...input.Key };
|
|
1555
|
+
if (input.UpdateExpression) {
|
|
1556
|
+
item = applyUpdateExpression(item, input.UpdateExpression, {
|
|
1557
|
+
expressionAttributeNames: input.ExpressionAttributeNames,
|
|
1558
|
+
expressionAttributeValues: input.ExpressionAttributeValues
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
for (const [key, value] of Object.entries(input.Key)) {
|
|
1562
|
+
item[key] = value;
|
|
1563
|
+
}
|
|
1564
|
+
table.updateItem(input.Key, item);
|
|
1565
|
+
const output = {};
|
|
1566
|
+
switch (input.ReturnValues) {
|
|
1567
|
+
case "ALL_OLD":
|
|
1568
|
+
if (existingItem) {
|
|
1569
|
+
output.Attributes = existingItem;
|
|
1570
|
+
}
|
|
1571
|
+
break;
|
|
1572
|
+
case "ALL_NEW":
|
|
1573
|
+
output.Attributes = item;
|
|
1574
|
+
break;
|
|
1575
|
+
case "UPDATED_OLD":
|
|
1576
|
+
if (existingItem && input.UpdateExpression) {
|
|
1577
|
+
output.Attributes = getUpdatedAttributes(existingItem, item);
|
|
1578
|
+
}
|
|
1579
|
+
break;
|
|
1580
|
+
case "UPDATED_NEW":
|
|
1581
|
+
if (input.UpdateExpression) {
|
|
1582
|
+
output.Attributes = getUpdatedAttributes(existingItem || {}, item);
|
|
1583
|
+
}
|
|
1584
|
+
break;
|
|
1585
|
+
}
|
|
1586
|
+
if (input.ReturnConsumedCapacity && input.ReturnConsumedCapacity !== "NONE") {
|
|
1587
|
+
output.ConsumedCapacity = {
|
|
1588
|
+
TableName: input.TableName,
|
|
1589
|
+
CapacityUnits: 1,
|
|
1590
|
+
WriteCapacityUnits: 1
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
return output;
|
|
1594
|
+
}
|
|
1595
|
+
function getUpdatedAttributes(oldItem, newItem) {
|
|
1596
|
+
const updated = {};
|
|
1597
|
+
for (const [key, newValue] of Object.entries(newItem)) {
|
|
1598
|
+
const oldValue = oldItem[key];
|
|
1599
|
+
if (!oldValue || JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
|
1600
|
+
updated[key] = newValue;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return updated;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// src/operations/query.ts
|
|
1607
|
+
function query(store, input) {
|
|
1608
|
+
if (!input.TableName) {
|
|
1609
|
+
throw new ValidationException("TableName is required");
|
|
1610
|
+
}
|
|
1611
|
+
if (!input.KeyConditionExpression) {
|
|
1612
|
+
throw new ValidationException("KeyConditionExpression is required");
|
|
1613
|
+
}
|
|
1614
|
+
const table = store.getTable(input.TableName);
|
|
1615
|
+
let keySchema = table.keySchema;
|
|
1616
|
+
if (input.IndexName) {
|
|
1617
|
+
const indexKeySchema = table.getIndexKeySchema(input.IndexName);
|
|
1618
|
+
if (!indexKeySchema) {
|
|
1619
|
+
throw new ResourceNotFoundException(`Index ${input.IndexName} not found`);
|
|
1620
|
+
}
|
|
1621
|
+
keySchema = indexKeySchema;
|
|
1622
|
+
}
|
|
1623
|
+
const keyCondition = parseKeyCondition(input.KeyConditionExpression, keySchema, {
|
|
1624
|
+
expressionAttributeNames: input.ExpressionAttributeNames,
|
|
1625
|
+
expressionAttributeValues: input.ExpressionAttributeValues
|
|
1626
|
+
});
|
|
1627
|
+
let items;
|
|
1628
|
+
let lastEvaluatedKey;
|
|
1629
|
+
if (input.IndexName) {
|
|
1630
|
+
const result = table.queryIndex(
|
|
1631
|
+
input.IndexName,
|
|
1632
|
+
{ [keyCondition.hashKey]: keyCondition.hashValue },
|
|
1633
|
+
{
|
|
1634
|
+
scanIndexForward: input.ScanIndexForward,
|
|
1635
|
+
exclusiveStartKey: input.ExclusiveStartKey
|
|
1636
|
+
}
|
|
1637
|
+
);
|
|
1638
|
+
items = result.items;
|
|
1639
|
+
lastEvaluatedKey = result.lastEvaluatedKey;
|
|
1640
|
+
} else {
|
|
1641
|
+
const result = table.queryByHashKey(
|
|
1642
|
+
{ [keyCondition.hashKey]: keyCondition.hashValue },
|
|
1643
|
+
{
|
|
1644
|
+
scanIndexForward: input.ScanIndexForward,
|
|
1645
|
+
exclusiveStartKey: input.ExclusiveStartKey
|
|
1646
|
+
}
|
|
1647
|
+
);
|
|
1648
|
+
items = result.items;
|
|
1649
|
+
lastEvaluatedKey = result.lastEvaluatedKey;
|
|
1650
|
+
}
|
|
1651
|
+
if (keyCondition.rangeCondition) {
|
|
1652
|
+
items = items.filter((item) => matchesKeyCondition(item, keyCondition));
|
|
1653
|
+
}
|
|
1654
|
+
const scannedCount = items.length;
|
|
1655
|
+
if (input.FilterExpression) {
|
|
1656
|
+
items = items.filter(
|
|
1657
|
+
(item) => evaluateCondition(input.FilterExpression, item, {
|
|
1658
|
+
expressionAttributeNames: input.ExpressionAttributeNames,
|
|
1659
|
+
expressionAttributeValues: input.ExpressionAttributeValues
|
|
1660
|
+
})
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
if (input.Limit && items.length > input.Limit) {
|
|
1664
|
+
items = items.slice(0, input.Limit);
|
|
1665
|
+
if (items.length > 0) {
|
|
1666
|
+
const lastItem = items[items.length - 1];
|
|
1667
|
+
lastEvaluatedKey = {};
|
|
1668
|
+
const hashKey = table.getHashKeyName();
|
|
1669
|
+
const rangeKey = table.getRangeKeyName();
|
|
1670
|
+
if (lastItem[hashKey]) {
|
|
1671
|
+
lastEvaluatedKey[hashKey] = lastItem[hashKey];
|
|
1672
|
+
}
|
|
1673
|
+
if (rangeKey && lastItem[rangeKey]) {
|
|
1674
|
+
lastEvaluatedKey[rangeKey] = lastItem[rangeKey];
|
|
1675
|
+
}
|
|
1676
|
+
if (input.IndexName) {
|
|
1677
|
+
const indexKeySchema = table.getIndexKeySchema(input.IndexName);
|
|
1678
|
+
if (indexKeySchema) {
|
|
1679
|
+
for (const key of indexKeySchema) {
|
|
1680
|
+
const attrValue = lastItem[key.AttributeName];
|
|
1681
|
+
if (attrValue) {
|
|
1682
|
+
lastEvaluatedKey[key.AttributeName] = attrValue;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (input.ProjectionExpression) {
|
|
1690
|
+
items = items.map((item) => applyProjection(item, input.ProjectionExpression, input.ExpressionAttributeNames));
|
|
1691
|
+
}
|
|
1692
|
+
const output = {
|
|
1693
|
+
Count: items.length,
|
|
1694
|
+
ScannedCount: scannedCount
|
|
1695
|
+
};
|
|
1696
|
+
if (input.Select !== "COUNT") {
|
|
1697
|
+
output.Items = items;
|
|
1698
|
+
}
|
|
1699
|
+
if (lastEvaluatedKey && Object.keys(lastEvaluatedKey).length > 0) {
|
|
1700
|
+
output.LastEvaluatedKey = lastEvaluatedKey;
|
|
1701
|
+
}
|
|
1702
|
+
if (input.ReturnConsumedCapacity && input.ReturnConsumedCapacity !== "NONE") {
|
|
1703
|
+
output.ConsumedCapacity = {
|
|
1704
|
+
TableName: input.TableName,
|
|
1705
|
+
CapacityUnits: Math.max(0.5, scannedCount * 0.5),
|
|
1706
|
+
ReadCapacityUnits: Math.max(0.5, scannedCount * 0.5)
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
return output;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// src/operations/scan.ts
|
|
1713
|
+
function scan(store, input) {
|
|
1714
|
+
if (!input.TableName) {
|
|
1715
|
+
throw new ValidationException("TableName is required");
|
|
1716
|
+
}
|
|
1717
|
+
const table = store.getTable(input.TableName);
|
|
1718
|
+
let items;
|
|
1719
|
+
let lastEvaluatedKey;
|
|
1720
|
+
if (input.IndexName) {
|
|
1721
|
+
if (!table.hasIndex(input.IndexName)) {
|
|
1722
|
+
throw new ResourceNotFoundException(`Index ${input.IndexName} not found`);
|
|
1723
|
+
}
|
|
1724
|
+
const result = table.scanIndex(input.IndexName, void 0, input.ExclusiveStartKey);
|
|
1725
|
+
items = result.items;
|
|
1726
|
+
lastEvaluatedKey = result.lastEvaluatedKey;
|
|
1727
|
+
} else {
|
|
1728
|
+
const result = table.scan(void 0, input.ExclusiveStartKey);
|
|
1729
|
+
items = result.items;
|
|
1730
|
+
lastEvaluatedKey = result.lastEvaluatedKey;
|
|
1731
|
+
}
|
|
1732
|
+
if (input.TotalSegments !== void 0 && input.Segment !== void 0) {
|
|
1733
|
+
const segmentSize = Math.ceil(items.length / input.TotalSegments);
|
|
1734
|
+
const start = input.Segment * segmentSize;
|
|
1735
|
+
const end = start + segmentSize;
|
|
1736
|
+
items = items.slice(start, end);
|
|
1737
|
+
}
|
|
1738
|
+
const scannedCount = items.length;
|
|
1739
|
+
if (input.FilterExpression) {
|
|
1740
|
+
items = items.filter(
|
|
1741
|
+
(item) => evaluateCondition(input.FilterExpression, item, {
|
|
1742
|
+
expressionAttributeNames: input.ExpressionAttributeNames,
|
|
1743
|
+
expressionAttributeValues: input.ExpressionAttributeValues
|
|
1744
|
+
})
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
if (input.Limit && items.length > input.Limit) {
|
|
1748
|
+
items = items.slice(0, input.Limit);
|
|
1749
|
+
if (items.length > 0) {
|
|
1750
|
+
const lastItem = items[items.length - 1];
|
|
1751
|
+
lastEvaluatedKey = {};
|
|
1752
|
+
const hashKey = table.getHashKeyName();
|
|
1753
|
+
const rangeKey = table.getRangeKeyName();
|
|
1754
|
+
if (lastItem[hashKey]) {
|
|
1755
|
+
lastEvaluatedKey[hashKey] = lastItem[hashKey];
|
|
1756
|
+
}
|
|
1757
|
+
if (rangeKey && lastItem[rangeKey]) {
|
|
1758
|
+
lastEvaluatedKey[rangeKey] = lastItem[rangeKey];
|
|
1759
|
+
}
|
|
1760
|
+
if (input.IndexName) {
|
|
1761
|
+
const indexKeySchema = table.getIndexKeySchema(input.IndexName);
|
|
1762
|
+
if (indexKeySchema) {
|
|
1763
|
+
for (const key of indexKeySchema) {
|
|
1764
|
+
const attrValue = lastItem[key.AttributeName];
|
|
1765
|
+
if (attrValue) {
|
|
1766
|
+
lastEvaluatedKey[key.AttributeName] = attrValue;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (input.ProjectionExpression) {
|
|
1774
|
+
items = items.map((item) => applyProjection(item, input.ProjectionExpression, input.ExpressionAttributeNames));
|
|
1775
|
+
}
|
|
1776
|
+
const output = {
|
|
1777
|
+
Count: items.length,
|
|
1778
|
+
ScannedCount: scannedCount
|
|
1779
|
+
};
|
|
1780
|
+
if (input.Select !== "COUNT") {
|
|
1781
|
+
output.Items = items;
|
|
1782
|
+
}
|
|
1783
|
+
if (lastEvaluatedKey && Object.keys(lastEvaluatedKey).length > 0) {
|
|
1784
|
+
output.LastEvaluatedKey = lastEvaluatedKey;
|
|
1785
|
+
}
|
|
1786
|
+
if (input.ReturnConsumedCapacity && input.ReturnConsumedCapacity !== "NONE") {
|
|
1787
|
+
output.ConsumedCapacity = {
|
|
1788
|
+
TableName: input.TableName,
|
|
1789
|
+
CapacityUnits: Math.max(0.5, scannedCount * 0.5),
|
|
1790
|
+
ReadCapacityUnits: Math.max(0.5, scannedCount * 0.5)
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
return output;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// src/operations/batch-get-item.ts
|
|
1797
|
+
var MAX_BATCH_GET_ITEMS = 100;
|
|
1798
|
+
function batchGetItem(store, input) {
|
|
1799
|
+
if (!input.RequestItems) {
|
|
1800
|
+
throw new ValidationException("RequestItems is required");
|
|
1801
|
+
}
|
|
1802
|
+
let totalKeys = 0;
|
|
1803
|
+
for (const tableRequest of Object.values(input.RequestItems)) {
|
|
1804
|
+
totalKeys += tableRequest.Keys.length;
|
|
1805
|
+
}
|
|
1806
|
+
if (totalKeys > MAX_BATCH_GET_ITEMS) {
|
|
1807
|
+
throw new ValidationException(`Too many items requested for the BatchGetItem call. Max is ${MAX_BATCH_GET_ITEMS}`);
|
|
1808
|
+
}
|
|
1809
|
+
const responses = {};
|
|
1810
|
+
const consumedCapacity = [];
|
|
1811
|
+
for (const [tableName, tableRequest] of Object.entries(input.RequestItems)) {
|
|
1812
|
+
const table = store.getTable(tableName);
|
|
1813
|
+
const items = [];
|
|
1814
|
+
let capacityUnits = 0;
|
|
1815
|
+
for (const key of tableRequest.Keys) {
|
|
1816
|
+
let item = table.getItem(key);
|
|
1817
|
+
if (item) {
|
|
1818
|
+
if (tableRequest.ProjectionExpression) {
|
|
1819
|
+
item = applyProjection(item, tableRequest.ProjectionExpression, tableRequest.ExpressionAttributeNames);
|
|
1820
|
+
}
|
|
1821
|
+
items.push(item);
|
|
1822
|
+
capacityUnits += 0.5;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
if (items.length > 0) {
|
|
1826
|
+
responses[tableName] = items;
|
|
1827
|
+
}
|
|
1828
|
+
if (input.ReturnConsumedCapacity && input.ReturnConsumedCapacity !== "NONE") {
|
|
1829
|
+
consumedCapacity.push({
|
|
1830
|
+
TableName: tableName,
|
|
1831
|
+
CapacityUnits: capacityUnits,
|
|
1832
|
+
ReadCapacityUnits: capacityUnits
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
const output = {};
|
|
1837
|
+
if (Object.keys(responses).length > 0) {
|
|
1838
|
+
output.Responses = responses;
|
|
1839
|
+
}
|
|
1840
|
+
if (input.ReturnConsumedCapacity && input.ReturnConsumedCapacity !== "NONE") {
|
|
1841
|
+
output.ConsumedCapacity = consumedCapacity;
|
|
1842
|
+
}
|
|
1843
|
+
return output;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// src/operations/batch-write-item.ts
|
|
1847
|
+
var MAX_BATCH_WRITE_ITEMS = 25;
|
|
1848
|
+
function batchWriteItem(store, input) {
|
|
1849
|
+
if (!input.RequestItems) {
|
|
1850
|
+
throw new ValidationException("RequestItems is required");
|
|
1851
|
+
}
|
|
1852
|
+
let totalItems = 0;
|
|
1853
|
+
for (const tableRequests of Object.values(input.RequestItems)) {
|
|
1854
|
+
totalItems += tableRequests.length;
|
|
1855
|
+
}
|
|
1856
|
+
if (totalItems > MAX_BATCH_WRITE_ITEMS) {
|
|
1857
|
+
throw new ValidationException(`Too many items requested for the BatchWriteItem call. Max is ${MAX_BATCH_WRITE_ITEMS}`);
|
|
1858
|
+
}
|
|
1859
|
+
const consumedCapacity = [];
|
|
1860
|
+
for (const [tableName, requests] of Object.entries(input.RequestItems)) {
|
|
1861
|
+
const table = store.getTable(tableName);
|
|
1862
|
+
let capacityUnits = 0;
|
|
1863
|
+
for (const request of requests) {
|
|
1864
|
+
if ("PutRequest" in request) {
|
|
1865
|
+
const item = request.PutRequest.Item;
|
|
1866
|
+
const hashKey = table.getHashKeyName();
|
|
1867
|
+
if (!item[hashKey]) {
|
|
1868
|
+
throw new ValidationException(`Missing the key ${hashKey} in the item`);
|
|
1869
|
+
}
|
|
1870
|
+
const rangeKey = table.getRangeKeyName();
|
|
1871
|
+
if (rangeKey && !item[rangeKey]) {
|
|
1872
|
+
throw new ValidationException(`Missing the key ${rangeKey} in the item`);
|
|
1873
|
+
}
|
|
1874
|
+
table.putItem(item);
|
|
1875
|
+
capacityUnits += 1;
|
|
1876
|
+
} else if ("DeleteRequest" in request) {
|
|
1877
|
+
const key = request.DeleteRequest.Key;
|
|
1878
|
+
const hashKey = table.getHashKeyName();
|
|
1879
|
+
if (!key[hashKey]) {
|
|
1880
|
+
throw new ValidationException(`Missing the key ${hashKey} in the key`);
|
|
1881
|
+
}
|
|
1882
|
+
const rangeKey = table.getRangeKeyName();
|
|
1883
|
+
if (rangeKey && !key[rangeKey]) {
|
|
1884
|
+
throw new ValidationException(`Missing the key ${rangeKey} in the key`);
|
|
1885
|
+
}
|
|
1886
|
+
table.deleteItem(key);
|
|
1887
|
+
capacityUnits += 1;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
if (input.ReturnConsumedCapacity && input.ReturnConsumedCapacity !== "NONE") {
|
|
1891
|
+
consumedCapacity.push({
|
|
1892
|
+
TableName: tableName,
|
|
1893
|
+
CapacityUnits: capacityUnits,
|
|
1894
|
+
WriteCapacityUnits: capacityUnits
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
const output = {};
|
|
1899
|
+
if (input.ReturnConsumedCapacity && input.ReturnConsumedCapacity !== "NONE") {
|
|
1900
|
+
output.ConsumedCapacity = consumedCapacity;
|
|
1901
|
+
}
|
|
1902
|
+
return output;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// src/operations/transact-get-items.ts
|
|
1906
|
+
var MAX_TRANSACT_ITEMS = 100;
|
|
1907
|
+
function transactGetItems(store, input) {
|
|
1908
|
+
if (!input.TransactItems) {
|
|
1909
|
+
throw new ValidationException("TransactItems is required");
|
|
1910
|
+
}
|
|
1911
|
+
if (input.TransactItems.length > MAX_TRANSACT_ITEMS) {
|
|
1912
|
+
throw new ValidationException(`Too many items in the TransactGetItems call. Max is ${MAX_TRANSACT_ITEMS}`);
|
|
1913
|
+
}
|
|
1914
|
+
const responses = [];
|
|
1915
|
+
const consumedCapacity = /* @__PURE__ */ new Map();
|
|
1916
|
+
for (const transactItem of input.TransactItems) {
|
|
1917
|
+
const { Get: getRequest } = transactItem;
|
|
1918
|
+
if (!getRequest.TableName) {
|
|
1919
|
+
throw new ValidationException("TableName is required in Get request");
|
|
1920
|
+
}
|
|
1921
|
+
if (!getRequest.Key) {
|
|
1922
|
+
throw new ValidationException("Key is required in Get request");
|
|
1923
|
+
}
|
|
1924
|
+
const table = store.getTable(getRequest.TableName);
|
|
1925
|
+
let item = table.getItem(getRequest.Key);
|
|
1926
|
+
if (item && getRequest.ProjectionExpression) {
|
|
1927
|
+
item = applyProjection(item, getRequest.ProjectionExpression, getRequest.ExpressionAttributeNames);
|
|
1928
|
+
}
|
|
1929
|
+
responses.push({ Item: item });
|
|
1930
|
+
const current = consumedCapacity.get(getRequest.TableName) || 0;
|
|
1931
|
+
consumedCapacity.set(getRequest.TableName, current + 2);
|
|
1932
|
+
}
|
|
1933
|
+
const output = {
|
|
1934
|
+
Responses: responses
|
|
1935
|
+
};
|
|
1936
|
+
if (input.ReturnConsumedCapacity && input.ReturnConsumedCapacity !== "NONE") {
|
|
1937
|
+
output.ConsumedCapacity = Array.from(consumedCapacity.entries()).map(([tableName, units]) => ({
|
|
1938
|
+
TableName: tableName,
|
|
1939
|
+
CapacityUnits: units,
|
|
1940
|
+
ReadCapacityUnits: units
|
|
1941
|
+
}));
|
|
1942
|
+
}
|
|
1943
|
+
return output;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// src/operations/transact-write-items.ts
|
|
1947
|
+
var MAX_TRANSACT_ITEMS2 = 100;
|
|
1948
|
+
var idempotencyTokens = /* @__PURE__ */ new Map();
|
|
1949
|
+
var IDEMPOTENCY_WINDOW_MS = 10 * 60 * 1e3;
|
|
1950
|
+
function transactWriteItems(store, input) {
|
|
1951
|
+
if (!input.TransactItems) {
|
|
1952
|
+
throw new ValidationException("TransactItems is required");
|
|
1953
|
+
}
|
|
1954
|
+
if (input.TransactItems.length > MAX_TRANSACT_ITEMS2) {
|
|
1955
|
+
throw new ValidationException(`Too many items in the TransactWriteItems call. Max is ${MAX_TRANSACT_ITEMS2}`);
|
|
1956
|
+
}
|
|
1957
|
+
if (input.ClientRequestToken) {
|
|
1958
|
+
const cached = idempotencyTokens.get(input.ClientRequestToken);
|
|
1959
|
+
if (cached) {
|
|
1960
|
+
if (Date.now() - cached.timestamp < IDEMPOTENCY_WINDOW_MS) {
|
|
1961
|
+
return cached.result;
|
|
1962
|
+
}
|
|
1963
|
+
idempotencyTokens.delete(input.ClientRequestToken);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
const itemKeys = /* @__PURE__ */ new Set();
|
|
1967
|
+
for (const transactItem of input.TransactItems) {
|
|
1968
|
+
let tableName;
|
|
1969
|
+
let key;
|
|
1970
|
+
if ("ConditionCheck" in transactItem) {
|
|
1971
|
+
tableName = transactItem.ConditionCheck.TableName;
|
|
1972
|
+
key = transactItem.ConditionCheck.Key;
|
|
1973
|
+
} else if ("Put" in transactItem) {
|
|
1974
|
+
tableName = transactItem.Put.TableName;
|
|
1975
|
+
const table2 = store.getTable(tableName);
|
|
1976
|
+
key = extractKey(transactItem.Put.Item, table2.keySchema);
|
|
1977
|
+
} else if ("Delete" in transactItem) {
|
|
1978
|
+
tableName = transactItem.Delete.TableName;
|
|
1979
|
+
key = transactItem.Delete.Key;
|
|
1980
|
+
} else if ("Update" in transactItem) {
|
|
1981
|
+
tableName = transactItem.Update.TableName;
|
|
1982
|
+
key = transactItem.Update.Key;
|
|
1983
|
+
} else {
|
|
1984
|
+
throw new ValidationException("Invalid transaction item");
|
|
1985
|
+
}
|
|
1986
|
+
const table = store.getTable(tableName);
|
|
1987
|
+
const keyString = `${tableName}#${serializeKey(key, table.keySchema)}`;
|
|
1988
|
+
if (itemKeys.has(keyString)) {
|
|
1989
|
+
throw new ValidationException("Transaction request cannot include multiple operations on one item");
|
|
1990
|
+
}
|
|
1991
|
+
itemKeys.add(keyString);
|
|
1992
|
+
}
|
|
1993
|
+
const cancellationReasons = [];
|
|
1994
|
+
let hasCancellation = false;
|
|
1995
|
+
for (const transactItem of input.TransactItems) {
|
|
1996
|
+
const reason = validateTransactionItem(store, transactItem);
|
|
1997
|
+
cancellationReasons.push(reason);
|
|
1998
|
+
if (reason.Code !== "None") {
|
|
1999
|
+
hasCancellation = true;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
if (hasCancellation) {
|
|
2003
|
+
throw new TransactionCanceledException("Transaction cancelled, please refer cancance reasons for specific reasons", cancellationReasons);
|
|
2004
|
+
}
|
|
2005
|
+
const consumedCapacity = /* @__PURE__ */ new Map();
|
|
2006
|
+
for (const transactItem of input.TransactItems) {
|
|
2007
|
+
executeTransactionItem(store, transactItem, consumedCapacity);
|
|
2008
|
+
}
|
|
2009
|
+
const output = {};
|
|
2010
|
+
if (input.ReturnConsumedCapacity && input.ReturnConsumedCapacity !== "NONE") {
|
|
2011
|
+
output.ConsumedCapacity = Array.from(consumedCapacity.entries()).map(([tableName, units]) => ({
|
|
2012
|
+
TableName: tableName,
|
|
2013
|
+
CapacityUnits: units,
|
|
2014
|
+
WriteCapacityUnits: units
|
|
2015
|
+
}));
|
|
2016
|
+
}
|
|
2017
|
+
if (input.ClientRequestToken) {
|
|
2018
|
+
idempotencyTokens.set(input.ClientRequestToken, {
|
|
2019
|
+
timestamp: Date.now(),
|
|
2020
|
+
result: output
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
return output;
|
|
2024
|
+
}
|
|
2025
|
+
function validateTransactionItem(store, transactItem) {
|
|
2026
|
+
try {
|
|
2027
|
+
if ("ConditionCheck" in transactItem) {
|
|
2028
|
+
const { ConditionCheck: check } = transactItem;
|
|
2029
|
+
const table = store.getTable(check.TableName);
|
|
2030
|
+
const existingItem = table.getItem(check.Key);
|
|
2031
|
+
const conditionMet = evaluateCondition(check.ConditionExpression, existingItem || {}, {
|
|
2032
|
+
expressionAttributeNames: check.ExpressionAttributeNames,
|
|
2033
|
+
expressionAttributeValues: check.ExpressionAttributeValues
|
|
2034
|
+
});
|
|
2035
|
+
if (!conditionMet) {
|
|
2036
|
+
return {
|
|
2037
|
+
Code: "ConditionalCheckFailed",
|
|
2038
|
+
Message: "The conditional request failed",
|
|
2039
|
+
Item: check.ReturnValuesOnConditionCheckFailure === "ALL_OLD" ? existingItem : void 0
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
} else if ("Put" in transactItem) {
|
|
2043
|
+
const { Put: put } = transactItem;
|
|
2044
|
+
if (put.ConditionExpression) {
|
|
2045
|
+
const table = store.getTable(put.TableName);
|
|
2046
|
+
const key = extractKey(put.Item, table.keySchema);
|
|
2047
|
+
const existingItem = table.getItem(key);
|
|
2048
|
+
const conditionMet = evaluateCondition(put.ConditionExpression, existingItem || {}, {
|
|
2049
|
+
expressionAttributeNames: put.ExpressionAttributeNames,
|
|
2050
|
+
expressionAttributeValues: put.ExpressionAttributeValues
|
|
2051
|
+
});
|
|
2052
|
+
if (!conditionMet) {
|
|
2053
|
+
return {
|
|
2054
|
+
Code: "ConditionalCheckFailed",
|
|
2055
|
+
Message: "The conditional request failed",
|
|
2056
|
+
Item: put.ReturnValuesOnConditionCheckFailure === "ALL_OLD" ? existingItem : void 0
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
} else if ("Delete" in transactItem) {
|
|
2061
|
+
const { Delete: del } = transactItem;
|
|
2062
|
+
if (del.ConditionExpression) {
|
|
2063
|
+
const table = store.getTable(del.TableName);
|
|
2064
|
+
const existingItem = table.getItem(del.Key);
|
|
2065
|
+
const conditionMet = evaluateCondition(del.ConditionExpression, existingItem || {}, {
|
|
2066
|
+
expressionAttributeNames: del.ExpressionAttributeNames,
|
|
2067
|
+
expressionAttributeValues: del.ExpressionAttributeValues
|
|
2068
|
+
});
|
|
2069
|
+
if (!conditionMet) {
|
|
2070
|
+
return {
|
|
2071
|
+
Code: "ConditionalCheckFailed",
|
|
2072
|
+
Message: "The conditional request failed",
|
|
2073
|
+
Item: del.ReturnValuesOnConditionCheckFailure === "ALL_OLD" ? existingItem : void 0
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
} else if ("Update" in transactItem) {
|
|
2078
|
+
const { Update: update } = transactItem;
|
|
2079
|
+
if (update.ConditionExpression) {
|
|
2080
|
+
const table = store.getTable(update.TableName);
|
|
2081
|
+
const existingItem = table.getItem(update.Key);
|
|
2082
|
+
const conditionMet = evaluateCondition(update.ConditionExpression, existingItem || {}, {
|
|
2083
|
+
expressionAttributeNames: update.ExpressionAttributeNames,
|
|
2084
|
+
expressionAttributeValues: update.ExpressionAttributeValues
|
|
2085
|
+
});
|
|
2086
|
+
if (!conditionMet) {
|
|
2087
|
+
return {
|
|
2088
|
+
Code: "ConditionalCheckFailed",
|
|
2089
|
+
Message: "The conditional request failed",
|
|
2090
|
+
Item: update.ReturnValuesOnConditionCheckFailure === "ALL_OLD" ? existingItem : void 0
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
return { Code: "None", Message: null };
|
|
2096
|
+
} catch (error) {
|
|
2097
|
+
if (error instanceof ConditionalCheckFailedException) {
|
|
2098
|
+
return {
|
|
2099
|
+
Code: "ConditionalCheckFailed",
|
|
2100
|
+
Message: error.message,
|
|
2101
|
+
Item: error.Item
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
return {
|
|
2105
|
+
Code: "ValidationError",
|
|
2106
|
+
Message: error instanceof Error ? error.message : "Unknown error"
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
function executeTransactionItem(store, transactItem, consumedCapacity) {
|
|
2111
|
+
if ("ConditionCheck" in transactItem) {
|
|
2112
|
+
const tableName = transactItem.ConditionCheck.TableName;
|
|
2113
|
+
const current = consumedCapacity.get(tableName) || 0;
|
|
2114
|
+
consumedCapacity.set(tableName, current + 2);
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
if ("Put" in transactItem) {
|
|
2118
|
+
const { Put: put } = transactItem;
|
|
2119
|
+
const table = store.getTable(put.TableName);
|
|
2120
|
+
table.putItem(put.Item);
|
|
2121
|
+
const current = consumedCapacity.get(put.TableName) || 0;
|
|
2122
|
+
consumedCapacity.set(put.TableName, current + 2);
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
if ("Delete" in transactItem) {
|
|
2126
|
+
const { Delete: del } = transactItem;
|
|
2127
|
+
const table = store.getTable(del.TableName);
|
|
2128
|
+
table.deleteItem(del.Key);
|
|
2129
|
+
const current = consumedCapacity.get(del.TableName) || 0;
|
|
2130
|
+
consumedCapacity.set(del.TableName, current + 2);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
if ("Update" in transactItem) {
|
|
2134
|
+
const { Update: update } = transactItem;
|
|
2135
|
+
const table = store.getTable(update.TableName);
|
|
2136
|
+
const existingItem = table.getItem(update.Key);
|
|
2137
|
+
let item = existingItem ? { ...existingItem } : { ...update.Key };
|
|
2138
|
+
item = applyUpdateExpression(item, update.UpdateExpression, {
|
|
2139
|
+
expressionAttributeNames: update.ExpressionAttributeNames,
|
|
2140
|
+
expressionAttributeValues: update.ExpressionAttributeValues
|
|
2141
|
+
});
|
|
2142
|
+
for (const [key, value] of Object.entries(update.Key)) {
|
|
2143
|
+
item[key] = value;
|
|
2144
|
+
}
|
|
2145
|
+
table.updateItem(update.Key, item);
|
|
2146
|
+
const current = consumedCapacity.get(update.TableName) || 0;
|
|
2147
|
+
consumedCapacity.set(update.TableName, current + 2);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
// src/server.ts
|
|
2152
|
+
var operations = {
|
|
2153
|
+
CreateTable: createTable,
|
|
2154
|
+
DeleteTable: deleteTable,
|
|
2155
|
+
DescribeTable: describeTable,
|
|
2156
|
+
ListTables: listTables,
|
|
2157
|
+
PutItem: putItem,
|
|
2158
|
+
GetItem: getItem,
|
|
2159
|
+
DeleteItem: deleteItem,
|
|
2160
|
+
UpdateItem: updateItem,
|
|
2161
|
+
Query: query,
|
|
2162
|
+
Scan: scan,
|
|
2163
|
+
BatchGetItem: batchGetItem,
|
|
2164
|
+
BatchWriteItem: batchWriteItem,
|
|
2165
|
+
TransactGetItems: transactGetItems,
|
|
2166
|
+
TransactWriteItems: transactWriteItems
|
|
2167
|
+
};
|
|
2168
|
+
function parseTarget(target) {
|
|
2169
|
+
if (!target) return null;
|
|
2170
|
+
const match = target.match(/^DynamoDB_\d+\.(\w+)$/);
|
|
2171
|
+
return match ? match[1] ?? null : null;
|
|
2172
|
+
}
|
|
2173
|
+
function generateUUID() {
|
|
2174
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
2175
|
+
return crypto.randomUUID();
|
|
2176
|
+
}
|
|
2177
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
2178
|
+
const r = Math.random() * 16 | 0;
|
|
2179
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
2180
|
+
return v.toString(16);
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
function formatError(error) {
|
|
2184
|
+
if (error instanceof DynamoDBError) {
|
|
2185
|
+
return {
|
|
2186
|
+
body: JSON.stringify(error.toJSON()),
|
|
2187
|
+
status: error.statusCode
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
return {
|
|
2191
|
+
body: JSON.stringify({
|
|
2192
|
+
__type: "com.amazonaws.dynamodb.v20120810#InternalServerError",
|
|
2193
|
+
message: error instanceof Error ? error.message : "Internal server error"
|
|
2194
|
+
}),
|
|
2195
|
+
status: 500
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
async function handleRequest(store, method, target, getBody) {
|
|
2199
|
+
const requestId = generateUUID();
|
|
2200
|
+
if (method !== "POST") {
|
|
2201
|
+
return {
|
|
2202
|
+
body: JSON.stringify({ message: "Method not allowed" }),
|
|
2203
|
+
status: 405,
|
|
2204
|
+
requestId
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
const operation = parseTarget(target);
|
|
2208
|
+
if (!operation) {
|
|
2209
|
+
return {
|
|
2210
|
+
body: JSON.stringify({
|
|
2211
|
+
__type: "com.amazon.coral.service#UnknownOperationException",
|
|
2212
|
+
message: "Unknown operation"
|
|
2213
|
+
}),
|
|
2214
|
+
status: 400,
|
|
2215
|
+
requestId
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
const handler = operations[operation];
|
|
2219
|
+
if (!handler) {
|
|
2220
|
+
return {
|
|
2221
|
+
body: JSON.stringify({
|
|
2222
|
+
__type: "com.amazon.coral.service#UnknownOperationException",
|
|
2223
|
+
message: `Unknown operation: ${operation}`
|
|
2224
|
+
}),
|
|
2225
|
+
status: 400,
|
|
2226
|
+
requestId
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
let body;
|
|
2230
|
+
try {
|
|
2231
|
+
const text = await getBody();
|
|
2232
|
+
body = text ? JSON.parse(text) : {};
|
|
2233
|
+
} catch {
|
|
2234
|
+
const err = formatError(new SerializationException("Could not parse request body"));
|
|
2235
|
+
return { ...err, requestId };
|
|
2236
|
+
}
|
|
2237
|
+
try {
|
|
2238
|
+
const result = handler(store, body);
|
|
2239
|
+
return {
|
|
2240
|
+
body: JSON.stringify(result),
|
|
2241
|
+
status: 200,
|
|
2242
|
+
requestId
|
|
2243
|
+
};
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
const err = formatError(error);
|
|
2246
|
+
return { ...err, requestId };
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
var isBun = typeof globalThis.Bun !== "undefined";
|
|
2250
|
+
function createBunServer(store, port) {
|
|
2251
|
+
const server = Bun.serve({
|
|
2252
|
+
port,
|
|
2253
|
+
async fetch(req) {
|
|
2254
|
+
const result = await handleRequest(store, req.method, req.headers.get("X-Amz-Target"), () => req.text());
|
|
2255
|
+
return new Response(result.body, {
|
|
2256
|
+
status: result.status,
|
|
2257
|
+
headers: {
|
|
2258
|
+
"Content-Type": "application/x-amz-json-1.0",
|
|
2259
|
+
"x-amzn-RequestId": result.requestId
|
|
2260
|
+
}
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
});
|
|
2264
|
+
return {
|
|
2265
|
+
port: server.port ?? port,
|
|
2266
|
+
stop: () => server.stop()
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
function createNodeServer(store, port) {
|
|
2270
|
+
return new Promise((resolve, reject) => {
|
|
2271
|
+
const server = createHttpServer(async (req, res) => {
|
|
2272
|
+
const getBody = () => {
|
|
2273
|
+
return new Promise((resolve2, reject2) => {
|
|
2274
|
+
let body = "";
|
|
2275
|
+
req.on("data", (chunk) => {
|
|
2276
|
+
body += chunk.toString();
|
|
2277
|
+
});
|
|
2278
|
+
req.on("end", () => resolve2(body));
|
|
2279
|
+
req.on("error", reject2);
|
|
2280
|
+
});
|
|
2281
|
+
};
|
|
2282
|
+
const result = await handleRequest(
|
|
2283
|
+
store,
|
|
2284
|
+
req.method ?? "GET",
|
|
2285
|
+
req.headers["x-amz-target"],
|
|
2286
|
+
getBody
|
|
2287
|
+
);
|
|
2288
|
+
res.writeHead(result.status, {
|
|
2289
|
+
"Content-Type": "application/x-amz-json-1.0",
|
|
2290
|
+
"x-amzn-RequestId": result.requestId
|
|
2291
|
+
});
|
|
2292
|
+
res.end(result.body);
|
|
2293
|
+
});
|
|
2294
|
+
server.on("error", reject);
|
|
2295
|
+
server.listen(port, () => {
|
|
2296
|
+
const address = server.address();
|
|
2297
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
2298
|
+
resolve({
|
|
2299
|
+
port: actualPort,
|
|
2300
|
+
stop: () => server.close()
|
|
2301
|
+
});
|
|
2302
|
+
});
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
function createServer(store, port) {
|
|
2306
|
+
if (isBun) {
|
|
2307
|
+
return createBunServer(store, port);
|
|
2308
|
+
}
|
|
2309
|
+
return createNodeServer(store, port);
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
// src/store/table.ts
|
|
2313
|
+
var sequenceCounter = 0;
|
|
2314
|
+
function generateSequenceNumber() {
|
|
2315
|
+
return String(++sequenceCounter).padStart(21, "0");
|
|
2316
|
+
}
|
|
2317
|
+
function generateEventId() {
|
|
2318
|
+
return crypto.randomUUID().replace(/-/g, "");
|
|
2319
|
+
}
|
|
2320
|
+
var Table = class {
|
|
2321
|
+
name;
|
|
2322
|
+
keySchema;
|
|
2323
|
+
attributeDefinitions;
|
|
2324
|
+
provisionedThroughput;
|
|
2325
|
+
billingMode;
|
|
2326
|
+
createdAt;
|
|
2327
|
+
tableId;
|
|
2328
|
+
streamSpecification;
|
|
2329
|
+
latestStreamArn;
|
|
2330
|
+
latestStreamLabel;
|
|
2331
|
+
ttlSpecification;
|
|
2332
|
+
items = /* @__PURE__ */ new Map();
|
|
2333
|
+
globalSecondaryIndexes = /* @__PURE__ */ new Map();
|
|
2334
|
+
localSecondaryIndexes = /* @__PURE__ */ new Map();
|
|
2335
|
+
streamCallbacks = /* @__PURE__ */ new Set();
|
|
2336
|
+
region;
|
|
2337
|
+
constructor(options, region = "us-east-1") {
|
|
2338
|
+
this.name = options.tableName;
|
|
2339
|
+
this.keySchema = options.keySchema;
|
|
2340
|
+
this.attributeDefinitions = options.attributeDefinitions;
|
|
2341
|
+
this.provisionedThroughput = options.provisionedThroughput;
|
|
2342
|
+
this.billingMode = options.billingMode ?? (options.provisionedThroughput ? "PROVISIONED" : "PAY_PER_REQUEST");
|
|
2343
|
+
this.createdAt = Date.now();
|
|
2344
|
+
this.tableId = crypto.randomUUID();
|
|
2345
|
+
this.region = region;
|
|
2346
|
+
this.streamSpecification = options.streamSpecification;
|
|
2347
|
+
this.ttlSpecification = options.timeToLiveSpecification;
|
|
2348
|
+
if (options.streamSpecification?.StreamEnabled) {
|
|
2349
|
+
this.latestStreamLabel = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2350
|
+
this.latestStreamArn = `arn:aws:dynamodb:${region}:000000000000:table/${this.name}/stream/${this.latestStreamLabel}`;
|
|
2351
|
+
}
|
|
2352
|
+
if (options.globalSecondaryIndexes) {
|
|
2353
|
+
for (const gsi of options.globalSecondaryIndexes) {
|
|
2354
|
+
this.globalSecondaryIndexes.set(gsi.IndexName, {
|
|
2355
|
+
keySchema: gsi.KeySchema,
|
|
2356
|
+
projection: gsi.Projection,
|
|
2357
|
+
provisionedThroughput: gsi.ProvisionedThroughput,
|
|
2358
|
+
items: /* @__PURE__ */ new Map()
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
if (options.localSecondaryIndexes) {
|
|
2363
|
+
for (const lsi of options.localSecondaryIndexes) {
|
|
2364
|
+
this.localSecondaryIndexes.set(lsi.IndexName, {
|
|
2365
|
+
keySchema: lsi.KeySchema,
|
|
2366
|
+
projection: lsi.Projection,
|
|
2367
|
+
items: /* @__PURE__ */ new Map()
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
getHashKeyName() {
|
|
2373
|
+
return getHashKey(this.keySchema);
|
|
2374
|
+
}
|
|
2375
|
+
getRangeKeyName() {
|
|
2376
|
+
return getRangeKey(this.keySchema);
|
|
2377
|
+
}
|
|
2378
|
+
getTtlAttributeName() {
|
|
2379
|
+
if (this.ttlSpecification?.Enabled) {
|
|
2380
|
+
return this.ttlSpecification.AttributeName;
|
|
2381
|
+
}
|
|
2382
|
+
return void 0;
|
|
2383
|
+
}
|
|
2384
|
+
setTtlSpecification(spec) {
|
|
2385
|
+
this.ttlSpecification = spec;
|
|
2386
|
+
}
|
|
2387
|
+
getTtlSpecification() {
|
|
2388
|
+
return this.ttlSpecification;
|
|
2389
|
+
}
|
|
2390
|
+
describe() {
|
|
2391
|
+
const desc = {
|
|
2392
|
+
TableName: this.name,
|
|
2393
|
+
TableStatus: "ACTIVE",
|
|
2394
|
+
CreationDateTime: this.createdAt / 1e3,
|
|
2395
|
+
TableArn: `arn:aws:dynamodb:${this.region}:000000000000:table/${this.name}`,
|
|
2396
|
+
TableId: this.tableId,
|
|
2397
|
+
KeySchema: this.keySchema,
|
|
2398
|
+
AttributeDefinitions: this.attributeDefinitions,
|
|
2399
|
+
ItemCount: this.items.size,
|
|
2400
|
+
TableSizeBytes: this.estimateTableSize()
|
|
2401
|
+
};
|
|
2402
|
+
if (this.billingMode === "PROVISIONED" && this.provisionedThroughput) {
|
|
2403
|
+
desc.ProvisionedThroughput = {
|
|
2404
|
+
ReadCapacityUnits: this.provisionedThroughput.ReadCapacityUnits,
|
|
2405
|
+
WriteCapacityUnits: this.provisionedThroughput.WriteCapacityUnits,
|
|
2406
|
+
NumberOfDecreasesToday: 0
|
|
2407
|
+
};
|
|
2408
|
+
} else {
|
|
2409
|
+
desc.BillingModeSummary = {
|
|
2410
|
+
BillingMode: "PAY_PER_REQUEST"
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
if (this.globalSecondaryIndexes.size > 0) {
|
|
2414
|
+
desc.GlobalSecondaryIndexes = this.describeGlobalSecondaryIndexes();
|
|
2415
|
+
}
|
|
2416
|
+
if (this.localSecondaryIndexes.size > 0) {
|
|
2417
|
+
desc.LocalSecondaryIndexes = this.describeLocalSecondaryIndexes();
|
|
2418
|
+
}
|
|
2419
|
+
if (this.streamSpecification) {
|
|
2420
|
+
desc.StreamSpecification = this.streamSpecification;
|
|
2421
|
+
if (this.latestStreamArn) {
|
|
2422
|
+
desc.LatestStreamArn = this.latestStreamArn;
|
|
2423
|
+
desc.LatestStreamLabel = this.latestStreamLabel;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
return desc;
|
|
2427
|
+
}
|
|
2428
|
+
describeGlobalSecondaryIndexes() {
|
|
2429
|
+
const indexes = [];
|
|
2430
|
+
for (const [name, data] of this.globalSecondaryIndexes) {
|
|
2431
|
+
let itemCount = 0;
|
|
2432
|
+
for (const keys of data.items.values()) {
|
|
2433
|
+
itemCount += keys.size;
|
|
2434
|
+
}
|
|
2435
|
+
indexes.push({
|
|
2436
|
+
IndexName: name,
|
|
2437
|
+
KeySchema: data.keySchema,
|
|
2438
|
+
Projection: data.projection,
|
|
2439
|
+
IndexStatus: "ACTIVE",
|
|
2440
|
+
ProvisionedThroughput: data.provisionedThroughput,
|
|
2441
|
+
IndexSizeBytes: 0,
|
|
2442
|
+
ItemCount: itemCount,
|
|
2443
|
+
IndexArn: `arn:aws:dynamodb:${this.region}:000000000000:table/${this.name}/index/${name}`
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
return indexes;
|
|
2447
|
+
}
|
|
2448
|
+
describeLocalSecondaryIndexes() {
|
|
2449
|
+
const indexes = [];
|
|
2450
|
+
for (const [name, data] of this.localSecondaryIndexes) {
|
|
2451
|
+
let itemCount = 0;
|
|
2452
|
+
for (const keys of data.items.values()) {
|
|
2453
|
+
itemCount += keys.size;
|
|
2454
|
+
}
|
|
2455
|
+
indexes.push({
|
|
2456
|
+
IndexName: name,
|
|
2457
|
+
KeySchema: data.keySchema,
|
|
2458
|
+
Projection: data.projection,
|
|
2459
|
+
IndexSizeBytes: 0,
|
|
2460
|
+
ItemCount: itemCount,
|
|
2461
|
+
IndexArn: `arn:aws:dynamodb:${this.region}:000000000000:table/${this.name}/index/${name}`
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
return indexes;
|
|
2465
|
+
}
|
|
2466
|
+
estimateTableSize() {
|
|
2467
|
+
let size = 0;
|
|
2468
|
+
for (const item of this.items.values()) {
|
|
2469
|
+
size += estimateItemSize(item);
|
|
2470
|
+
}
|
|
2471
|
+
return size;
|
|
2472
|
+
}
|
|
2473
|
+
getItem(key) {
|
|
2474
|
+
const keyString = serializeKey(key, this.keySchema);
|
|
2475
|
+
const item = this.items.get(keyString);
|
|
2476
|
+
return item ? deepClone(item) : void 0;
|
|
2477
|
+
}
|
|
2478
|
+
putItem(item) {
|
|
2479
|
+
const key = extractKey(item, this.keySchema);
|
|
2480
|
+
const keyString = serializeKey(key, this.keySchema);
|
|
2481
|
+
const oldItem = this.items.get(keyString);
|
|
2482
|
+
this.items.set(keyString, deepClone(item));
|
|
2483
|
+
this.updateIndexes(item, oldItem);
|
|
2484
|
+
this.emitStreamRecord(oldItem ? "MODIFY" : "INSERT", key, oldItem, item);
|
|
2485
|
+
return oldItem ? deepClone(oldItem) : void 0;
|
|
2486
|
+
}
|
|
2487
|
+
deleteItem(key) {
|
|
2488
|
+
const keyString = serializeKey(key, this.keySchema);
|
|
2489
|
+
const oldItem = this.items.get(keyString);
|
|
2490
|
+
if (oldItem) {
|
|
2491
|
+
this.items.delete(keyString);
|
|
2492
|
+
this.removeFromIndexes(oldItem);
|
|
2493
|
+
this.emitStreamRecord("REMOVE", key, oldItem, void 0);
|
|
2494
|
+
}
|
|
2495
|
+
return oldItem ? deepClone(oldItem) : void 0;
|
|
2496
|
+
}
|
|
2497
|
+
updateItem(key, updatedItem) {
|
|
2498
|
+
const keyString = serializeKey(key, this.keySchema);
|
|
2499
|
+
const oldItem = this.items.get(keyString);
|
|
2500
|
+
this.items.set(keyString, deepClone(updatedItem));
|
|
2501
|
+
this.updateIndexes(updatedItem, oldItem);
|
|
2502
|
+
this.emitStreamRecord(oldItem ? "MODIFY" : "INSERT", key, oldItem, updatedItem);
|
|
2503
|
+
return oldItem ? deepClone(oldItem) : void 0;
|
|
2504
|
+
}
|
|
2505
|
+
updateIndexes(newItem, oldItem) {
|
|
2506
|
+
if (oldItem) {
|
|
2507
|
+
this.removeFromIndexes(oldItem);
|
|
2508
|
+
}
|
|
2509
|
+
this.addToIndexes(newItem);
|
|
2510
|
+
}
|
|
2511
|
+
addToIndexes(item) {
|
|
2512
|
+
const primaryKey = serializeKey(extractKey(item, this.keySchema), this.keySchema);
|
|
2513
|
+
for (const [, indexData] of this.globalSecondaryIndexes) {
|
|
2514
|
+
const indexKey = this.buildIndexKey(item, indexData.keySchema);
|
|
2515
|
+
if (indexKey) {
|
|
2516
|
+
let keys = indexData.items.get(indexKey);
|
|
2517
|
+
if (!keys) {
|
|
2518
|
+
keys = /* @__PURE__ */ new Set();
|
|
2519
|
+
indexData.items.set(indexKey, keys);
|
|
2520
|
+
}
|
|
2521
|
+
keys.add(primaryKey);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
for (const [, indexData] of this.localSecondaryIndexes) {
|
|
2525
|
+
const indexKey = this.buildIndexKey(item, indexData.keySchema);
|
|
2526
|
+
if (indexKey) {
|
|
2527
|
+
let keys = indexData.items.get(indexKey);
|
|
2528
|
+
if (!keys) {
|
|
2529
|
+
keys = /* @__PURE__ */ new Set();
|
|
2530
|
+
indexData.items.set(indexKey, keys);
|
|
2531
|
+
}
|
|
2532
|
+
keys.add(primaryKey);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
removeFromIndexes(item) {
|
|
2537
|
+
const primaryKey = serializeKey(extractKey(item, this.keySchema), this.keySchema);
|
|
2538
|
+
for (const [, indexData] of this.globalSecondaryIndexes) {
|
|
2539
|
+
const indexKey = this.buildIndexKey(item, indexData.keySchema);
|
|
2540
|
+
if (indexKey) {
|
|
2541
|
+
const keys = indexData.items.get(indexKey);
|
|
2542
|
+
if (keys) {
|
|
2543
|
+
keys.delete(primaryKey);
|
|
2544
|
+
if (keys.size === 0) {
|
|
2545
|
+
indexData.items.delete(indexKey);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
for (const [, indexData] of this.localSecondaryIndexes) {
|
|
2551
|
+
const indexKey = this.buildIndexKey(item, indexData.keySchema);
|
|
2552
|
+
if (indexKey) {
|
|
2553
|
+
const keys = indexData.items.get(indexKey);
|
|
2554
|
+
if (keys) {
|
|
2555
|
+
keys.delete(primaryKey);
|
|
2556
|
+
if (keys.size === 0) {
|
|
2557
|
+
indexData.items.delete(indexKey);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
buildIndexKey(item, keySchema) {
|
|
2564
|
+
const hashAttr = getHashKey(keySchema);
|
|
2565
|
+
if (!item[hashAttr]) {
|
|
2566
|
+
return null;
|
|
2567
|
+
}
|
|
2568
|
+
const rangeAttr = getRangeKey(keySchema);
|
|
2569
|
+
if (rangeAttr && !item[rangeAttr]) {
|
|
2570
|
+
return null;
|
|
2571
|
+
}
|
|
2572
|
+
return serializeKey(extractKey(item, keySchema), keySchema);
|
|
2573
|
+
}
|
|
2574
|
+
scan(limit, exclusiveStartKey) {
|
|
2575
|
+
const hashAttr = this.getHashKeyName();
|
|
2576
|
+
const rangeAttr = this.getRangeKeyName();
|
|
2577
|
+
const allItems = Array.from(this.items.values()).map((item) => deepClone(item));
|
|
2578
|
+
allItems.sort((a, b) => {
|
|
2579
|
+
const hashA = a[hashAttr];
|
|
2580
|
+
const hashB = b[hashAttr];
|
|
2581
|
+
if (hashA && hashB) {
|
|
2582
|
+
const hashCmp = hashAttributeValue(hashA).localeCompare(hashAttributeValue(hashB));
|
|
2583
|
+
if (hashCmp !== 0) return hashCmp;
|
|
2584
|
+
}
|
|
2585
|
+
if (rangeAttr) {
|
|
2586
|
+
const rangeA = a[rangeAttr];
|
|
2587
|
+
const rangeB = b[rangeAttr];
|
|
2588
|
+
if (rangeA && rangeB) {
|
|
2589
|
+
return this.compareAttributes(rangeA, rangeB);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
return 0;
|
|
2593
|
+
});
|
|
2594
|
+
let startIdx = 0;
|
|
2595
|
+
if (exclusiveStartKey) {
|
|
2596
|
+
const startKey = serializeKey(exclusiveStartKey, this.keySchema);
|
|
2597
|
+
startIdx = allItems.findIndex(
|
|
2598
|
+
(item) => serializeKey(extractKey(item, this.keySchema), this.keySchema) === startKey
|
|
2599
|
+
);
|
|
2600
|
+
if (startIdx !== -1) {
|
|
2601
|
+
startIdx++;
|
|
2602
|
+
} else {
|
|
2603
|
+
startIdx = 0;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
const items = limit ? allItems.slice(startIdx, startIdx + limit) : allItems.slice(startIdx);
|
|
2607
|
+
const hasMore = limit ? startIdx + limit < allItems.length : false;
|
|
2608
|
+
const lastItem = items[items.length - 1];
|
|
2609
|
+
return {
|
|
2610
|
+
items,
|
|
2611
|
+
lastEvaluatedKey: hasMore && lastItem ? extractKey(lastItem, this.keySchema) : void 0
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
queryByHashKey(hashValue, options) {
|
|
2615
|
+
const hashAttr = this.getHashKeyName();
|
|
2616
|
+
const rangeAttr = this.getRangeKeyName();
|
|
2617
|
+
const matchingItems = [];
|
|
2618
|
+
for (const item of this.items.values()) {
|
|
2619
|
+
const itemHashValue = item[hashAttr];
|
|
2620
|
+
const queryHashValue = hashValue[hashAttr];
|
|
2621
|
+
if (itemHashValue && queryHashValue && this.attributeEquals(itemHashValue, queryHashValue)) {
|
|
2622
|
+
matchingItems.push(deepClone(item));
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
if (rangeAttr) {
|
|
2626
|
+
matchingItems.sort((a, b) => {
|
|
2627
|
+
const aVal = a[rangeAttr];
|
|
2628
|
+
const bVal = b[rangeAttr];
|
|
2629
|
+
if (!aVal && !bVal) return 0;
|
|
2630
|
+
if (!aVal) return 1;
|
|
2631
|
+
if (!bVal) return -1;
|
|
2632
|
+
const cmp = this.compareAttributes(aVal, bVal);
|
|
2633
|
+
return options?.scanIndexForward === false ? -cmp : cmp;
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
let startIdx = 0;
|
|
2637
|
+
if (options?.exclusiveStartKey) {
|
|
2638
|
+
const startKey = serializeKey(options.exclusiveStartKey, this.keySchema);
|
|
2639
|
+
startIdx = matchingItems.findIndex(
|
|
2640
|
+
(item) => serializeKey(extractKey(item, this.keySchema), this.keySchema) === startKey
|
|
2641
|
+
);
|
|
2642
|
+
if (startIdx !== -1) {
|
|
2643
|
+
startIdx++;
|
|
2644
|
+
} else {
|
|
2645
|
+
startIdx = 0;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
const limit = options?.limit;
|
|
2649
|
+
const sliced = limit ? matchingItems.slice(startIdx, startIdx + limit) : matchingItems.slice(startIdx);
|
|
2650
|
+
const hasMore = limit ? startIdx + limit < matchingItems.length : false;
|
|
2651
|
+
const lastItem = sliced[sliced.length - 1];
|
|
2652
|
+
return {
|
|
2653
|
+
items: sliced,
|
|
2654
|
+
lastEvaluatedKey: hasMore && lastItem ? extractKey(lastItem, this.keySchema) : void 0
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2657
|
+
queryIndex(indexName, hashValue, options) {
|
|
2658
|
+
const gsi = this.globalSecondaryIndexes.get(indexName);
|
|
2659
|
+
const lsi = this.localSecondaryIndexes.get(indexName);
|
|
2660
|
+
const indexData = gsi || lsi;
|
|
2661
|
+
if (!indexData) {
|
|
2662
|
+
throw new Error(`Index ${indexName} not found`);
|
|
2663
|
+
}
|
|
2664
|
+
const indexHashAttr = getHashKey(indexData.keySchema);
|
|
2665
|
+
const indexRangeAttr = getRangeKey(indexData.keySchema);
|
|
2666
|
+
const matchingItems = [];
|
|
2667
|
+
for (const item of this.items.values()) {
|
|
2668
|
+
const itemHashValue = item[indexHashAttr];
|
|
2669
|
+
const queryHashValue = hashValue[indexHashAttr];
|
|
2670
|
+
if (!itemHashValue || !queryHashValue || !this.attributeEquals(itemHashValue, queryHashValue)) {
|
|
2671
|
+
continue;
|
|
2672
|
+
}
|
|
2673
|
+
if (indexRangeAttr && !item[indexRangeAttr]) {
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
matchingItems.push(deepClone(item));
|
|
2677
|
+
}
|
|
2678
|
+
if (indexRangeAttr) {
|
|
2679
|
+
matchingItems.sort((a, b) => {
|
|
2680
|
+
const aVal = a[indexRangeAttr];
|
|
2681
|
+
const bVal = b[indexRangeAttr];
|
|
2682
|
+
if (!aVal && !bVal) return 0;
|
|
2683
|
+
if (!aVal) return 1;
|
|
2684
|
+
if (!bVal) return -1;
|
|
2685
|
+
const cmp = this.compareAttributes(aVal, bVal);
|
|
2686
|
+
return options?.scanIndexForward === false ? -cmp : cmp;
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
let startIdx = 0;
|
|
2690
|
+
if (options?.exclusiveStartKey) {
|
|
2691
|
+
const combinedKeySchema = [...indexData.keySchema];
|
|
2692
|
+
const tableRangeKey = this.getRangeKeyName();
|
|
2693
|
+
if (tableRangeKey && !combinedKeySchema.some((k) => k.AttributeName === tableRangeKey)) {
|
|
2694
|
+
combinedKeySchema.push({ AttributeName: tableRangeKey, KeyType: "RANGE" });
|
|
2695
|
+
}
|
|
2696
|
+
const tableHashKey = this.getHashKeyName();
|
|
2697
|
+
if (!combinedKeySchema.some((k) => k.AttributeName === tableHashKey)) {
|
|
2698
|
+
combinedKeySchema.push({ AttributeName: tableHashKey, KeyType: "HASH" });
|
|
2699
|
+
}
|
|
2700
|
+
const startKey = serializeKey(options.exclusiveStartKey, combinedKeySchema);
|
|
2701
|
+
startIdx = matchingItems.findIndex(
|
|
2702
|
+
(item) => serializeKey(extractKey(item, combinedKeySchema), combinedKeySchema) === startKey
|
|
2703
|
+
);
|
|
2704
|
+
if (startIdx !== -1) {
|
|
2705
|
+
startIdx++;
|
|
2706
|
+
} else {
|
|
2707
|
+
startIdx = 0;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
const limit = options?.limit;
|
|
2711
|
+
const sliced = limit ? matchingItems.slice(startIdx, startIdx + limit) : matchingItems.slice(startIdx);
|
|
2712
|
+
const hasMore = limit ? startIdx + limit < matchingItems.length : false;
|
|
2713
|
+
const lastItem = sliced[sliced.length - 1];
|
|
2714
|
+
let lastEvaluatedKey;
|
|
2715
|
+
if (hasMore && lastItem) {
|
|
2716
|
+
lastEvaluatedKey = extractKey(lastItem, this.keySchema);
|
|
2717
|
+
for (const keyElement of indexData.keySchema) {
|
|
2718
|
+
const attrValue = lastItem[keyElement.AttributeName];
|
|
2719
|
+
if (attrValue) {
|
|
2720
|
+
lastEvaluatedKey[keyElement.AttributeName] = attrValue;
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
return {
|
|
2725
|
+
items: sliced,
|
|
2726
|
+
lastEvaluatedKey,
|
|
2727
|
+
indexKeySchema: indexData.keySchema
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
scanIndex(indexName, limit, exclusiveStartKey) {
|
|
2731
|
+
const gsi = this.globalSecondaryIndexes.get(indexName);
|
|
2732
|
+
const lsi = this.localSecondaryIndexes.get(indexName);
|
|
2733
|
+
const indexData = gsi || lsi;
|
|
2734
|
+
if (!indexData) {
|
|
2735
|
+
throw new Error(`Index ${indexName} not found`);
|
|
2736
|
+
}
|
|
2737
|
+
const indexHashAttr = getHashKey(indexData.keySchema);
|
|
2738
|
+
const indexRangeAttr = getRangeKey(indexData.keySchema);
|
|
2739
|
+
const matchingItems = [];
|
|
2740
|
+
for (const item of this.items.values()) {
|
|
2741
|
+
if (item[indexHashAttr]) {
|
|
2742
|
+
matchingItems.push(deepClone(item));
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
matchingItems.sort((a, b) => {
|
|
2746
|
+
const hashA = a[indexHashAttr];
|
|
2747
|
+
const hashB = b[indexHashAttr];
|
|
2748
|
+
if (hashA && hashB) {
|
|
2749
|
+
const hashCmp = hashAttributeValue(hashA).localeCompare(hashAttributeValue(hashB));
|
|
2750
|
+
if (hashCmp !== 0) return hashCmp;
|
|
2751
|
+
}
|
|
2752
|
+
if (indexRangeAttr) {
|
|
2753
|
+
const rangeA = a[indexRangeAttr];
|
|
2754
|
+
const rangeB = b[indexRangeAttr];
|
|
2755
|
+
if (rangeA && rangeB) {
|
|
2756
|
+
return this.compareAttributes(rangeA, rangeB);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
return 0;
|
|
2760
|
+
});
|
|
2761
|
+
let startIdx = 0;
|
|
2762
|
+
if (exclusiveStartKey) {
|
|
2763
|
+
const startKey = serializeKey(exclusiveStartKey, this.keySchema);
|
|
2764
|
+
startIdx = matchingItems.findIndex(
|
|
2765
|
+
(item) => serializeKey(extractKey(item, this.keySchema), this.keySchema) === startKey
|
|
2766
|
+
);
|
|
2767
|
+
if (startIdx !== -1) {
|
|
2768
|
+
startIdx++;
|
|
2769
|
+
} else {
|
|
2770
|
+
startIdx = 0;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
const sliced = limit ? matchingItems.slice(startIdx, startIdx + limit) : matchingItems.slice(startIdx);
|
|
2774
|
+
const hasMore = limit ? startIdx + limit < matchingItems.length : false;
|
|
2775
|
+
const lastItem = sliced[sliced.length - 1];
|
|
2776
|
+
let lastEvaluatedKey;
|
|
2777
|
+
if (hasMore && lastItem) {
|
|
2778
|
+
lastEvaluatedKey = extractKey(lastItem, this.keySchema);
|
|
2779
|
+
for (const keyElement of indexData.keySchema) {
|
|
2780
|
+
const attrValue = lastItem[keyElement.AttributeName];
|
|
2781
|
+
if (attrValue) {
|
|
2782
|
+
lastEvaluatedKey[keyElement.AttributeName] = attrValue;
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
return {
|
|
2787
|
+
items: sliced,
|
|
2788
|
+
lastEvaluatedKey,
|
|
2789
|
+
indexKeySchema: indexData.keySchema
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
hasIndex(indexName) {
|
|
2793
|
+
return this.globalSecondaryIndexes.has(indexName) || this.localSecondaryIndexes.has(indexName);
|
|
2794
|
+
}
|
|
2795
|
+
getIndexKeySchema(indexName) {
|
|
2796
|
+
return this.globalSecondaryIndexes.get(indexName)?.keySchema || this.localSecondaryIndexes.get(indexName)?.keySchema;
|
|
2797
|
+
}
|
|
2798
|
+
attributeEquals(a, b) {
|
|
2799
|
+
if ("S" in a && "S" in b) return a.S === b.S;
|
|
2800
|
+
if ("N" in a && "N" in b) return a.N === b.N;
|
|
2801
|
+
if ("B" in a && "B" in b) return a.B === b.B;
|
|
2802
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
2803
|
+
}
|
|
2804
|
+
compareAttributes(a, b) {
|
|
2805
|
+
if ("S" in a && "S" in b) return a.S.localeCompare(b.S);
|
|
2806
|
+
if ("N" in a && "N" in b) return parseFloat(a.N) - parseFloat(b.N);
|
|
2807
|
+
if ("B" in a && "B" in b) return a.B.localeCompare(b.B);
|
|
2808
|
+
return 0;
|
|
2809
|
+
}
|
|
2810
|
+
getAllItems() {
|
|
2811
|
+
return Array.from(this.items.values()).map((item) => deepClone(item));
|
|
2812
|
+
}
|
|
2813
|
+
clear() {
|
|
2814
|
+
this.items.clear();
|
|
2815
|
+
for (const index of this.globalSecondaryIndexes.values()) {
|
|
2816
|
+
index.items.clear();
|
|
2817
|
+
}
|
|
2818
|
+
for (const index of this.localSecondaryIndexes.values()) {
|
|
2819
|
+
index.items.clear();
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
onStreamRecord(callback) {
|
|
2823
|
+
this.streamCallbacks.add(callback);
|
|
2824
|
+
return () => {
|
|
2825
|
+
this.streamCallbacks.delete(callback);
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
emitStreamRecord(eventName, keys, oldImage, newImage) {
|
|
2829
|
+
if (!this.streamSpecification?.StreamEnabled || this.streamCallbacks.size === 0) {
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
const viewType = this.streamSpecification.StreamViewType || "NEW_AND_OLD_IMAGES";
|
|
2833
|
+
const record = {
|
|
2834
|
+
eventID: generateEventId(),
|
|
2835
|
+
eventName,
|
|
2836
|
+
eventVersion: "1.1",
|
|
2837
|
+
eventSource: "aws:dynamodb",
|
|
2838
|
+
awsRegion: this.region,
|
|
2839
|
+
dynamodb: {
|
|
2840
|
+
ApproximateCreationDateTime: Date.now() / 1e3,
|
|
2841
|
+
Keys: keys,
|
|
2842
|
+
SequenceNumber: generateSequenceNumber(),
|
|
2843
|
+
SizeBytes: estimateItemSize(keys),
|
|
2844
|
+
StreamViewType: viewType
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
if (viewType === "NEW_IMAGE" || viewType === "NEW_AND_OLD_IMAGES") {
|
|
2848
|
+
if (newImage) {
|
|
2849
|
+
record.dynamodb.NewImage = deepClone(newImage);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
if (viewType === "OLD_IMAGE" || viewType === "NEW_AND_OLD_IMAGES") {
|
|
2853
|
+
if (oldImage) {
|
|
2854
|
+
record.dynamodb.OldImage = deepClone(oldImage);
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
for (const callback of this.streamCallbacks) {
|
|
2858
|
+
try {
|
|
2859
|
+
callback(record);
|
|
2860
|
+
} catch {
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
expireTtlItems(currentTimeSeconds) {
|
|
2865
|
+
const ttlAttr = this.getTtlAttributeName();
|
|
2866
|
+
if (!ttlAttr) {
|
|
2867
|
+
return [];
|
|
2868
|
+
}
|
|
2869
|
+
const expiredItems = [];
|
|
2870
|
+
for (const [keyString, item] of this.items) {
|
|
2871
|
+
const ttlValue = item[ttlAttr];
|
|
2872
|
+
if (ttlValue && "N" in ttlValue) {
|
|
2873
|
+
const ttlTimestamp = parseInt(ttlValue.N, 10);
|
|
2874
|
+
if (ttlTimestamp <= currentTimeSeconds) {
|
|
2875
|
+
expiredItems.push(deepClone(item));
|
|
2876
|
+
const key = extractKey(item, this.keySchema);
|
|
2877
|
+
this.items.delete(keyString);
|
|
2878
|
+
this.removeFromIndexes(item);
|
|
2879
|
+
this.emitStreamRecord("REMOVE", key, item, void 0);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return expiredItems;
|
|
2884
|
+
}
|
|
2885
|
+
};
|
|
2886
|
+
|
|
2887
|
+
// src/store/index.ts
|
|
2888
|
+
var TableStore = class {
|
|
2889
|
+
tables = /* @__PURE__ */ new Map();
|
|
2890
|
+
region;
|
|
32
2891
|
constructor(region = "us-east-1") {
|
|
33
2892
|
this.region = region;
|
|
34
|
-
this.endpoint = (0, import_url_parser.parseUrl)(`http://localhost`);
|
|
35
2893
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
process;
|
|
40
|
-
async listen(port) {
|
|
41
|
-
if (this.process) {
|
|
42
|
-
throw new Error(`DynamoDB server is already listening on port: ${this.endpoint.port}`);
|
|
2894
|
+
createTable(input) {
|
|
2895
|
+
if (this.tables.has(input.TableName)) {
|
|
2896
|
+
throw new ResourceInUseException(`Table already exists: ${input.TableName}`);
|
|
43
2897
|
}
|
|
44
|
-
|
|
45
|
-
|
|
2898
|
+
const table = new Table(
|
|
2899
|
+
{
|
|
2900
|
+
tableName: input.TableName,
|
|
2901
|
+
keySchema: input.KeySchema,
|
|
2902
|
+
attributeDefinitions: input.AttributeDefinitions,
|
|
2903
|
+
provisionedThroughput: input.ProvisionedThroughput,
|
|
2904
|
+
billingMode: input.BillingMode,
|
|
2905
|
+
globalSecondaryIndexes: input.GlobalSecondaryIndexes,
|
|
2906
|
+
localSecondaryIndexes: input.LocalSecondaryIndexes,
|
|
2907
|
+
streamSpecification: input.StreamSpecification,
|
|
2908
|
+
timeToLiveSpecification: input.TimeToLiveSpecification
|
|
2909
|
+
},
|
|
2910
|
+
this.region
|
|
2911
|
+
);
|
|
2912
|
+
this.tables.set(input.TableName, table);
|
|
2913
|
+
return table;
|
|
2914
|
+
}
|
|
2915
|
+
getTable(tableName) {
|
|
2916
|
+
const table = this.tables.get(tableName);
|
|
2917
|
+
if (!table) {
|
|
2918
|
+
throw new ResourceNotFoundException(`Requested resource not found: Table: ${tableName} not found`);
|
|
46
2919
|
}
|
|
47
|
-
|
|
48
|
-
this.process = await (0, import_dynamo_db_local.spawn)({ port });
|
|
2920
|
+
return table;
|
|
49
2921
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
2922
|
+
hasTable(tableName) {
|
|
2923
|
+
return this.tables.has(tableName);
|
|
2924
|
+
}
|
|
2925
|
+
deleteTable(tableName) {
|
|
2926
|
+
const table = this.tables.get(tableName);
|
|
2927
|
+
if (!table) {
|
|
2928
|
+
throw new ResourceNotFoundException(`Requested resource not found: Table: ${tableName} not found`);
|
|
55
2929
|
}
|
|
2930
|
+
this.tables.delete(tableName);
|
|
2931
|
+
return table;
|
|
56
2932
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
2933
|
+
listTables(exclusiveStartTableName, limit) {
|
|
2934
|
+
const allNames = Array.from(this.tables.keys()).sort();
|
|
2935
|
+
let startIdx = 0;
|
|
2936
|
+
if (exclusiveStartTableName) {
|
|
2937
|
+
startIdx = allNames.indexOf(exclusiveStartTableName);
|
|
2938
|
+
if (startIdx !== -1) {
|
|
2939
|
+
startIdx++;
|
|
2940
|
+
} else {
|
|
2941
|
+
startIdx = 0;
|
|
2942
|
+
}
|
|
66
2943
|
}
|
|
2944
|
+
const tableNames = limit ? allNames.slice(startIdx, startIdx + limit) : allNames.slice(startIdx);
|
|
2945
|
+
const hasMore = limit ? startIdx + tableNames.length < allNames.length : false;
|
|
2946
|
+
const lastEvaluatedTableName = hasMore ? tableNames[tableNames.length - 1] : void 0;
|
|
2947
|
+
return { tableNames, lastEvaluatedTableName };
|
|
67
2948
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
2949
|
+
clear() {
|
|
2950
|
+
this.tables.clear();
|
|
2951
|
+
}
|
|
2952
|
+
expireTtlItems(currentTimeSeconds) {
|
|
2953
|
+
for (const table of this.tables.values()) {
|
|
2954
|
+
table.expireTtlItems(currentTimeSeconds);
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
};
|
|
2958
|
+
|
|
2959
|
+
// src/dynamodb-server.ts
|
|
2960
|
+
var DynamoDBServer = class {
|
|
2961
|
+
server;
|
|
2962
|
+
javaServer;
|
|
2963
|
+
store;
|
|
2964
|
+
clock;
|
|
2965
|
+
config;
|
|
2966
|
+
endpoint;
|
|
2967
|
+
client;
|
|
2968
|
+
documentClient;
|
|
2969
|
+
streamCallbacks = /* @__PURE__ */ new Map();
|
|
2970
|
+
constructor(config = {}) {
|
|
2971
|
+
this.config = {
|
|
2972
|
+
port: config.port ?? 0,
|
|
2973
|
+
region: config.region ?? "us-east-1",
|
|
2974
|
+
hostname: config.hostname ?? "localhost",
|
|
2975
|
+
engine: config.engine ?? "memory"
|
|
2976
|
+
};
|
|
2977
|
+
this.endpoint = {
|
|
2978
|
+
protocol: "http:",
|
|
2979
|
+
hostname: this.config.hostname,
|
|
2980
|
+
path: "/"
|
|
2981
|
+
};
|
|
2982
|
+
this.store = new TableStore(this.config.region);
|
|
2983
|
+
this.clock = new VirtualClock();
|
|
2984
|
+
}
|
|
2985
|
+
async listen(port) {
|
|
2986
|
+
if (this.server || this.javaServer) {
|
|
2987
|
+
throw new Error("Server is already running");
|
|
2988
|
+
}
|
|
2989
|
+
const listenPort = port ?? this.config.port;
|
|
2990
|
+
if (this.config.engine === "java") {
|
|
2991
|
+
this.javaServer = createJavaServer(listenPort, this.config.region);
|
|
2992
|
+
await this.javaServer.wait();
|
|
2993
|
+
this.config.port = this.javaServer.port;
|
|
2994
|
+
} else {
|
|
2995
|
+
const serverOrPromise = createServer(this.store, listenPort);
|
|
2996
|
+
if (serverOrPromise instanceof Promise) {
|
|
2997
|
+
this.server = await serverOrPromise;
|
|
2998
|
+
} else {
|
|
2999
|
+
this.server = serverOrPromise;
|
|
73
3000
|
}
|
|
74
|
-
|
|
3001
|
+
this.config.port = this.server.port;
|
|
75
3002
|
}
|
|
76
|
-
|
|
3003
|
+
this.endpoint.port = this.config.port;
|
|
3004
|
+
}
|
|
3005
|
+
async stop() {
|
|
3006
|
+
if (this.server) {
|
|
3007
|
+
this.server.stop();
|
|
3008
|
+
this.server = void 0;
|
|
3009
|
+
}
|
|
3010
|
+
if (this.javaServer) {
|
|
3011
|
+
await this.javaServer.stop();
|
|
3012
|
+
this.javaServer = void 0;
|
|
3013
|
+
}
|
|
3014
|
+
this.client = void 0;
|
|
3015
|
+
this.documentClient = void 0;
|
|
3016
|
+
}
|
|
3017
|
+
get port() {
|
|
3018
|
+
return this.config.port;
|
|
3019
|
+
}
|
|
3020
|
+
get engine() {
|
|
3021
|
+
return this.config.engine;
|
|
3022
|
+
}
|
|
3023
|
+
getEndpoint() {
|
|
3024
|
+
return this.endpoint;
|
|
77
3025
|
}
|
|
78
|
-
/** Get DynamoDBClient connected to dynamodb local. */
|
|
79
3026
|
getClient() {
|
|
80
3027
|
if (!this.client) {
|
|
81
|
-
this.client = new
|
|
82
|
-
maxAttempts: 3,
|
|
3028
|
+
this.client = new DynamoDBClient2({
|
|
83
3029
|
endpoint: this.endpoint,
|
|
84
|
-
region: this.region,
|
|
85
|
-
tls: false,
|
|
3030
|
+
region: this.config.region,
|
|
86
3031
|
credentials: {
|
|
87
3032
|
accessKeyId: "fake",
|
|
88
3033
|
secretAccessKey: "fake"
|
|
@@ -91,10 +3036,9 @@ var DynamoDBServer = class {
|
|
|
91
3036
|
}
|
|
92
3037
|
return this.client;
|
|
93
3038
|
}
|
|
94
|
-
/** Get DynamoDBDocumentClient connected to dynamodb local. */
|
|
95
3039
|
getDocumentClient() {
|
|
96
3040
|
if (!this.documentClient) {
|
|
97
|
-
this.documentClient =
|
|
3041
|
+
this.documentClient = DynamoDBDocumentClient.from(this.getClient(), {
|
|
98
3042
|
marshallOptions: {
|
|
99
3043
|
removeUndefinedValues: true
|
|
100
3044
|
}
|
|
@@ -102,8 +3046,91 @@ var DynamoDBServer = class {
|
|
|
102
3046
|
}
|
|
103
3047
|
return this.documentClient;
|
|
104
3048
|
}
|
|
3049
|
+
/**
|
|
3050
|
+
* Advance the virtual clock by the specified number of milliseconds.
|
|
3051
|
+
* This triggers TTL processing for expired items.
|
|
3052
|
+
* Only available when using the 'memory' engine.
|
|
3053
|
+
*/
|
|
3054
|
+
advanceTime(ms) {
|
|
3055
|
+
if (this.config.engine === "java") {
|
|
3056
|
+
throw new Error("advanceTime is not supported with the Java engine");
|
|
3057
|
+
}
|
|
3058
|
+
this.clock.advance(ms);
|
|
3059
|
+
this.processTTL();
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Set the virtual clock to the specified timestamp.
|
|
3063
|
+
* This triggers TTL processing for expired items.
|
|
3064
|
+
* Only available when using the 'memory' engine.
|
|
3065
|
+
*/
|
|
3066
|
+
setTime(timestamp) {
|
|
3067
|
+
if (this.config.engine === "java") {
|
|
3068
|
+
throw new Error("setTime is not supported with the Java engine");
|
|
3069
|
+
}
|
|
3070
|
+
this.clock.set(timestamp);
|
|
3071
|
+
this.processTTL();
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Get the current virtual clock time.
|
|
3075
|
+
* Only available when using the 'memory' engine.
|
|
3076
|
+
*/
|
|
3077
|
+
getTime() {
|
|
3078
|
+
if (this.config.engine === "java") {
|
|
3079
|
+
throw new Error("getTime is not supported with the Java engine");
|
|
3080
|
+
}
|
|
3081
|
+
return this.clock.now();
|
|
3082
|
+
}
|
|
3083
|
+
processTTL() {
|
|
3084
|
+
const currentTimeSeconds = this.clock.nowInSeconds();
|
|
3085
|
+
this.store.expireTtlItems(currentTimeSeconds);
|
|
3086
|
+
}
|
|
3087
|
+
/**
|
|
3088
|
+
* Register a callback for stream records on a specific table.
|
|
3089
|
+
* Only available when using the 'memory' engine.
|
|
3090
|
+
*/
|
|
3091
|
+
onStreamRecord(tableName, callback) {
|
|
3092
|
+
if (this.config.engine === "java") {
|
|
3093
|
+
throw new Error("onStreamRecord is not supported with the Java engine");
|
|
3094
|
+
}
|
|
3095
|
+
const table = this.store.getTable(tableName);
|
|
3096
|
+
return table.onStreamRecord(callback);
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Reset the server, clearing all tables and data.
|
|
3100
|
+
* Only available when using the 'memory' engine.
|
|
3101
|
+
*/
|
|
3102
|
+
reset() {
|
|
3103
|
+
if (this.config.engine === "java") {
|
|
3104
|
+
throw new Error("reset is not supported with the Java engine");
|
|
3105
|
+
}
|
|
3106
|
+
this.store.clear();
|
|
3107
|
+
this.clock.reset();
|
|
3108
|
+
this.streamCallbacks.clear();
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3111
|
+
* Get the internal table store.
|
|
3112
|
+
* Only available when using the 'memory' engine.
|
|
3113
|
+
*/
|
|
3114
|
+
getTableStore() {
|
|
3115
|
+
if (this.config.engine === "java") {
|
|
3116
|
+
throw new Error("getTableStore is not supported with the Java engine");
|
|
3117
|
+
}
|
|
3118
|
+
return this.store;
|
|
3119
|
+
}
|
|
3120
|
+
};
|
|
3121
|
+
export {
|
|
3122
|
+
ConditionalCheckFailedException,
|
|
3123
|
+
DynamoDBError,
|
|
3124
|
+
DynamoDBServer,
|
|
3125
|
+
IdempotentParameterMismatchException,
|
|
3126
|
+
InternalServerError,
|
|
3127
|
+
ItemCollectionSizeLimitExceededException,
|
|
3128
|
+
ProvisionedThroughputExceededException,
|
|
3129
|
+
ResourceInUseException,
|
|
3130
|
+
ResourceNotFoundException,
|
|
3131
|
+
SerializationException,
|
|
3132
|
+
TransactionCanceledException,
|
|
3133
|
+
TransactionConflictException,
|
|
3134
|
+
ValidationException,
|
|
3135
|
+
VirtualClock
|
|
105
3136
|
};
|
|
106
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
107
|
-
0 && (module.exports = {
|
|
108
|
-
DynamoDBServer
|
|
109
|
-
});
|