@awsless/dynamodb-server 0.1.4 → 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) {
@@ -598,6 +641,25 @@ function evaluateCondition(expression, item, context) {
598
641
  }
599
642
  throw new ValidationException(`Unexpected token: ${token.type}`);
600
643
  }
644
+ function parseSizeValue() {
645
+ consume("FUNCTION");
646
+ consume("LPAREN");
647
+ const pathToken = consume("PATH");
648
+ consume("RPAREN");
649
+ const segments = parsePath(pathToken.value, context.expressionAttributeNames);
650
+ const value = getValueAtPath(item, segments);
651
+ let size = 0;
652
+ if (value) {
653
+ if ("S" in value) size = value.S.length;
654
+ else if ("B" in value) size = value.B.length;
655
+ else if ("SS" in value) size = value.SS.length;
656
+ else if ("NS" in value) size = value.NS.length;
657
+ else if ("BS" in value) size = value.BS.length;
658
+ else if ("L" in value) size = value.L.length;
659
+ else if ("M" in value) size = Object.keys(value.M).length;
660
+ }
661
+ return size;
662
+ }
601
663
  function parseFunction() {
602
664
  const funcToken = consume("FUNCTION");
603
665
  consume("LPAREN");
@@ -707,12 +769,18 @@ function evaluateCondition(expression, item, context) {
707
769
  const nextToken = current();
708
770
  if (nextToken?.type === "COMPARATOR") {
709
771
  consume("COMPARATOR");
710
- const rightToken = consume("VALUE");
711
- const rightValue = resolveValue(rightToken.value);
712
- if (!rightValue || !("N" in rightValue)) {
713
- throw new ValidationException("Size comparison requires numeric operand");
772
+ let rightNum;
773
+ if (current()?.type === "FUNCTION" && current()?.value === "size") {
774
+ rightNum = parseSizeValue();
775
+ } else {
776
+ const rightToken = consume();
777
+ const rightValue = resolveOperand2(rightToken);
778
+ if (!rightValue || !("N" in rightValue)) {
779
+ throw new ValidationException("Size comparison requires numeric operand");
780
+ }
781
+ rightNum = Number(rightValue.N);
714
782
  }
715
- return compareNumbers(size, Number(rightValue.N), nextToken.value);
783
+ return compareNumbers(size, rightNum, nextToken.value);
716
784
  }
717
785
  return size > 0;
718
786
  }
@@ -884,10 +952,30 @@ function getHashKey(keySchema) {
884
952
  }
885
953
  return hash.AttributeName;
886
954
  }
955
+ function getHashKeys(keySchema) {
956
+ return keySchema.filter((k) => k.KeyType === "HASH").map((k) => k.AttributeName);
957
+ }
887
958
  function getRangeKey(keySchema) {
888
959
  const range = keySchema.find((k) => k.KeyType === "RANGE");
889
960
  return range?.AttributeName;
890
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
+ }
891
979
  function deepClone(obj) {
892
980
  return JSON.parse(JSON.stringify(obj));
893
981
  }
@@ -897,8 +985,10 @@ function estimateItemSize(item) {
897
985
 
898
986
  // src/expressions/key-condition.ts
899
987
  function parseKeyCondition(expression, keySchema, context) {
900
- const hashKeyName = getHashKey(keySchema);
901
- const rangeKeyName = getRangeKey(keySchema);
988
+ const hashKeyNames = getHashKeys(keySchema);
989
+ const rangeKeyNames = getRangeKeys(keySchema);
990
+ const hashKeyName = hashKeyNames[0];
991
+ const rangeKeyName = rangeKeyNames[0];
902
992
  const resolvedNames = context.expressionAttributeNames || {};
903
993
  const resolvedValues = context.expressionAttributeValues || {};
904
994
  function resolveName(name) {
@@ -943,17 +1033,17 @@ function parseKeyCondition(expression, keySchema, context) {
943
1033
  return s;
944
1034
  }
945
1035
  const normalizedExpression = stripOuterParens(expression);
946
- const parts = normalizedExpression.split(/\s+AND\s+/i);
947
- let hashValue;
948
- let rangeCondition;
1036
+ const parts = splitKeyConditions(normalizedExpression);
1037
+ const hashConditions = /* @__PURE__ */ new Map();
1038
+ const rangeConditions = /* @__PURE__ */ new Map();
949
1039
  for (const part of parts) {
950
1040
  const trimmed = stripOuterParens(part);
951
1041
  const beginsWithMatch = trimmed.match(/^begins_with\s*\(\s*([#\w]+)\s*,\s*(:\w+)\s*\)$/i);
952
1042
  if (beginsWithMatch) {
953
1043
  const attrName = resolveName(beginsWithMatch[1]);
954
1044
  const value = resolveValue(beginsWithMatch[2]);
955
- if (attrName === rangeKeyName) {
956
- rangeCondition = { operator: "begins_with", value };
1045
+ if (rangeKeyNames.includes(attrName)) {
1046
+ rangeConditions.set(attrName, { operator: "begins_with", value });
957
1047
  } else {
958
1048
  throw new ValidationException(`begins_with can only be used on sort key`);
959
1049
  }
@@ -964,8 +1054,8 @@ function parseKeyCondition(expression, keySchema, context) {
964
1054
  const attrName = resolveName(betweenMatch[1]);
965
1055
  const value1 = resolveValue(betweenMatch[2]);
966
1056
  const value2 = resolveValue(betweenMatch[3]);
967
- if (attrName === rangeKeyName) {
968
- rangeCondition = { operator: "BETWEEN", value: value1, value2 };
1057
+ if (rangeKeyNames.includes(attrName)) {
1058
+ rangeConditions.set(attrName, { operator: "BETWEEN", value: value1, value2 });
969
1059
  } else {
970
1060
  throw new ValidationException(`BETWEEN can only be used on sort key`);
971
1061
  }
@@ -976,13 +1066,13 @@ function parseKeyCondition(expression, keySchema, context) {
976
1066
  const attrName = resolveName(comparisonMatch[1]);
977
1067
  const operator = comparisonMatch[2];
978
1068
  const value = resolveValue(comparisonMatch[3]);
979
- if (attrName === hashKeyName) {
1069
+ if (hashKeyNames.includes(attrName)) {
980
1070
  if (operator !== "=") {
981
- throw new ValidationException(`Hash key condition must use = operator`);
1071
+ throw new ValidationException(`Partition key condition must use = operator`);
982
1072
  }
983
- hashValue = value;
984
- } else if (attrName === rangeKeyName) {
985
- rangeCondition = { operator, value };
1073
+ hashConditions.set(attrName, value);
1074
+ } else if (rangeKeyNames.includes(attrName)) {
1075
+ rangeConditions.set(attrName, { operator, value });
986
1076
  } else {
987
1077
  throw new ValidationException(`Key condition references unknown attribute: ${attrName}`);
988
1078
  }
@@ -990,30 +1080,112 @@ function parseKeyCondition(expression, keySchema, context) {
990
1080
  }
991
1081
  throw new ValidationException(`Invalid key condition expression: ${trimmed}`);
992
1082
  }
993
- if (!hashValue) {
994
- 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
+ }
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;
995
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
+ });
996
1121
  return {
997
1122
  hashKey: hashKeyName,
998
- hashValue,
1123
+ hashValue: hashConditions.get(hashKeyName),
999
1124
  rangeKey: rangeKeyName,
1000
- 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
1001
1132
  };
1002
1133
  }
1003
1134
  function matchesKeyCondition(item, condition) {
1004
- const itemHashValue = item[condition.hashKey];
1005
- if (!itemHashValue || !attributeEquals(itemHashValue, condition.hashValue)) {
1006
- 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
+ }
1007
1140
  }
1008
- if (condition.rangeCondition && condition.rangeKey) {
1009
- const itemRangeValue = item[condition.rangeKey];
1141
+ for (const rangeCondition of condition.rangeConditions) {
1142
+ const itemRangeValue = item[rangeCondition.key];
1010
1143
  if (!itemRangeValue) {
1011
1144
  return false;
1012
1145
  }
1013
- return matchesRangeCondition(itemRangeValue, condition.rangeCondition);
1146
+ if (!matchesRangeCondition(itemRangeValue, rangeCondition)) {
1147
+ return false;
1148
+ }
1014
1149
  }
1015
1150
  return true;
1016
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
+ }
1017
1189
  function matchesRangeCondition(value, condition) {
1018
1190
  const cmp5 = compareValues2(value, condition.value);
1019
1191
  switch (condition.operator) {
@@ -1634,9 +1806,10 @@ function query(store, input) {
1634
1806
  let items;
1635
1807
  let lastEvaluatedKey;
1636
1808
  if (input.IndexName) {
1809
+ const hashValues = Object.fromEntries(keyCondition.hashConditions.map((condition) => [condition.key, condition.value]));
1637
1810
  const result = table.queryIndex(
1638
1811
  input.IndexName,
1639
- { [keyCondition.hashKey]: keyCondition.hashValue },
1812
+ hashValues,
1640
1813
  {
1641
1814
  scanIndexForward: input.ScanIndexForward,
1642
1815
  exclusiveStartKey: input.ExclusiveStartKey
@@ -1645,8 +1818,9 @@ function query(store, input) {
1645
1818
  items = result.items;
1646
1819
  lastEvaluatedKey = result.lastEvaluatedKey;
1647
1820
  } else {
1821
+ const hashValues = Object.fromEntries(keyCondition.hashConditions.map((condition) => [condition.key, condition.value]));
1648
1822
  const result = table.queryByHashKey(
1649
- { [keyCondition.hashKey]: keyCondition.hashValue },
1823
+ hashValues,
1650
1824
  {
1651
1825
  scanIndexForward: input.ScanIndexForward,
1652
1826
  exclusiveStartKey: input.ExclusiveStartKey
@@ -1671,24 +1845,11 @@ function query(store, input) {
1671
1845
  items = items.slice(0, input.Limit);
1672
1846
  if (items.length > 0) {
1673
1847
  const lastItem = items[items.length - 1];
1674
- lastEvaluatedKey = {};
1675
- const hashKey = table.getHashKeyName();
1676
- const rangeKey = table.getRangeKeyName();
1677
- if (lastItem[hashKey]) {
1678
- lastEvaluatedKey[hashKey] = lastItem[hashKey];
1679
- }
1680
- if (rangeKey && lastItem[rangeKey]) {
1681
- lastEvaluatedKey[rangeKey] = lastItem[rangeKey];
1682
- }
1848
+ lastEvaluatedKey = extractKey(lastItem, table.keySchema);
1683
1849
  if (input.IndexName) {
1684
1850
  const indexKeySchema = table.getIndexKeySchema(input.IndexName);
1685
1851
  if (indexKeySchema) {
1686
- for (const key of indexKeySchema) {
1687
- const attrValue = lastItem[key.AttributeName];
1688
- if (attrValue) {
1689
- lastEvaluatedKey[key.AttributeName] = attrValue;
1690
- }
1691
- }
1852
+ lastEvaluatedKey = extractKey(lastItem, mergeKeySchemas(indexKeySchema, table.keySchema));
1692
1853
  }
1693
1854
  }
1694
1855
  }
@@ -1755,24 +1916,11 @@ function scan(store, input) {
1755
1916
  items = items.slice(0, input.Limit);
1756
1917
  if (items.length > 0) {
1757
1918
  const lastItem = items[items.length - 1];
1758
- lastEvaluatedKey = {};
1759
- const hashKey = table.getHashKeyName();
1760
- const rangeKey = table.getRangeKeyName();
1761
- if (lastItem[hashKey]) {
1762
- lastEvaluatedKey[hashKey] = lastItem[hashKey];
1763
- }
1764
- if (rangeKey && lastItem[rangeKey]) {
1765
- lastEvaluatedKey[rangeKey] = lastItem[rangeKey];
1766
- }
1919
+ lastEvaluatedKey = extractKey(lastItem, table.keySchema);
1767
1920
  if (input.IndexName) {
1768
1921
  const indexKeySchema = table.getIndexKeySchema(input.IndexName);
1769
1922
  if (indexKeySchema) {
1770
- for (const key of indexKeySchema) {
1771
- const attrValue = lastItem[key.AttributeName];
1772
- if (attrValue) {
1773
- lastEvaluatedKey[key.AttributeName] = attrValue;
1774
- }
1775
- }
1923
+ lastEvaluatedKey = extractKey(lastItem, mergeKeySchemas(indexKeySchema, table.keySchema));
1776
1924
  }
1777
1925
  }
1778
1926
  }
@@ -2569,12 +2717,7 @@ var Table = class {
2569
2717
  }
2570
2718
  }
2571
2719
  buildIndexKey(item, keySchema) {
2572
- const hashAttr = getHashKey(keySchema);
2573
- if (!item[hashAttr]) {
2574
- return null;
2575
- }
2576
- const rangeAttr = getRangeKey(keySchema);
2577
- if (rangeAttr && !item[rangeAttr]) {
2720
+ if (!hasCompleteKey(item, keySchema)) {
2578
2721
  return null;
2579
2722
  }
2580
2723
  return serializeKey(extractKey(item, keySchema), keySchema);
@@ -2620,47 +2763,7 @@ var Table = class {
2620
2763
  };
2621
2764
  }
2622
2765
  queryByHashKey(hashValue, options) {
2623
- const hashAttr = this.getHashKeyName();
2624
- const rangeAttr = this.getRangeKeyName();
2625
- const matchingItems = [];
2626
- for (const item of this.items.values()) {
2627
- const itemHashValue = item[hashAttr];
2628
- const queryHashValue = hashValue[hashAttr];
2629
- if (itemHashValue && queryHashValue && this.attributeEquals(itemHashValue, queryHashValue)) {
2630
- matchingItems.push(deepClone(item));
2631
- }
2632
- }
2633
- if (rangeAttr) {
2634
- matchingItems.sort((a, b) => {
2635
- const aVal = a[rangeAttr];
2636
- const bVal = b[rangeAttr];
2637
- if (!aVal && !bVal) return 0;
2638
- if (!aVal) return 1;
2639
- if (!bVal) return -1;
2640
- const cmp5 = this.compareAttributes(aVal, bVal);
2641
- return options?.scanIndexForward === false ? -cmp5 : cmp5;
2642
- });
2643
- }
2644
- let startIdx = 0;
2645
- if (options?.exclusiveStartKey) {
2646
- const startKey = serializeKey(options.exclusiveStartKey, this.keySchema);
2647
- startIdx = matchingItems.findIndex(
2648
- (item) => serializeKey(extractKey(item, this.keySchema), this.keySchema) === startKey
2649
- );
2650
- if (startIdx !== -1) {
2651
- startIdx++;
2652
- } else {
2653
- startIdx = 0;
2654
- }
2655
- }
2656
- const limit = options?.limit;
2657
- const sliced = limit ? matchingItems.slice(startIdx, startIdx + limit) : matchingItems.slice(startIdx);
2658
- const hasMore = limit ? startIdx + limit < matchingItems.length : false;
2659
- const lastItem = sliced[sliced.length - 1];
2660
- return {
2661
- items: sliced,
2662
- lastEvaluatedKey: hasMore && lastItem ? extractKey(lastItem, this.keySchema) : void 0
2663
- };
2766
+ return this.queryBySchema(this.keySchema, hashValue, this.keySchema, false, options);
2664
2767
  }
2665
2768
  queryIndex(indexName, hashValue, options) {
2666
2769
  const gsi = this.globalSecondaryIndexes.get(indexName);
@@ -2669,69 +2772,16 @@ var Table = class {
2669
2772
  if (!indexData) {
2670
2773
  throw new Error(`Index ${indexName} not found`);
2671
2774
  }
2672
- const indexHashAttr = getHashKey(indexData.keySchema);
2673
- const indexRangeAttr = getRangeKey(indexData.keySchema);
2674
- const matchingItems = [];
2675
- for (const item of this.items.values()) {
2676
- const itemHashValue = item[indexHashAttr];
2677
- const queryHashValue = hashValue[indexHashAttr];
2678
- if (!itemHashValue || !queryHashValue || !this.attributeEquals(itemHashValue, queryHashValue)) {
2679
- continue;
2680
- }
2681
- if (indexRangeAttr && !item[indexRangeAttr]) {
2682
- continue;
2683
- }
2684
- matchingItems.push(deepClone(item));
2685
- }
2686
- if (indexRangeAttr) {
2687
- matchingItems.sort((a, b) => {
2688
- const aVal = a[indexRangeAttr];
2689
- const bVal = b[indexRangeAttr];
2690
- if (!aVal && !bVal) return 0;
2691
- if (!aVal) return 1;
2692
- if (!bVal) return -1;
2693
- const cmp5 = this.compareAttributes(aVal, bVal);
2694
- return options?.scanIndexForward === false ? -cmp5 : cmp5;
2695
- });
2696
- }
2697
- let startIdx = 0;
2698
- if (options?.exclusiveStartKey) {
2699
- const combinedKeySchema = [...indexData.keySchema];
2700
- const tableRangeKey = this.getRangeKeyName();
2701
- if (tableRangeKey && !combinedKeySchema.some((k) => k.AttributeName === tableRangeKey)) {
2702
- combinedKeySchema.push({ AttributeName: tableRangeKey, KeyType: "RANGE" });
2703
- }
2704
- const tableHashKey = this.getHashKeyName();
2705
- if (!combinedKeySchema.some((k) => k.AttributeName === tableHashKey)) {
2706
- combinedKeySchema.push({ AttributeName: tableHashKey, KeyType: "HASH" });
2707
- }
2708
- const startKey = serializeKey(options.exclusiveStartKey, combinedKeySchema);
2709
- startIdx = matchingItems.findIndex(
2710
- (item) => serializeKey(extractKey(item, combinedKeySchema), combinedKeySchema) === startKey
2711
- );
2712
- if (startIdx !== -1) {
2713
- startIdx++;
2714
- } else {
2715
- startIdx = 0;
2716
- }
2717
- }
2718
- const limit = options?.limit;
2719
- const sliced = limit ? matchingItems.slice(startIdx, startIdx + limit) : matchingItems.slice(startIdx);
2720
- const hasMore = limit ? startIdx + limit < matchingItems.length : false;
2721
- const lastItem = sliced[sliced.length - 1];
2722
- let lastEvaluatedKey;
2723
- if (hasMore && lastItem) {
2724
- lastEvaluatedKey = extractKey(lastItem, this.keySchema);
2725
- for (const keyElement of indexData.keySchema) {
2726
- const attrValue = lastItem[keyElement.AttributeName];
2727
- if (attrValue) {
2728
- lastEvaluatedKey[keyElement.AttributeName] = attrValue;
2729
- }
2730
- }
2731
- }
2775
+ const result = this.queryBySchema(
2776
+ indexData.keySchema,
2777
+ hashValue,
2778
+ mergeKeySchemas(indexData.keySchema, this.keySchema),
2779
+ true,
2780
+ options
2781
+ );
2732
2782
  return {
2733
- items: sliced,
2734
- lastEvaluatedKey,
2783
+ items: result.items,
2784
+ lastEvaluatedKey: result.lastEvaluatedKey,
2735
2785
  indexKeySchema: indexData.keySchema
2736
2786
  };
2737
2787
  }
@@ -2742,18 +2792,19 @@ var Table = class {
2742
2792
  if (!indexData) {
2743
2793
  throw new Error(`Index ${indexName} not found`);
2744
2794
  }
2745
- const indexHashAttr = getHashKey(indexData.keySchema);
2746
2795
  const matchingItems = [];
2747
2796
  for (const item of this.items.values()) {
2748
- if (item[indexHashAttr]) {
2797
+ if (hasCompleteKey(item, indexData.keySchema)) {
2749
2798
  matchingItems.push(deepClone(item));
2750
2799
  }
2751
2800
  }
2801
+ matchingItems.sort((a, b) => this.compareByKeySchema(a, b, indexData.keySchema, true));
2752
2802
  let startIdx = 0;
2753
2803
  if (exclusiveStartKey) {
2754
- const startKey = serializeKey(exclusiveStartKey, this.keySchema);
2804
+ const paginationKeySchema = mergeKeySchemas(indexData.keySchema, this.keySchema);
2805
+ const startKey = serializeKey(exclusiveStartKey, paginationKeySchema);
2755
2806
  startIdx = matchingItems.findIndex(
2756
- (item) => serializeKey(extractKey(item, this.keySchema), this.keySchema) === startKey
2807
+ (item) => serializeKey(extractKey(item, paginationKeySchema), paginationKeySchema) === startKey
2757
2808
  );
2758
2809
  if (startIdx !== -1) {
2759
2810
  startIdx++;
@@ -2766,13 +2817,7 @@ var Table = class {
2766
2817
  const lastItem = sliced[sliced.length - 1];
2767
2818
  let lastEvaluatedKey;
2768
2819
  if (hasMore && lastItem) {
2769
- lastEvaluatedKey = extractKey(lastItem, this.keySchema);
2770
- for (const keyElement of indexData.keySchema) {
2771
- const attrValue = lastItem[keyElement.AttributeName];
2772
- if (attrValue) {
2773
- lastEvaluatedKey[keyElement.AttributeName] = attrValue;
2774
- }
2775
- }
2820
+ lastEvaluatedKey = extractKey(lastItem, mergeKeySchemas(indexData.keySchema, this.keySchema));
2776
2821
  }
2777
2822
  return {
2778
2823
  items: sliced,
@@ -2798,6 +2843,67 @@ var Table = class {
2798
2843
  if ("B" in a && "B" in b) return a.B.localeCompare(b.B);
2799
2844
  return 0;
2800
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
+ }
2801
2907
  getAllItems() {
2802
2908
  return Array.from(this.items.values()).map((item) => deepClone(item));
2803
2909
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awsless/dynamodb-server",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",