@awsless/dynamodb-server 0.1.5 → 0.1.7
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 +62 -61
- package/dist/index.d.ts +2 -0
- package/dist/index.js +285 -197
- package/package.json +1 -1
package/README.MD
CHANGED
|
@@ -9,6 +9,7 @@ A local DynamoDB server for testing and development. Provides both a fast in-mem
|
|
|
9
9
|
- **DynamoDB Streams** - Support for stream callbacks on item changes
|
|
10
10
|
- **TTL Support** - Time-to-live expiration for items
|
|
11
11
|
- **GSI/LSI Support** - Global and Local Secondary Index support
|
|
12
|
+
- **Multi Key for GSI Support** - Multi-key support for Global Secondary Indexes
|
|
12
13
|
|
|
13
14
|
## Setup
|
|
14
15
|
|
|
@@ -21,13 +22,13 @@ npm i @awsless/dynamodb-server
|
|
|
21
22
|
## Basic Usage
|
|
22
23
|
|
|
23
24
|
```ts
|
|
24
|
-
import { DynamoDBServer } from
|
|
25
|
+
import { DynamoDBServer } from '@awsless/dynamodb-server'
|
|
25
26
|
|
|
26
27
|
// Create a server with the fast in-memory engine (default)
|
|
27
28
|
const server = new DynamoDBServer()
|
|
28
29
|
|
|
29
30
|
// Or use the Java engine for full compatibility
|
|
30
|
-
const server = new DynamoDBServer({ engine:
|
|
31
|
+
const server = new DynamoDBServer({ engine: 'java' })
|
|
31
32
|
|
|
32
33
|
// Start the server
|
|
33
34
|
await server.listen(8000)
|
|
@@ -49,7 +50,7 @@ await server.stop()
|
|
|
49
50
|
The in-memory engine is optimized for speed and is perfect for unit tests. It implements the core DynamoDB operations without requiring Java.
|
|
50
51
|
|
|
51
52
|
```ts
|
|
52
|
-
const server = new DynamoDBServer({ engine:
|
|
53
|
+
const server = new DynamoDBServer({ engine: 'memory' })
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
### Java Engine
|
|
@@ -57,24 +58,24 @@ const server = new DynamoDBServer({ engine: "memory" })
|
|
|
57
58
|
The Java engine uses AWS DynamoDB Local for full compatibility with DynamoDB behavior. Requires Java to be installed.
|
|
58
59
|
|
|
59
60
|
```ts
|
|
60
|
-
const server = new DynamoDBServer({ engine:
|
|
61
|
+
const server = new DynamoDBServer({ engine: 'java' })
|
|
61
62
|
```
|
|
62
63
|
|
|
63
64
|
## Configuration Options
|
|
64
65
|
|
|
65
66
|
```ts
|
|
66
67
|
const server = new DynamoDBServer({
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
// Engine type: "memory" (default) or "java"
|
|
69
|
+
engine: 'memory',
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
// Hostname to bind to (default: "localhost")
|
|
72
|
+
hostname: 'localhost',
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
// Port to listen on (default: auto-assigned)
|
|
75
|
+
port: 8000,
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
// AWS region (default: "us-east-1")
|
|
78
|
+
region: 'us-east-1',
|
|
78
79
|
})
|
|
79
80
|
```
|
|
80
81
|
|
|
@@ -83,11 +84,11 @@ const server = new DynamoDBServer({
|
|
|
83
84
|
You can subscribe to item changes using stream callbacks:
|
|
84
85
|
|
|
85
86
|
```ts
|
|
86
|
-
const unsubscribe = server.onStreamRecord(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
const unsubscribe = server.onStreamRecord('my-table', record => {
|
|
88
|
+
console.log('Stream event:', record.eventName) // INSERT, MODIFY, or REMOVE
|
|
89
|
+
console.log('Keys:', record.dynamodb.Keys)
|
|
90
|
+
console.log('New Image:', record.dynamodb.NewImage)
|
|
91
|
+
console.log('Old Image:', record.dynamodb.OldImage)
|
|
91
92
|
})
|
|
92
93
|
|
|
93
94
|
// Unsubscribe when done
|
|
@@ -97,50 +98,50 @@ unsubscribe()
|
|
|
97
98
|
## Testing with Vitest/Jest
|
|
98
99
|
|
|
99
100
|
```ts
|
|
100
|
-
import { DynamoDBServer } from
|
|
101
|
-
import { CreateTableCommand } from
|
|
102
|
-
import { PutCommand, GetCommand } from
|
|
103
|
-
|
|
104
|
-
describe(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
101
|
+
import { DynamoDBServer } from '@awsless/dynamodb-server'
|
|
102
|
+
import { CreateTableCommand } from '@aws-sdk/client-dynamodb'
|
|
103
|
+
import { PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb'
|
|
104
|
+
|
|
105
|
+
describe('My DynamoDB Tests', () => {
|
|
106
|
+
const server = new DynamoDBServer()
|
|
107
|
+
|
|
108
|
+
beforeAll(async () => {
|
|
109
|
+
await server.listen()
|
|
110
|
+
|
|
111
|
+
// Create your tables
|
|
112
|
+
await server.getClient().send(
|
|
113
|
+
new CreateTableCommand({
|
|
114
|
+
TableName: 'users',
|
|
115
|
+
KeySchema: [{ AttributeName: 'id', KeyType: 'HASH' }],
|
|
116
|
+
AttributeDefinitions: [{ AttributeName: 'id', AttributeType: 'N' }],
|
|
117
|
+
BillingMode: 'PAY_PER_REQUEST',
|
|
118
|
+
})
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
afterAll(async () => {
|
|
123
|
+
await server.stop()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should store and retrieve items', async () => {
|
|
127
|
+
const client = server.getDocumentClient()
|
|
128
|
+
|
|
129
|
+
await client.send(
|
|
130
|
+
new PutCommand({
|
|
131
|
+
TableName: 'users',
|
|
132
|
+
Item: { id: 1, name: 'John' },
|
|
133
|
+
})
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const result = await client.send(
|
|
137
|
+
new GetCommand({
|
|
138
|
+
TableName: 'users',
|
|
139
|
+
Key: { id: 1 },
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
expect(result.Item).toEqual({ id: 1, name: 'John' })
|
|
144
|
+
})
|
|
144
145
|
})
|
|
145
146
|
```
|
|
146
147
|
|
package/dist/index.d.ts
CHANGED
|
@@ -218,6 +218,8 @@ declare class Table {
|
|
|
218
218
|
getIndexKeySchema(indexName: string): KeySchemaElement[] | undefined;
|
|
219
219
|
private attributeEquals;
|
|
220
220
|
private compareAttributes;
|
|
221
|
+
private compareByKeySchema;
|
|
222
|
+
private queryBySchema;
|
|
221
223
|
getAllItems(): AttributeMap[];
|
|
222
224
|
clear(): void;
|
|
223
225
|
onStreamRecord(callback: StreamCallback): () => void;
|
package/dist/index.js
CHANGED
|
@@ -188,9 +188,15 @@ function validateAndCreate(store, input) {
|
|
|
188
188
|
);
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
+
if (input.GlobalSecondaryIndexes) {
|
|
192
|
+
for (const gsi of input.GlobalSecondaryIndexes) {
|
|
193
|
+
validateSecondaryIndexKeySchema(gsi.IndexName, gsi.KeySchema, definedAttrs, true);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
191
196
|
if (input.LocalSecondaryIndexes) {
|
|
192
197
|
const tableHashKey = hashKeys[0].AttributeName;
|
|
193
198
|
for (const lsi of input.LocalSecondaryIndexes) {
|
|
199
|
+
validateSecondaryIndexKeySchema(lsi.IndexName, lsi.KeySchema, definedAttrs, false);
|
|
194
200
|
const lsiHashKey = lsi.KeySchema.find((k) => k.KeyType === "HASH");
|
|
195
201
|
if (!lsiHashKey || lsiHashKey.AttributeName !== tableHashKey) {
|
|
196
202
|
throw new ValidationException(
|
|
@@ -215,6 +221,43 @@ function createTable(store, input) {
|
|
|
215
221
|
const tableDescription = validateAndCreate(store, input);
|
|
216
222
|
return { TableDescription: tableDescription };
|
|
217
223
|
}
|
|
224
|
+
function validateSecondaryIndexKeySchema(indexName, keySchema, definedAttrs, isGlobal) {
|
|
225
|
+
if (!keySchema.length) {
|
|
226
|
+
throw new ValidationException(`Index ${indexName} must define a key schema`);
|
|
227
|
+
}
|
|
228
|
+
const hashKeys = keySchema.filter((k) => k.KeyType === "HASH");
|
|
229
|
+
const rangeKeys = keySchema.filter((k) => k.KeyType === "RANGE");
|
|
230
|
+
const maxHashKeys = isGlobal ? 4 : 1;
|
|
231
|
+
const maxRangeKeys = isGlobal ? 4 : 1;
|
|
232
|
+
if (hashKeys.length === 0 || hashKeys.length > maxHashKeys) {
|
|
233
|
+
throw new ValidationException(
|
|
234
|
+
isGlobal ? `Global secondary index ${indexName} must have between 1 and 4 partition key attributes` : `Local secondary index ${indexName} must have exactly one hash key`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (rangeKeys.length > maxRangeKeys) {
|
|
238
|
+
throw new ValidationException(
|
|
239
|
+
isGlobal ? `Global secondary index ${indexName} can have at most 4 sort key attributes` : `Local secondary index ${indexName} can have at most one range key`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (isGlobal && hashKeys.length + rangeKeys.length > 8) {
|
|
243
|
+
throw new ValidationException(`Global secondary index ${indexName} can have at most 8 key attributes`);
|
|
244
|
+
}
|
|
245
|
+
let seenRange = false;
|
|
246
|
+
for (const keyElement of keySchema) {
|
|
247
|
+
if (!definedAttrs.has(keyElement.AttributeName)) {
|
|
248
|
+
throw new ValidationException(
|
|
249
|
+
`Attribute ${keyElement.AttributeName} is specified in index ${indexName} but not in AttributeDefinitions`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (keyElement.KeyType === "RANGE") {
|
|
253
|
+
seenRange = true;
|
|
254
|
+
} else if (seenRange) {
|
|
255
|
+
throw new ValidationException(
|
|
256
|
+
`Index ${indexName} must list all partition key attributes before sort key attributes`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
218
261
|
|
|
219
262
|
// src/operations/delete-table.ts
|
|
220
263
|
function deleteTable(store, input) {
|
|
@@ -909,10 +952,30 @@ function getHashKey(keySchema) {
|
|
|
909
952
|
}
|
|
910
953
|
return hash.AttributeName;
|
|
911
954
|
}
|
|
955
|
+
function getHashKeys(keySchema) {
|
|
956
|
+
return keySchema.filter((k) => k.KeyType === "HASH").map((k) => k.AttributeName);
|
|
957
|
+
}
|
|
912
958
|
function getRangeKey(keySchema) {
|
|
913
959
|
const range = keySchema.find((k) => k.KeyType === "RANGE");
|
|
914
960
|
return range?.AttributeName;
|
|
915
961
|
}
|
|
962
|
+
function getRangeKeys(keySchema) {
|
|
963
|
+
return keySchema.filter((k) => k.KeyType === "RANGE").map((k) => k.AttributeName);
|
|
964
|
+
}
|
|
965
|
+
function hasCompleteKey(item, keySchema) {
|
|
966
|
+
return keySchema.every((element) => Boolean(item[element.AttributeName]));
|
|
967
|
+
}
|
|
968
|
+
function mergeKeySchemas(...schemas) {
|
|
969
|
+
const merged = [];
|
|
970
|
+
for (const schema of schemas) {
|
|
971
|
+
for (const element of schema) {
|
|
972
|
+
if (!merged.some((existing) => existing.AttributeName === element.AttributeName)) {
|
|
973
|
+
merged.push(element);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return merged;
|
|
978
|
+
}
|
|
916
979
|
function deepClone(obj) {
|
|
917
980
|
return JSON.parse(JSON.stringify(obj));
|
|
918
981
|
}
|
|
@@ -921,9 +984,31 @@ function estimateItemSize(item) {
|
|
|
921
984
|
}
|
|
922
985
|
|
|
923
986
|
// src/expressions/key-condition.ts
|
|
987
|
+
function stripOuterParens(expression) {
|
|
988
|
+
let normalized = expression.trim();
|
|
989
|
+
while (normalized.startsWith("(") && normalized.endsWith(")")) {
|
|
990
|
+
let depth = 0;
|
|
991
|
+
let balanced = true;
|
|
992
|
+
for (let index = 0; index < normalized.length - 1; index++) {
|
|
993
|
+
if (normalized[index] === "(") depth++;
|
|
994
|
+
else if (normalized[index] === ")") depth--;
|
|
995
|
+
if (depth === 0) {
|
|
996
|
+
balanced = false;
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (!balanced) {
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
normalized = normalized.slice(1, -1).trim();
|
|
1004
|
+
}
|
|
1005
|
+
return normalized;
|
|
1006
|
+
}
|
|
924
1007
|
function parseKeyCondition(expression, keySchema, context) {
|
|
925
|
-
const
|
|
926
|
-
const
|
|
1008
|
+
const hashKeyNames = getHashKeys(keySchema);
|
|
1009
|
+
const rangeKeyNames = getRangeKeys(keySchema);
|
|
1010
|
+
const hashKeyName = hashKeyNames[0];
|
|
1011
|
+
const rangeKeyName = rangeKeyNames[0];
|
|
927
1012
|
const resolvedNames = context.expressionAttributeNames || {};
|
|
928
1013
|
const resolvedValues = context.expressionAttributeValues || {};
|
|
929
1014
|
function resolveName(name) {
|
|
@@ -946,39 +1031,18 @@ function parseKeyCondition(expression, keySchema, context) {
|
|
|
946
1031
|
}
|
|
947
1032
|
throw new ValidationException(`Invalid value reference: ${ref}`);
|
|
948
1033
|
}
|
|
949
|
-
function stripOuterParens(expr) {
|
|
950
|
-
let s = expr.trim();
|
|
951
|
-
while (s.startsWith("(") && s.endsWith(")")) {
|
|
952
|
-
let depth = 0;
|
|
953
|
-
let balanced = true;
|
|
954
|
-
for (let i = 0; i < s.length - 1; i++) {
|
|
955
|
-
if (s[i] === "(") depth++;
|
|
956
|
-
else if (s[i] === ")") depth--;
|
|
957
|
-
if (depth === 0) {
|
|
958
|
-
balanced = false;
|
|
959
|
-
break;
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
if (balanced) {
|
|
963
|
-
s = s.slice(1, -1).trim();
|
|
964
|
-
} else {
|
|
965
|
-
break;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
return s;
|
|
969
|
-
}
|
|
970
1034
|
const normalizedExpression = stripOuterParens(expression);
|
|
971
|
-
const parts = normalizedExpression
|
|
972
|
-
|
|
973
|
-
|
|
1035
|
+
const parts = flattenKeyConditions(normalizedExpression);
|
|
1036
|
+
const hashConditions = /* @__PURE__ */ new Map();
|
|
1037
|
+
const rangeConditions = /* @__PURE__ */ new Map();
|
|
974
1038
|
for (const part of parts) {
|
|
975
1039
|
const trimmed = stripOuterParens(part);
|
|
976
1040
|
const beginsWithMatch = trimmed.match(/^begins_with\s*\(\s*([#\w]+)\s*,\s*(:\w+)\s*\)$/i);
|
|
977
1041
|
if (beginsWithMatch) {
|
|
978
1042
|
const attrName = resolveName(beginsWithMatch[1]);
|
|
979
1043
|
const value = resolveValue(beginsWithMatch[2]);
|
|
980
|
-
if (attrName
|
|
981
|
-
|
|
1044
|
+
if (rangeKeyNames.includes(attrName)) {
|
|
1045
|
+
rangeConditions.set(attrName, { operator: "begins_with", value });
|
|
982
1046
|
} else {
|
|
983
1047
|
throw new ValidationException(`begins_with can only be used on sort key`);
|
|
984
1048
|
}
|
|
@@ -989,8 +1053,8 @@ function parseKeyCondition(expression, keySchema, context) {
|
|
|
989
1053
|
const attrName = resolveName(betweenMatch[1]);
|
|
990
1054
|
const value1 = resolveValue(betweenMatch[2]);
|
|
991
1055
|
const value2 = resolveValue(betweenMatch[3]);
|
|
992
|
-
if (attrName
|
|
993
|
-
|
|
1056
|
+
if (rangeKeyNames.includes(attrName)) {
|
|
1057
|
+
rangeConditions.set(attrName, { operator: "BETWEEN", value: value1, value2 });
|
|
994
1058
|
} else {
|
|
995
1059
|
throw new ValidationException(`BETWEEN can only be used on sort key`);
|
|
996
1060
|
}
|
|
@@ -1001,13 +1065,13 @@ function parseKeyCondition(expression, keySchema, context) {
|
|
|
1001
1065
|
const attrName = resolveName(comparisonMatch[1]);
|
|
1002
1066
|
const operator = comparisonMatch[2];
|
|
1003
1067
|
const value = resolveValue(comparisonMatch[3]);
|
|
1004
|
-
if (attrName
|
|
1068
|
+
if (hashKeyNames.includes(attrName)) {
|
|
1005
1069
|
if (operator !== "=") {
|
|
1006
|
-
throw new ValidationException(`
|
|
1070
|
+
throw new ValidationException(`Partition key condition must use = operator`);
|
|
1007
1071
|
}
|
|
1008
|
-
|
|
1009
|
-
} else if (attrName
|
|
1010
|
-
|
|
1072
|
+
hashConditions.set(attrName, value);
|
|
1073
|
+
} else if (rangeKeyNames.includes(attrName)) {
|
|
1074
|
+
rangeConditions.set(attrName, { operator, value });
|
|
1011
1075
|
} else {
|
|
1012
1076
|
throw new ValidationException(`Key condition references unknown attribute: ${attrName}`);
|
|
1013
1077
|
}
|
|
@@ -1015,30 +1079,120 @@ function parseKeyCondition(expression, keySchema, context) {
|
|
|
1015
1079
|
}
|
|
1016
1080
|
throw new ValidationException(`Invalid key condition expression: ${trimmed}`);
|
|
1017
1081
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1082
|
+
for (const key of hashKeyNames) {
|
|
1083
|
+
if (!hashConditions.has(key)) {
|
|
1084
|
+
throw new ValidationException(`Key condition must specify equality for partition key ${key}`);
|
|
1085
|
+
}
|
|
1020
1086
|
}
|
|
1087
|
+
let lastSortKeyIndex = -1;
|
|
1088
|
+
for (const [index, key] of rangeKeyNames.entries()) {
|
|
1089
|
+
const condition = rangeConditions.get(key);
|
|
1090
|
+
if (!condition) {
|
|
1091
|
+
if (lastSortKeyIndex !== -1) {
|
|
1092
|
+
throw new ValidationException(`Sort key conditions must reference a contiguous prefix of the key schema`);
|
|
1093
|
+
}
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
if (index > 0 && !rangeConditions.has(rangeKeyNames[index - 1])) {
|
|
1097
|
+
throw new ValidationException(`Sort key conditions must reference a contiguous prefix of the key schema`);
|
|
1098
|
+
}
|
|
1099
|
+
if (lastSortKeyIndex !== -1) {
|
|
1100
|
+
const previous = rangeConditions.get(rangeKeyNames[lastSortKeyIndex]);
|
|
1101
|
+
if (previous && previous.operator !== "=") {
|
|
1102
|
+
throw new ValidationException(`Only the last sort key condition can use a range operator`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
lastSortKeyIndex = index;
|
|
1106
|
+
}
|
|
1107
|
+
const orderedHashConditions = hashKeyNames.map((key) => ({
|
|
1108
|
+
key,
|
|
1109
|
+
value: hashConditions.get(key)
|
|
1110
|
+
}));
|
|
1111
|
+
const orderedRangeConditions = rangeKeyNames.filter((key) => rangeConditions.has(key)).map((key) => {
|
|
1112
|
+
const condition = rangeConditions.get(key);
|
|
1113
|
+
return {
|
|
1114
|
+
key,
|
|
1115
|
+
operator: condition.operator,
|
|
1116
|
+
value: condition.value,
|
|
1117
|
+
value2: condition.value2
|
|
1118
|
+
};
|
|
1119
|
+
});
|
|
1021
1120
|
return {
|
|
1022
1121
|
hashKey: hashKeyName,
|
|
1023
|
-
hashValue,
|
|
1122
|
+
hashValue: hashConditions.get(hashKeyName),
|
|
1024
1123
|
rangeKey: rangeKeyName,
|
|
1025
|
-
rangeCondition
|
|
1124
|
+
rangeCondition: orderedRangeConditions[0] ? {
|
|
1125
|
+
operator: orderedRangeConditions[0].operator,
|
|
1126
|
+
value: orderedRangeConditions[0].value,
|
|
1127
|
+
value2: orderedRangeConditions[0].value2
|
|
1128
|
+
} : void 0,
|
|
1129
|
+
hashConditions: orderedHashConditions,
|
|
1130
|
+
rangeConditions: orderedRangeConditions
|
|
1026
1131
|
};
|
|
1027
1132
|
}
|
|
1028
1133
|
function matchesKeyCondition(item, condition) {
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1134
|
+
for (const hashCondition of condition.hashConditions) {
|
|
1135
|
+
const itemHashValue = item[hashCondition.key];
|
|
1136
|
+
if (!itemHashValue || !attributeEquals(itemHashValue, hashCondition.value)) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1032
1139
|
}
|
|
1033
|
-
|
|
1034
|
-
const itemRangeValue = item[
|
|
1140
|
+
for (const rangeCondition of condition.rangeConditions) {
|
|
1141
|
+
const itemRangeValue = item[rangeCondition.key];
|
|
1035
1142
|
if (!itemRangeValue) {
|
|
1036
1143
|
return false;
|
|
1037
1144
|
}
|
|
1038
|
-
|
|
1145
|
+
if (!matchesRangeCondition(itemRangeValue, rangeCondition)) {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1039
1148
|
}
|
|
1040
1149
|
return true;
|
|
1041
1150
|
}
|
|
1151
|
+
function flattenKeyConditions(expression) {
|
|
1152
|
+
const normalized = stripOuterParens(expression);
|
|
1153
|
+
const parts = splitTopLevelAndConditions(normalized);
|
|
1154
|
+
if (parts.length === 1) {
|
|
1155
|
+
return [normalized];
|
|
1156
|
+
}
|
|
1157
|
+
return parts.flatMap((part) => flattenKeyConditions(part));
|
|
1158
|
+
}
|
|
1159
|
+
function splitTopLevelAndConditions(expression) {
|
|
1160
|
+
const parts = [];
|
|
1161
|
+
let depth = 0;
|
|
1162
|
+
let segmentStart = 0;
|
|
1163
|
+
let betweenPending = false;
|
|
1164
|
+
for (let index = 0; index < expression.length; index++) {
|
|
1165
|
+
const char = expression[index];
|
|
1166
|
+
if (char === "(") {
|
|
1167
|
+
depth++;
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
if (char === ")") {
|
|
1171
|
+
depth--;
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (depth !== 0) {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if (/^BETWEEN\b/i.test(expression.slice(index))) {
|
|
1178
|
+
betweenPending = true;
|
|
1179
|
+
index += "BETWEEN".length - 1;
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
if (/^\s+AND\s+/i.test(expression.slice(index))) {
|
|
1183
|
+
if (betweenPending) {
|
|
1184
|
+
betweenPending = false;
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
parts.push(expression.slice(segmentStart, index).trim());
|
|
1188
|
+
const andMatch = expression.slice(index).match(/^\s+AND\s+/i);
|
|
1189
|
+
index += andMatch[0].length - 1;
|
|
1190
|
+
segmentStart = index + 1;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
parts.push(expression.slice(segmentStart).trim());
|
|
1194
|
+
return parts.filter(Boolean);
|
|
1195
|
+
}
|
|
1042
1196
|
function matchesRangeCondition(value, condition) {
|
|
1043
1197
|
const cmp5 = compareValues2(value, condition.value);
|
|
1044
1198
|
switch (condition.operator) {
|
|
@@ -1659,9 +1813,10 @@ function query(store, input) {
|
|
|
1659
1813
|
let items;
|
|
1660
1814
|
let lastEvaluatedKey;
|
|
1661
1815
|
if (input.IndexName) {
|
|
1816
|
+
const hashValues = Object.fromEntries(keyCondition.hashConditions.map((condition) => [condition.key, condition.value]));
|
|
1662
1817
|
const result = table.queryIndex(
|
|
1663
1818
|
input.IndexName,
|
|
1664
|
-
|
|
1819
|
+
hashValues,
|
|
1665
1820
|
{
|
|
1666
1821
|
scanIndexForward: input.ScanIndexForward,
|
|
1667
1822
|
exclusiveStartKey: input.ExclusiveStartKey
|
|
@@ -1670,8 +1825,9 @@ function query(store, input) {
|
|
|
1670
1825
|
items = result.items;
|
|
1671
1826
|
lastEvaluatedKey = result.lastEvaluatedKey;
|
|
1672
1827
|
} else {
|
|
1828
|
+
const hashValues = Object.fromEntries(keyCondition.hashConditions.map((condition) => [condition.key, condition.value]));
|
|
1673
1829
|
const result = table.queryByHashKey(
|
|
1674
|
-
|
|
1830
|
+
hashValues,
|
|
1675
1831
|
{
|
|
1676
1832
|
scanIndexForward: input.ScanIndexForward,
|
|
1677
1833
|
exclusiveStartKey: input.ExclusiveStartKey
|
|
@@ -1696,24 +1852,11 @@ function query(store, input) {
|
|
|
1696
1852
|
items = items.slice(0, input.Limit);
|
|
1697
1853
|
if (items.length > 0) {
|
|
1698
1854
|
const lastItem = items[items.length - 1];
|
|
1699
|
-
lastEvaluatedKey =
|
|
1700
|
-
const hashKey = table.getHashKeyName();
|
|
1701
|
-
const rangeKey = table.getRangeKeyName();
|
|
1702
|
-
if (lastItem[hashKey]) {
|
|
1703
|
-
lastEvaluatedKey[hashKey] = lastItem[hashKey];
|
|
1704
|
-
}
|
|
1705
|
-
if (rangeKey && lastItem[rangeKey]) {
|
|
1706
|
-
lastEvaluatedKey[rangeKey] = lastItem[rangeKey];
|
|
1707
|
-
}
|
|
1855
|
+
lastEvaluatedKey = extractKey(lastItem, table.keySchema);
|
|
1708
1856
|
if (input.IndexName) {
|
|
1709
1857
|
const indexKeySchema = table.getIndexKeySchema(input.IndexName);
|
|
1710
1858
|
if (indexKeySchema) {
|
|
1711
|
-
|
|
1712
|
-
const attrValue = lastItem[key.AttributeName];
|
|
1713
|
-
if (attrValue) {
|
|
1714
|
-
lastEvaluatedKey[key.AttributeName] = attrValue;
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1859
|
+
lastEvaluatedKey = extractKey(lastItem, mergeKeySchemas(indexKeySchema, table.keySchema));
|
|
1717
1860
|
}
|
|
1718
1861
|
}
|
|
1719
1862
|
}
|
|
@@ -1780,24 +1923,11 @@ function scan(store, input) {
|
|
|
1780
1923
|
items = items.slice(0, input.Limit);
|
|
1781
1924
|
if (items.length > 0) {
|
|
1782
1925
|
const lastItem = items[items.length - 1];
|
|
1783
|
-
lastEvaluatedKey =
|
|
1784
|
-
const hashKey = table.getHashKeyName();
|
|
1785
|
-
const rangeKey = table.getRangeKeyName();
|
|
1786
|
-
if (lastItem[hashKey]) {
|
|
1787
|
-
lastEvaluatedKey[hashKey] = lastItem[hashKey];
|
|
1788
|
-
}
|
|
1789
|
-
if (rangeKey && lastItem[rangeKey]) {
|
|
1790
|
-
lastEvaluatedKey[rangeKey] = lastItem[rangeKey];
|
|
1791
|
-
}
|
|
1926
|
+
lastEvaluatedKey = extractKey(lastItem, table.keySchema);
|
|
1792
1927
|
if (input.IndexName) {
|
|
1793
1928
|
const indexKeySchema = table.getIndexKeySchema(input.IndexName);
|
|
1794
1929
|
if (indexKeySchema) {
|
|
1795
|
-
|
|
1796
|
-
const attrValue = lastItem[key.AttributeName];
|
|
1797
|
-
if (attrValue) {
|
|
1798
|
-
lastEvaluatedKey[key.AttributeName] = attrValue;
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1930
|
+
lastEvaluatedKey = extractKey(lastItem, mergeKeySchemas(indexKeySchema, table.keySchema));
|
|
1801
1931
|
}
|
|
1802
1932
|
}
|
|
1803
1933
|
}
|
|
@@ -2594,12 +2724,7 @@ var Table = class {
|
|
|
2594
2724
|
}
|
|
2595
2725
|
}
|
|
2596
2726
|
buildIndexKey(item, keySchema) {
|
|
2597
|
-
|
|
2598
|
-
if (!item[hashAttr]) {
|
|
2599
|
-
return null;
|
|
2600
|
-
}
|
|
2601
|
-
const rangeAttr = getRangeKey(keySchema);
|
|
2602
|
-
if (rangeAttr && !item[rangeAttr]) {
|
|
2727
|
+
if (!hasCompleteKey(item, keySchema)) {
|
|
2603
2728
|
return null;
|
|
2604
2729
|
}
|
|
2605
2730
|
return serializeKey(extractKey(item, keySchema), keySchema);
|
|
@@ -2645,47 +2770,7 @@ var Table = class {
|
|
|
2645
2770
|
};
|
|
2646
2771
|
}
|
|
2647
2772
|
queryByHashKey(hashValue, options) {
|
|
2648
|
-
|
|
2649
|
-
const rangeAttr = this.getRangeKeyName();
|
|
2650
|
-
const matchingItems = [];
|
|
2651
|
-
for (const item of this.items.values()) {
|
|
2652
|
-
const itemHashValue = item[hashAttr];
|
|
2653
|
-
const queryHashValue = hashValue[hashAttr];
|
|
2654
|
-
if (itemHashValue && queryHashValue && this.attributeEquals(itemHashValue, queryHashValue)) {
|
|
2655
|
-
matchingItems.push(deepClone(item));
|
|
2656
|
-
}
|
|
2657
|
-
}
|
|
2658
|
-
if (rangeAttr) {
|
|
2659
|
-
matchingItems.sort((a, b) => {
|
|
2660
|
-
const aVal = a[rangeAttr];
|
|
2661
|
-
const bVal = b[rangeAttr];
|
|
2662
|
-
if (!aVal && !bVal) return 0;
|
|
2663
|
-
if (!aVal) return 1;
|
|
2664
|
-
if (!bVal) return -1;
|
|
2665
|
-
const cmp5 = this.compareAttributes(aVal, bVal);
|
|
2666
|
-
return options?.scanIndexForward === false ? -cmp5 : cmp5;
|
|
2667
|
-
});
|
|
2668
|
-
}
|
|
2669
|
-
let startIdx = 0;
|
|
2670
|
-
if (options?.exclusiveStartKey) {
|
|
2671
|
-
const startKey = serializeKey(options.exclusiveStartKey, this.keySchema);
|
|
2672
|
-
startIdx = matchingItems.findIndex(
|
|
2673
|
-
(item) => serializeKey(extractKey(item, this.keySchema), this.keySchema) === startKey
|
|
2674
|
-
);
|
|
2675
|
-
if (startIdx !== -1) {
|
|
2676
|
-
startIdx++;
|
|
2677
|
-
} else {
|
|
2678
|
-
startIdx = 0;
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
const limit = options?.limit;
|
|
2682
|
-
const sliced = limit ? matchingItems.slice(startIdx, startIdx + limit) : matchingItems.slice(startIdx);
|
|
2683
|
-
const hasMore = limit ? startIdx + limit < matchingItems.length : false;
|
|
2684
|
-
const lastItem = sliced[sliced.length - 1];
|
|
2685
|
-
return {
|
|
2686
|
-
items: sliced,
|
|
2687
|
-
lastEvaluatedKey: hasMore && lastItem ? extractKey(lastItem, this.keySchema) : void 0
|
|
2688
|
-
};
|
|
2773
|
+
return this.queryBySchema(this.keySchema, hashValue, this.keySchema, false, options);
|
|
2689
2774
|
}
|
|
2690
2775
|
queryIndex(indexName, hashValue, options) {
|
|
2691
2776
|
const gsi = this.globalSecondaryIndexes.get(indexName);
|
|
@@ -2694,69 +2779,16 @@ var Table = class {
|
|
|
2694
2779
|
if (!indexData) {
|
|
2695
2780
|
throw new Error(`Index ${indexName} not found`);
|
|
2696
2781
|
}
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
continue;
|
|
2705
|
-
}
|
|
2706
|
-
if (indexRangeAttr && !item[indexRangeAttr]) {
|
|
2707
|
-
continue;
|
|
2708
|
-
}
|
|
2709
|
-
matchingItems.push(deepClone(item));
|
|
2710
|
-
}
|
|
2711
|
-
if (indexRangeAttr) {
|
|
2712
|
-
matchingItems.sort((a, b) => {
|
|
2713
|
-
const aVal = a[indexRangeAttr];
|
|
2714
|
-
const bVal = b[indexRangeAttr];
|
|
2715
|
-
if (!aVal && !bVal) return 0;
|
|
2716
|
-
if (!aVal) return 1;
|
|
2717
|
-
if (!bVal) return -1;
|
|
2718
|
-
const cmp5 = this.compareAttributes(aVal, bVal);
|
|
2719
|
-
return options?.scanIndexForward === false ? -cmp5 : cmp5;
|
|
2720
|
-
});
|
|
2721
|
-
}
|
|
2722
|
-
let startIdx = 0;
|
|
2723
|
-
if (options?.exclusiveStartKey) {
|
|
2724
|
-
const combinedKeySchema = [...indexData.keySchema];
|
|
2725
|
-
const tableRangeKey = this.getRangeKeyName();
|
|
2726
|
-
if (tableRangeKey && !combinedKeySchema.some((k) => k.AttributeName === tableRangeKey)) {
|
|
2727
|
-
combinedKeySchema.push({ AttributeName: tableRangeKey, KeyType: "RANGE" });
|
|
2728
|
-
}
|
|
2729
|
-
const tableHashKey = this.getHashKeyName();
|
|
2730
|
-
if (!combinedKeySchema.some((k) => k.AttributeName === tableHashKey)) {
|
|
2731
|
-
combinedKeySchema.push({ AttributeName: tableHashKey, KeyType: "HASH" });
|
|
2732
|
-
}
|
|
2733
|
-
const startKey = serializeKey(options.exclusiveStartKey, combinedKeySchema);
|
|
2734
|
-
startIdx = matchingItems.findIndex(
|
|
2735
|
-
(item) => serializeKey(extractKey(item, combinedKeySchema), combinedKeySchema) === startKey
|
|
2736
|
-
);
|
|
2737
|
-
if (startIdx !== -1) {
|
|
2738
|
-
startIdx++;
|
|
2739
|
-
} else {
|
|
2740
|
-
startIdx = 0;
|
|
2741
|
-
}
|
|
2742
|
-
}
|
|
2743
|
-
const limit = options?.limit;
|
|
2744
|
-
const sliced = limit ? matchingItems.slice(startIdx, startIdx + limit) : matchingItems.slice(startIdx);
|
|
2745
|
-
const hasMore = limit ? startIdx + limit < matchingItems.length : false;
|
|
2746
|
-
const lastItem = sliced[sliced.length - 1];
|
|
2747
|
-
let lastEvaluatedKey;
|
|
2748
|
-
if (hasMore && lastItem) {
|
|
2749
|
-
lastEvaluatedKey = extractKey(lastItem, this.keySchema);
|
|
2750
|
-
for (const keyElement of indexData.keySchema) {
|
|
2751
|
-
const attrValue = lastItem[keyElement.AttributeName];
|
|
2752
|
-
if (attrValue) {
|
|
2753
|
-
lastEvaluatedKey[keyElement.AttributeName] = attrValue;
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
}
|
|
2782
|
+
const result = this.queryBySchema(
|
|
2783
|
+
indexData.keySchema,
|
|
2784
|
+
hashValue,
|
|
2785
|
+
mergeKeySchemas(indexData.keySchema, this.keySchema),
|
|
2786
|
+
true,
|
|
2787
|
+
options
|
|
2788
|
+
);
|
|
2757
2789
|
return {
|
|
2758
|
-
items:
|
|
2759
|
-
lastEvaluatedKey,
|
|
2790
|
+
items: result.items,
|
|
2791
|
+
lastEvaluatedKey: result.lastEvaluatedKey,
|
|
2760
2792
|
indexKeySchema: indexData.keySchema
|
|
2761
2793
|
};
|
|
2762
2794
|
}
|
|
@@ -2767,18 +2799,19 @@ var Table = class {
|
|
|
2767
2799
|
if (!indexData) {
|
|
2768
2800
|
throw new Error(`Index ${indexName} not found`);
|
|
2769
2801
|
}
|
|
2770
|
-
const indexHashAttr = getHashKey(indexData.keySchema);
|
|
2771
2802
|
const matchingItems = [];
|
|
2772
2803
|
for (const item of this.items.values()) {
|
|
2773
|
-
if (item
|
|
2804
|
+
if (hasCompleteKey(item, indexData.keySchema)) {
|
|
2774
2805
|
matchingItems.push(deepClone(item));
|
|
2775
2806
|
}
|
|
2776
2807
|
}
|
|
2808
|
+
matchingItems.sort((a, b) => this.compareByKeySchema(a, b, indexData.keySchema, true));
|
|
2777
2809
|
let startIdx = 0;
|
|
2778
2810
|
if (exclusiveStartKey) {
|
|
2779
|
-
const
|
|
2811
|
+
const paginationKeySchema = mergeKeySchemas(indexData.keySchema, this.keySchema);
|
|
2812
|
+
const startKey = serializeKey(exclusiveStartKey, paginationKeySchema);
|
|
2780
2813
|
startIdx = matchingItems.findIndex(
|
|
2781
|
-
(item) => serializeKey(extractKey(item,
|
|
2814
|
+
(item) => serializeKey(extractKey(item, paginationKeySchema), paginationKeySchema) === startKey
|
|
2782
2815
|
);
|
|
2783
2816
|
if (startIdx !== -1) {
|
|
2784
2817
|
startIdx++;
|
|
@@ -2791,13 +2824,7 @@ var Table = class {
|
|
|
2791
2824
|
const lastItem = sliced[sliced.length - 1];
|
|
2792
2825
|
let lastEvaluatedKey;
|
|
2793
2826
|
if (hasMore && lastItem) {
|
|
2794
|
-
lastEvaluatedKey = extractKey(lastItem, this.keySchema);
|
|
2795
|
-
for (const keyElement of indexData.keySchema) {
|
|
2796
|
-
const attrValue = lastItem[keyElement.AttributeName];
|
|
2797
|
-
if (attrValue) {
|
|
2798
|
-
lastEvaluatedKey[keyElement.AttributeName] = attrValue;
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2827
|
+
lastEvaluatedKey = extractKey(lastItem, mergeKeySchemas(indexData.keySchema, this.keySchema));
|
|
2801
2828
|
}
|
|
2802
2829
|
return {
|
|
2803
2830
|
items: sliced,
|
|
@@ -2823,6 +2850,67 @@ var Table = class {
|
|
|
2823
2850
|
if ("B" in a && "B" in b) return a.B.localeCompare(b.B);
|
|
2824
2851
|
return 0;
|
|
2825
2852
|
}
|
|
2853
|
+
compareByKeySchema(a, b, keySchema, scanIndexForward = true) {
|
|
2854
|
+
for (const attributeName of [...getHashKeys(keySchema), ...getRangeKeys(keySchema)]) {
|
|
2855
|
+
const aVal = a[attributeName];
|
|
2856
|
+
const bVal = b[attributeName];
|
|
2857
|
+
if (!aVal && !bVal) {
|
|
2858
|
+
continue;
|
|
2859
|
+
}
|
|
2860
|
+
if (!aVal) {
|
|
2861
|
+
return 1;
|
|
2862
|
+
}
|
|
2863
|
+
if (!bVal) {
|
|
2864
|
+
return -1;
|
|
2865
|
+
}
|
|
2866
|
+
const cmp5 = this.compareAttributes(aVal, bVal);
|
|
2867
|
+
if (cmp5 !== 0) {
|
|
2868
|
+
return scanIndexForward ? cmp5 : -cmp5;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
return 0;
|
|
2872
|
+
}
|
|
2873
|
+
queryBySchema(keySchema, hashValue, paginationKeySchema, requireCompleteKey, options) {
|
|
2874
|
+
const hashAttrs = getHashKeys(keySchema);
|
|
2875
|
+
const rangeAttrs = getRangeKeys(keySchema);
|
|
2876
|
+
const matchingItems = [];
|
|
2877
|
+
for (const item of this.items.values()) {
|
|
2878
|
+
if (requireCompleteKey && !hasCompleteKey(item, keySchema)) {
|
|
2879
|
+
continue;
|
|
2880
|
+
}
|
|
2881
|
+
const matchesPartitionKey = hashAttrs.every((attr) => {
|
|
2882
|
+
const itemHashValue = item[attr];
|
|
2883
|
+
const queryHashValue = hashValue[attr];
|
|
2884
|
+
return itemHashValue && queryHashValue && this.attributeEquals(itemHashValue, queryHashValue);
|
|
2885
|
+
});
|
|
2886
|
+
if (matchesPartitionKey) {
|
|
2887
|
+
matchingItems.push(deepClone(item));
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
if (rangeAttrs.length > 0) {
|
|
2891
|
+
matchingItems.sort((a, b) => this.compareByKeySchema(a, b, keySchema, options?.scanIndexForward !== false));
|
|
2892
|
+
}
|
|
2893
|
+
let startIdx = 0;
|
|
2894
|
+
if (options?.exclusiveStartKey) {
|
|
2895
|
+
const startKey = serializeKey(options.exclusiveStartKey, paginationKeySchema);
|
|
2896
|
+
startIdx = matchingItems.findIndex(
|
|
2897
|
+
(item) => serializeKey(extractKey(item, paginationKeySchema), paginationKeySchema) === startKey
|
|
2898
|
+
);
|
|
2899
|
+
if (startIdx !== -1) {
|
|
2900
|
+
startIdx++;
|
|
2901
|
+
} else {
|
|
2902
|
+
startIdx = 0;
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
const limit = options?.limit;
|
|
2906
|
+
const sliced = limit ? matchingItems.slice(startIdx, startIdx + limit) : matchingItems.slice(startIdx);
|
|
2907
|
+
const hasMore = limit ? startIdx + limit < matchingItems.length : false;
|
|
2908
|
+
const lastItem = sliced[sliced.length - 1];
|
|
2909
|
+
return {
|
|
2910
|
+
items: sliced,
|
|
2911
|
+
lastEvaluatedKey: hasMore && lastItem ? extractKey(lastItem, paginationKeySchema) : void 0
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2826
2914
|
getAllItems() {
|
|
2827
2915
|
return Array.from(this.items.values()).map((item) => deepClone(item));
|
|
2828
2916
|
}
|