@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 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 "@awsless/dynamodb-server"
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: "java" })
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: "memory" })
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: "java" })
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
- // Engine type: "memory" (default) or "java"
68
- engine: "memory",
68
+ // Engine type: "memory" (default) or "java"
69
+ engine: 'memory',
69
70
 
70
- // Hostname to bind to (default: "localhost")
71
- hostname: "localhost",
71
+ // Hostname to bind to (default: "localhost")
72
+ hostname: 'localhost',
72
73
 
73
- // Port to listen on (default: auto-assigned)
74
- port: 8000,
74
+ // Port to listen on (default: auto-assigned)
75
+ port: 8000,
75
76
 
76
- // AWS region (default: "us-east-1")
77
- region: "us-east-1",
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("my-table", record => {
87
- console.log("Stream event:", record.eventName) // INSERT, MODIFY, or REMOVE
88
- console.log("Keys:", record.dynamodb.Keys)
89
- console.log("New Image:", record.dynamodb.NewImage)
90
- console.log("Old Image:", record.dynamodb.OldImage)
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 "@awsless/dynamodb-server"
101
- import { CreateTableCommand } from "@aws-sdk/client-dynamodb"
102
- import { PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb"
103
-
104
- describe("My DynamoDB Tests", () => {
105
- const server = new DynamoDBServer()
106
-
107
- beforeAll(async () => {
108
- await server.listen()
109
-
110
- // Create your tables
111
- await server.getClient().send(
112
- new CreateTableCommand({
113
- TableName: "users",
114
- KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
115
- AttributeDefinitions: [{ AttributeName: "id", AttributeType: "N" }],
116
- BillingMode: "PAY_PER_REQUEST",
117
- })
118
- )
119
- })
120
-
121
- afterAll(async () => {
122
- await server.stop()
123
- })
124
-
125
- it("should store and retrieve items", async () => {
126
- const client = server.getDocumentClient()
127
-
128
- await client.send(
129
- new PutCommand({
130
- TableName: "users",
131
- Item: { id: 1, name: "John" },
132
- })
133
- )
134
-
135
- const result = await client.send(
136
- new GetCommand({
137
- TableName: "users",
138
- Key: { id: 1 },
139
- })
140
- )
141
-
142
- expect(result.Item).toEqual({ id: 1, name: "John" })
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 hashKeyName = getHashKey(keySchema);
926
- const rangeKeyName = getRangeKey(keySchema);
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.split(/\s+AND\s+/i);
972
- let hashValue;
973
- let rangeCondition;
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 === rangeKeyName) {
981
- rangeCondition = { operator: "begins_with", value };
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 === rangeKeyName) {
993
- rangeCondition = { operator: "BETWEEN", value: value1, value2 };
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 === hashKeyName) {
1068
+ if (hashKeyNames.includes(attrName)) {
1005
1069
  if (operator !== "=") {
1006
- throw new ValidationException(`Hash key condition must use = operator`);
1070
+ throw new ValidationException(`Partition key condition must use = operator`);
1007
1071
  }
1008
- hashValue = value;
1009
- } else if (attrName === rangeKeyName) {
1010
- rangeCondition = { operator, value };
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
- if (!hashValue) {
1019
- throw new ValidationException(`Key condition must specify hash key equality`);
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 itemHashValue = item[condition.hashKey];
1030
- if (!itemHashValue || !attributeEquals(itemHashValue, condition.hashValue)) {
1031
- return false;
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
- if (condition.rangeCondition && condition.rangeKey) {
1034
- const itemRangeValue = item[condition.rangeKey];
1140
+ for (const rangeCondition of condition.rangeConditions) {
1141
+ const itemRangeValue = item[rangeCondition.key];
1035
1142
  if (!itemRangeValue) {
1036
1143
  return false;
1037
1144
  }
1038
- return matchesRangeCondition(itemRangeValue, condition.rangeCondition);
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
- { [keyCondition.hashKey]: keyCondition.hashValue },
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
- { [keyCondition.hashKey]: keyCondition.hashValue },
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
- for (const key of indexKeySchema) {
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
- for (const key of indexKeySchema) {
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
- const hashAttr = getHashKey(keySchema);
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
- const hashAttr = this.getHashKeyName();
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 indexHashAttr = getHashKey(indexData.keySchema);
2698
- const indexRangeAttr = getRangeKey(indexData.keySchema);
2699
- const matchingItems = [];
2700
- for (const item of this.items.values()) {
2701
- const itemHashValue = item[indexHashAttr];
2702
- const queryHashValue = hashValue[indexHashAttr];
2703
- if (!itemHashValue || !queryHashValue || !this.attributeEquals(itemHashValue, queryHashValue)) {
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: sliced,
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[indexHashAttr]) {
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 startKey = serializeKey(exclusiveStartKey, this.keySchema);
2811
+ const paginationKeySchema = mergeKeySchemas(indexData.keySchema, this.keySchema);
2812
+ const startKey = serializeKey(exclusiveStartKey, paginationKeySchema);
2780
2813
  startIdx = matchingItems.findIndex(
2781
- (item) => serializeKey(extractKey(item, this.keySchema), this.keySchema) === startKey
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awsless/dynamodb-server",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",