@awsless/dynamodb-server 0.1.5 → 0.1.6

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/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
  }
@@ -922,8 +985,10 @@ function estimateItemSize(item) {
922
985
 
923
986
  // src/expressions/key-condition.ts
924
987
  function parseKeyCondition(expression, keySchema, context) {
925
- const hashKeyName = getHashKey(keySchema);
926
- const rangeKeyName = getRangeKey(keySchema);
988
+ const hashKeyNames = getHashKeys(keySchema);
989
+ const rangeKeyNames = getRangeKeys(keySchema);
990
+ const hashKeyName = hashKeyNames[0];
991
+ const rangeKeyName = rangeKeyNames[0];
927
992
  const resolvedNames = context.expressionAttributeNames || {};
928
993
  const resolvedValues = context.expressionAttributeValues || {};
929
994
  function resolveName(name) {
@@ -968,17 +1033,17 @@ function parseKeyCondition(expression, keySchema, context) {
968
1033
  return s;
969
1034
  }
970
1035
  const normalizedExpression = stripOuterParens(expression);
971
- const parts = normalizedExpression.split(/\s+AND\s+/i);
972
- let hashValue;
973
- let rangeCondition;
1036
+ const parts = splitKeyConditions(normalizedExpression);
1037
+ const hashConditions = /* @__PURE__ */ new Map();
1038
+ const rangeConditions = /* @__PURE__ */ new Map();
974
1039
  for (const part of parts) {
975
1040
  const trimmed = stripOuterParens(part);
976
1041
  const beginsWithMatch = trimmed.match(/^begins_with\s*\(\s*([#\w]+)\s*,\s*(:\w+)\s*\)$/i);
977
1042
  if (beginsWithMatch) {
978
1043
  const attrName = resolveName(beginsWithMatch[1]);
979
1044
  const value = resolveValue(beginsWithMatch[2]);
980
- if (attrName === rangeKeyName) {
981
- rangeCondition = { operator: "begins_with", value };
1045
+ if (rangeKeyNames.includes(attrName)) {
1046
+ rangeConditions.set(attrName, { operator: "begins_with", value });
982
1047
  } else {
983
1048
  throw new ValidationException(`begins_with can only be used on sort key`);
984
1049
  }
@@ -989,8 +1054,8 @@ function parseKeyCondition(expression, keySchema, context) {
989
1054
  const attrName = resolveName(betweenMatch[1]);
990
1055
  const value1 = resolveValue(betweenMatch[2]);
991
1056
  const value2 = resolveValue(betweenMatch[3]);
992
- if (attrName === rangeKeyName) {
993
- rangeCondition = { operator: "BETWEEN", value: value1, value2 };
1057
+ if (rangeKeyNames.includes(attrName)) {
1058
+ rangeConditions.set(attrName, { operator: "BETWEEN", value: value1, value2 });
994
1059
  } else {
995
1060
  throw new ValidationException(`BETWEEN can only be used on sort key`);
996
1061
  }
@@ -1001,13 +1066,13 @@ function parseKeyCondition(expression, keySchema, context) {
1001
1066
  const attrName = resolveName(comparisonMatch[1]);
1002
1067
  const operator = comparisonMatch[2];
1003
1068
  const value = resolveValue(comparisonMatch[3]);
1004
- if (attrName === hashKeyName) {
1069
+ if (hashKeyNames.includes(attrName)) {
1005
1070
  if (operator !== "=") {
1006
- throw new ValidationException(`Hash key condition must use = operator`);
1071
+ throw new ValidationException(`Partition key condition must use = operator`);
1007
1072
  }
1008
- hashValue = value;
1009
- } else if (attrName === rangeKeyName) {
1010
- rangeCondition = { operator, value };
1073
+ hashConditions.set(attrName, value);
1074
+ } else if (rangeKeyNames.includes(attrName)) {
1075
+ rangeConditions.set(attrName, { operator, value });
1011
1076
  } else {
1012
1077
  throw new ValidationException(`Key condition references unknown attribute: ${attrName}`);
1013
1078
  }
@@ -1015,30 +1080,112 @@ function parseKeyCondition(expression, keySchema, context) {
1015
1080
  }
1016
1081
  throw new ValidationException(`Invalid key condition expression: ${trimmed}`);
1017
1082
  }
1018
- if (!hashValue) {
1019
- throw new ValidationException(`Key condition must specify hash key equality`);
1083
+ for (const key of hashKeyNames) {
1084
+ if (!hashConditions.has(key)) {
1085
+ throw new ValidationException(`Key condition must specify equality for partition key ${key}`);
1086
+ }
1020
1087
  }
1088
+ let lastSortKeyIndex = -1;
1089
+ for (const [index, key] of rangeKeyNames.entries()) {
1090
+ const condition = rangeConditions.get(key);
1091
+ if (!condition) {
1092
+ if (lastSortKeyIndex !== -1) {
1093
+ throw new ValidationException(`Sort key conditions must reference a contiguous prefix of the key schema`);
1094
+ }
1095
+ continue;
1096
+ }
1097
+ if (index > 0 && !rangeConditions.has(rangeKeyNames[index - 1])) {
1098
+ throw new ValidationException(`Sort key conditions must reference a contiguous prefix of the key schema`);
1099
+ }
1100
+ if (lastSortKeyIndex !== -1) {
1101
+ const previous = rangeConditions.get(rangeKeyNames[lastSortKeyIndex]);
1102
+ if (previous && previous.operator !== "=") {
1103
+ throw new ValidationException(`Only the last sort key condition can use a range operator`);
1104
+ }
1105
+ }
1106
+ lastSortKeyIndex = index;
1107
+ }
1108
+ const orderedHashConditions = hashKeyNames.map((key) => ({
1109
+ key,
1110
+ value: hashConditions.get(key)
1111
+ }));
1112
+ const orderedRangeConditions = rangeKeyNames.filter((key) => rangeConditions.has(key)).map((key) => {
1113
+ const condition = rangeConditions.get(key);
1114
+ return {
1115
+ key,
1116
+ operator: condition.operator,
1117
+ value: condition.value,
1118
+ value2: condition.value2
1119
+ };
1120
+ });
1021
1121
  return {
1022
1122
  hashKey: hashKeyName,
1023
- hashValue,
1123
+ hashValue: hashConditions.get(hashKeyName),
1024
1124
  rangeKey: rangeKeyName,
1025
- rangeCondition
1125
+ rangeCondition: orderedRangeConditions[0] ? {
1126
+ operator: orderedRangeConditions[0].operator,
1127
+ value: orderedRangeConditions[0].value,
1128
+ value2: orderedRangeConditions[0].value2
1129
+ } : void 0,
1130
+ hashConditions: orderedHashConditions,
1131
+ rangeConditions: orderedRangeConditions
1026
1132
  };
1027
1133
  }
1028
1134
  function matchesKeyCondition(item, condition) {
1029
- const itemHashValue = item[condition.hashKey];
1030
- if (!itemHashValue || !attributeEquals(itemHashValue, condition.hashValue)) {
1031
- return false;
1135
+ for (const hashCondition of condition.hashConditions) {
1136
+ const itemHashValue = item[hashCondition.key];
1137
+ if (!itemHashValue || !attributeEquals(itemHashValue, hashCondition.value)) {
1138
+ return false;
1139
+ }
1032
1140
  }
1033
- if (condition.rangeCondition && condition.rangeKey) {
1034
- const itemRangeValue = item[condition.rangeKey];
1141
+ for (const rangeCondition of condition.rangeConditions) {
1142
+ const itemRangeValue = item[rangeCondition.key];
1035
1143
  if (!itemRangeValue) {
1036
1144
  return false;
1037
1145
  }
1038
- return matchesRangeCondition(itemRangeValue, condition.rangeCondition);
1146
+ if (!matchesRangeCondition(itemRangeValue, rangeCondition)) {
1147
+ return false;
1148
+ }
1039
1149
  }
1040
1150
  return true;
1041
1151
  }
1152
+ function splitKeyConditions(expression) {
1153
+ const parts = [];
1154
+ let depth = 0;
1155
+ let segmentStart = 0;
1156
+ let betweenPending = false;
1157
+ for (let index = 0; index < expression.length; index++) {
1158
+ const char = expression[index];
1159
+ if (char === "(") {
1160
+ depth++;
1161
+ continue;
1162
+ }
1163
+ if (char === ")") {
1164
+ depth--;
1165
+ continue;
1166
+ }
1167
+ if (depth !== 0) {
1168
+ continue;
1169
+ }
1170
+ if (/^BETWEEN\b/i.test(expression.slice(index))) {
1171
+ betweenPending = true;
1172
+ index += "BETWEEN".length - 1;
1173
+ continue;
1174
+ }
1175
+ if (/^\s+AND\s+/i.test(expression.slice(index))) {
1176
+ if (betweenPending) {
1177
+ betweenPending = false;
1178
+ continue;
1179
+ }
1180
+ parts.push(expression.slice(segmentStart, index).trim());
1181
+ const andMatch = expression.slice(index).match(/^\s+AND\s+/i);
1182
+ index += andMatch[0].length - 1;
1183
+ segmentStart = index + 1;
1184
+ }
1185
+ }
1186
+ parts.push(expression.slice(segmentStart).trim());
1187
+ return parts.filter(Boolean);
1188
+ }
1042
1189
  function matchesRangeCondition(value, condition) {
1043
1190
  const cmp5 = compareValues2(value, condition.value);
1044
1191
  switch (condition.operator) {
@@ -1659,9 +1806,10 @@ function query(store, input) {
1659
1806
  let items;
1660
1807
  let lastEvaluatedKey;
1661
1808
  if (input.IndexName) {
1809
+ const hashValues = Object.fromEntries(keyCondition.hashConditions.map((condition) => [condition.key, condition.value]));
1662
1810
  const result = table.queryIndex(
1663
1811
  input.IndexName,
1664
- { [keyCondition.hashKey]: keyCondition.hashValue },
1812
+ hashValues,
1665
1813
  {
1666
1814
  scanIndexForward: input.ScanIndexForward,
1667
1815
  exclusiveStartKey: input.ExclusiveStartKey
@@ -1670,8 +1818,9 @@ function query(store, input) {
1670
1818
  items = result.items;
1671
1819
  lastEvaluatedKey = result.lastEvaluatedKey;
1672
1820
  } else {
1821
+ const hashValues = Object.fromEntries(keyCondition.hashConditions.map((condition) => [condition.key, condition.value]));
1673
1822
  const result = table.queryByHashKey(
1674
- { [keyCondition.hashKey]: keyCondition.hashValue },
1823
+ hashValues,
1675
1824
  {
1676
1825
  scanIndexForward: input.ScanIndexForward,
1677
1826
  exclusiveStartKey: input.ExclusiveStartKey
@@ -1696,24 +1845,11 @@ function query(store, input) {
1696
1845
  items = items.slice(0, input.Limit);
1697
1846
  if (items.length > 0) {
1698
1847
  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
- }
1848
+ lastEvaluatedKey = extractKey(lastItem, table.keySchema);
1708
1849
  if (input.IndexName) {
1709
1850
  const indexKeySchema = table.getIndexKeySchema(input.IndexName);
1710
1851
  if (indexKeySchema) {
1711
- for (const key of indexKeySchema) {
1712
- const attrValue = lastItem[key.AttributeName];
1713
- if (attrValue) {
1714
- lastEvaluatedKey[key.AttributeName] = attrValue;
1715
- }
1716
- }
1852
+ lastEvaluatedKey = extractKey(lastItem, mergeKeySchemas(indexKeySchema, table.keySchema));
1717
1853
  }
1718
1854
  }
1719
1855
  }
@@ -1780,24 +1916,11 @@ function scan(store, input) {
1780
1916
  items = items.slice(0, input.Limit);
1781
1917
  if (items.length > 0) {
1782
1918
  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
- }
1919
+ lastEvaluatedKey = extractKey(lastItem, table.keySchema);
1792
1920
  if (input.IndexName) {
1793
1921
  const indexKeySchema = table.getIndexKeySchema(input.IndexName);
1794
1922
  if (indexKeySchema) {
1795
- for (const key of indexKeySchema) {
1796
- const attrValue = lastItem[key.AttributeName];
1797
- if (attrValue) {
1798
- lastEvaluatedKey[key.AttributeName] = attrValue;
1799
- }
1800
- }
1923
+ lastEvaluatedKey = extractKey(lastItem, mergeKeySchemas(indexKeySchema, table.keySchema));
1801
1924
  }
1802
1925
  }
1803
1926
  }
@@ -2594,12 +2717,7 @@ var Table = class {
2594
2717
  }
2595
2718
  }
2596
2719
  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]) {
2720
+ if (!hasCompleteKey(item, keySchema)) {
2603
2721
  return null;
2604
2722
  }
2605
2723
  return serializeKey(extractKey(item, keySchema), keySchema);
@@ -2645,47 +2763,7 @@ var Table = class {
2645
2763
  };
2646
2764
  }
2647
2765
  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
- };
2766
+ return this.queryBySchema(this.keySchema, hashValue, this.keySchema, false, options);
2689
2767
  }
2690
2768
  queryIndex(indexName, hashValue, options) {
2691
2769
  const gsi = this.globalSecondaryIndexes.get(indexName);
@@ -2694,69 +2772,16 @@ var Table = class {
2694
2772
  if (!indexData) {
2695
2773
  throw new Error(`Index ${indexName} not found`);
2696
2774
  }
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
- }
2775
+ const result = this.queryBySchema(
2776
+ indexData.keySchema,
2777
+ hashValue,
2778
+ mergeKeySchemas(indexData.keySchema, this.keySchema),
2779
+ true,
2780
+ options
2781
+ );
2757
2782
  return {
2758
- items: sliced,
2759
- lastEvaluatedKey,
2783
+ items: result.items,
2784
+ lastEvaluatedKey: result.lastEvaluatedKey,
2760
2785
  indexKeySchema: indexData.keySchema
2761
2786
  };
2762
2787
  }
@@ -2767,18 +2792,19 @@ var Table = class {
2767
2792
  if (!indexData) {
2768
2793
  throw new Error(`Index ${indexName} not found`);
2769
2794
  }
2770
- const indexHashAttr = getHashKey(indexData.keySchema);
2771
2795
  const matchingItems = [];
2772
2796
  for (const item of this.items.values()) {
2773
- if (item[indexHashAttr]) {
2797
+ if (hasCompleteKey(item, indexData.keySchema)) {
2774
2798
  matchingItems.push(deepClone(item));
2775
2799
  }
2776
2800
  }
2801
+ matchingItems.sort((a, b) => this.compareByKeySchema(a, b, indexData.keySchema, true));
2777
2802
  let startIdx = 0;
2778
2803
  if (exclusiveStartKey) {
2779
- const startKey = serializeKey(exclusiveStartKey, this.keySchema);
2804
+ const paginationKeySchema = mergeKeySchemas(indexData.keySchema, this.keySchema);
2805
+ const startKey = serializeKey(exclusiveStartKey, paginationKeySchema);
2780
2806
  startIdx = matchingItems.findIndex(
2781
- (item) => serializeKey(extractKey(item, this.keySchema), this.keySchema) === startKey
2807
+ (item) => serializeKey(extractKey(item, paginationKeySchema), paginationKeySchema) === startKey
2782
2808
  );
2783
2809
  if (startIdx !== -1) {
2784
2810
  startIdx++;
@@ -2791,13 +2817,7 @@ var Table = class {
2791
2817
  const lastItem = sliced[sliced.length - 1];
2792
2818
  let lastEvaluatedKey;
2793
2819
  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
- }
2820
+ lastEvaluatedKey = extractKey(lastItem, mergeKeySchemas(indexData.keySchema, this.keySchema));
2801
2821
  }
2802
2822
  return {
2803
2823
  items: sliced,
@@ -2823,6 +2843,67 @@ var Table = class {
2823
2843
  if ("B" in a && "B" in b) return a.B.localeCompare(b.B);
2824
2844
  return 0;
2825
2845
  }
2846
+ compareByKeySchema(a, b, keySchema, scanIndexForward = true) {
2847
+ for (const attributeName of [...getHashKeys(keySchema), ...getRangeKeys(keySchema)]) {
2848
+ const aVal = a[attributeName];
2849
+ const bVal = b[attributeName];
2850
+ if (!aVal && !bVal) {
2851
+ continue;
2852
+ }
2853
+ if (!aVal) {
2854
+ return 1;
2855
+ }
2856
+ if (!bVal) {
2857
+ return -1;
2858
+ }
2859
+ const cmp5 = this.compareAttributes(aVal, bVal);
2860
+ if (cmp5 !== 0) {
2861
+ return scanIndexForward ? cmp5 : -cmp5;
2862
+ }
2863
+ }
2864
+ return 0;
2865
+ }
2866
+ queryBySchema(keySchema, hashValue, paginationKeySchema, requireCompleteKey, options) {
2867
+ const hashAttrs = getHashKeys(keySchema);
2868
+ const rangeAttrs = getRangeKeys(keySchema);
2869
+ const matchingItems = [];
2870
+ for (const item of this.items.values()) {
2871
+ if (requireCompleteKey && !hasCompleteKey(item, keySchema)) {
2872
+ continue;
2873
+ }
2874
+ const matchesPartitionKey = hashAttrs.every((attr) => {
2875
+ const itemHashValue = item[attr];
2876
+ const queryHashValue = hashValue[attr];
2877
+ return itemHashValue && queryHashValue && this.attributeEquals(itemHashValue, queryHashValue);
2878
+ });
2879
+ if (matchesPartitionKey) {
2880
+ matchingItems.push(deepClone(item));
2881
+ }
2882
+ }
2883
+ if (rangeAttrs.length > 0) {
2884
+ matchingItems.sort((a, b) => this.compareByKeySchema(a, b, keySchema, options?.scanIndexForward !== false));
2885
+ }
2886
+ let startIdx = 0;
2887
+ if (options?.exclusiveStartKey) {
2888
+ const startKey = serializeKey(options.exclusiveStartKey, paginationKeySchema);
2889
+ startIdx = matchingItems.findIndex(
2890
+ (item) => serializeKey(extractKey(item, paginationKeySchema), paginationKeySchema) === startKey
2891
+ );
2892
+ if (startIdx !== -1) {
2893
+ startIdx++;
2894
+ } else {
2895
+ startIdx = 0;
2896
+ }
2897
+ }
2898
+ const limit = options?.limit;
2899
+ const sliced = limit ? matchingItems.slice(startIdx, startIdx + limit) : matchingItems.slice(startIdx);
2900
+ const hasMore = limit ? startIdx + limit < matchingItems.length : false;
2901
+ const lastItem = sliced[sliced.length - 1];
2902
+ return {
2903
+ items: sliced,
2904
+ lastEvaluatedKey: hasMore && lastItem ? extractKey(lastItem, paginationKeySchema) : void 0
2905
+ };
2906
+ }
2826
2907
  getAllItems() {
2827
2908
  return Array.from(this.items.values()).map((item) => deepClone(item));
2828
2909
  }
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.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",