electrodb 3.6.2 → 3.7.1

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/src/entity.js CHANGED
@@ -33,7 +33,7 @@ const {
33
33
  IndexProjectionOptions,
34
34
  } = require("./types");
35
35
  const { FilterFactory } = require("./filters");
36
- const { FilterOperations, formatExpressionName } = require("./operations");
36
+ const { FilterOperations, ExpressionState, formatExpressionName } = require("./operations");
37
37
  const { WhereFactory } = require("./where");
38
38
  const { clauses, ChainState } = require("./clauses");
39
39
  const { EventManager } = require("./events");
@@ -312,6 +312,8 @@ class Entity {
312
312
  const chain = this._makeChain(index, clauses, clauses.index, chainOptions);
313
313
  if (options.indexType === IndexTypes.clustered) {
314
314
  return chain.clusteredCollection(collection, facets);
315
+ } else if (options.indexType === IndexTypes.composite) {
316
+ return chain.compositeCollection(collection, facets);
315
317
  } else {
316
318
  return chain.collection(collection, facets);
317
319
  }
@@ -432,21 +434,19 @@ class Entity {
432
434
  }
433
435
 
434
436
  async transactWrite(parameters, config) {
435
- let response = await this._exec(
437
+ return this._exec(
436
438
  MethodTypes.transactWrite,
437
439
  parameters,
438
440
  config,
439
441
  );
440
- return response;
441
442
  }
442
443
 
443
444
  async transactGet(parameters, config) {
444
- let response = await this._exec(
445
+ return this._exec(
445
446
  MethodTypes.transactGet,
446
447
  parameters,
447
448
  config,
448
449
  );
449
- return response;
450
450
  }
451
451
 
452
452
  async go(method, parameters = {}, config = {}) {
@@ -516,7 +516,8 @@ class Entity {
516
516
  );
517
517
  };
518
518
  const dynamoDBMethod = MethodTypeTranslation[method];
519
- return this.client[dynamoDBMethod](params)
519
+ const client = config.client || this.client;
520
+ return client[dynamoDBMethod](params)
520
521
  .promise()
521
522
  .then((results) => {
522
523
  notifyQuery();
@@ -1035,12 +1036,13 @@ class Entity {
1035
1036
  let keys = {};
1036
1037
  const secondaryIndexStrictMode =
1037
1038
  options.strict === "all" || options.strict === "pk" ? "pk" : "none";
1038
- for (const { index } of Object.values(this.model.indexes)) {
1039
+ for (const index of Object.values(this.model.indexes)) {
1040
+ const indexName = index.index;
1039
1041
  const indexKeys = this._fromCompositeToKeysByIndex(
1040
- { indexName: index, provided },
1042
+ { indexName, provided },
1041
1043
  {
1042
1044
  strict:
1043
- index === TableIndex ? options.strict : secondaryIndexStrictMode,
1045
+ indexName === TableIndex ? options.strict : secondaryIndexStrictMode,
1044
1046
  },
1045
1047
  );
1046
1048
  if (indexKeys) {
@@ -1224,21 +1226,28 @@ class Entity {
1224
1226
  ) {
1225
1227
  let allKeys = {};
1226
1228
 
1227
- const indexKeys = this._deconstructIndex({
1228
- index: indexName,
1229
- keys: provided,
1230
- });
1231
- if (!indexKeys) {
1232
- throw new e.ElectroError(
1233
- e.ErrorCodes.InvalidConversionKeysProvided,
1234
- `Provided keys did not include valid properties for the index "${indexName}"`,
1235
- );
1229
+ if (this._getIndexType(indexName) === IndexTypes.composite) {
1230
+ const item = this.model.schema.translateFromFields(provided);
1231
+ allKeys = {
1232
+ ...this._findFacets(item, this.model.facets.byIndex[indexName].pk),
1233
+ ...this._findFacets(item, this.model.facets.byIndex[indexName].sk),
1234
+ }
1235
+ } else {
1236
+ const indexKeys = this._deconstructIndex({
1237
+ index: indexName,
1238
+ keys: provided,
1239
+ });
1240
+ if (!indexKeys) {
1241
+ throw new e.ElectroError(
1242
+ e.ErrorCodes.InvalidConversionKeysProvided,
1243
+ `Provided keys did not include valid properties for the index "${indexName}"`,
1244
+ );
1245
+ }
1246
+ allKeys = {
1247
+ ...indexKeys,
1248
+ };
1236
1249
  }
1237
1250
 
1238
- allKeys = {
1239
- ...indexKeys,
1240
- };
1241
-
1242
1251
  let tableKeys;
1243
1252
  if (indexName !== TableIndex) {
1244
1253
  tableKeys = this._deconstructIndex({ index: TableIndex, keys: provided });
@@ -1283,10 +1292,17 @@ class Entity {
1283
1292
  );
1284
1293
  }
1285
1294
 
1295
+ _getIndexType(indexName) {
1296
+ return this.model.facets.byIndex[indexName].type;
1297
+ }
1298
+
1286
1299
  _trimKeysToIndex({ indexName = TableIndex, provided }) {
1287
1300
  if (!provided) {
1288
1301
  return null;
1289
1302
  }
1303
+ if (this.model.facets.byIndex[indexName].type === IndexTypes.composite) {
1304
+ return provided;
1305
+ }
1290
1306
 
1291
1307
  const pkName = this.model.translations.keys[indexName].pk;
1292
1308
  const skName = this.model.translations.keys[indexName].sk;
@@ -1315,13 +1331,17 @@ class Entity {
1315
1331
  );
1316
1332
  }
1317
1333
 
1318
- const pkName = this.model.translations.keys[indexName].pk;
1319
- const skName = this.model.translations.keys[indexName].sk;
1320
-
1321
- return (
1322
- provided[pkName] !== undefined &&
1323
- (!skName || provided[skName] !== undefined)
1324
- );
1334
+ if (this._getIndexType(indexName) === IndexTypes.composite) {
1335
+ const item = this.model.schema.translateFromFields(provided);
1336
+ return this.model.facets.byIndex[indexName].pk.every((attr) => item[attr] !== undefined);
1337
+ } else {
1338
+ const pkName = this.model.translations.keys[indexName].pk;
1339
+ const skName = this.model.translations.keys[indexName].sk;
1340
+ return (
1341
+ provided[pkName] !== undefined &&
1342
+ (!skName || provided[skName] !== undefined)
1343
+ );
1344
+ }
1325
1345
  }
1326
1346
 
1327
1347
  _formatReturnPager(config, lastEvaluatedKey) {
@@ -1585,12 +1605,19 @@ class Entity {
1585
1605
 
1586
1606
  _constructPagerIndex(index = TableIndex, item, options = {}) {
1587
1607
  let pkAttributes = options.relaxedPk
1588
- ? item
1608
+ ? this._findFacets(item, this.model.facets.byIndex[index].pk)
1589
1609
  : this._expectFacets(item, this.model.facets.byIndex[index].pk);
1590
1610
  let skAttributes = options.relaxedSk
1591
- ? item
1611
+ ? this._findFacets(item, this.model.facets.byIndex[index].sk)
1592
1612
  : this._expectFacets(item, this.model.facets.byIndex[index].sk);
1593
1613
 
1614
+ if (this._getIndexType(index) === IndexTypes.composite) {
1615
+ return this.model.schema.translateToFields({
1616
+ ...pkAttributes,
1617
+ ...skAttributes,
1618
+ });
1619
+ }
1620
+
1594
1621
  let keys = this._makeIndexKeys({
1595
1622
  index,
1596
1623
  pkAttributes,
@@ -1905,6 +1932,10 @@ class Entity {
1905
1932
  config.hydrator = option.hydrator;
1906
1933
  }
1907
1934
 
1935
+ if (option.client !== undefined) {
1936
+ config.client = c.normalizeClient(option.client);
1937
+ }
1938
+
1908
1939
  if (option._includeOnResponseItem) {
1909
1940
  config._includeOnResponseItem = {
1910
1941
  ...config._includeOnResponseItem,
@@ -2257,20 +2288,20 @@ class Entity {
2257
2288
  return key;
2258
2289
  }
2259
2290
 
2260
- getIdentifierExpressions(alias = this.getName()) {
2261
- let name = this.getName();
2262
- let version = this.getVersion();
2263
- return {
2264
- names: {
2265
- [`#${this.identifiers.entity}`]: this.identifiers.entity,
2266
- [`#${this.identifiers.version}`]: this.identifiers.version,
2267
- },
2268
- values: {
2269
- [`:${this.identifiers.entity}_${alias}`]: name,
2270
- [`:${this.identifiers.version}_${alias}`]: version,
2271
- },
2272
- expression: `(#${this.identifiers.entity} = :${this.identifiers.entity}_${alias} AND #${this.identifiers.version} = :${this.identifiers.version}_${alias})`,
2273
- };
2291
+ applyIdentifierExpressionState(expressionState, alias) {
2292
+ const name = this.getName();
2293
+ const version = this.getVersion();
2294
+ const nameRef = expressionState.setName({}, this.identifiers.entity, this.identifiers.entity);
2295
+ const versionRef = expressionState.setName({}, this.identifiers.version, this.identifiers.version);
2296
+ const nameVal = expressionState.setValue(
2297
+ `${this.identifiers.entity}_${alias || name}`,
2298
+ name,
2299
+ );
2300
+ const versionVal = expressionState.setValue(
2301
+ `${this.identifiers.version}_${alias || name}`,
2302
+ version,
2303
+ );
2304
+ return `(${nameRef.expression} = ${nameVal} AND ${versionRef.expression} = ${versionVal})`;
2274
2305
  }
2275
2306
 
2276
2307
  /* istanbul ignore next */
@@ -2682,7 +2713,7 @@ class Entity {
2682
2713
  }
2683
2714
  expressions.UpdateExpression = `${operation.toUpperCase()} ${expressions.UpdateExpression.join(
2684
2715
  ", ",
2685
- )}`;
2716
+ )}`.trim();
2686
2717
  return expressions;
2687
2718
  }
2688
2719
 
@@ -2709,61 +2740,236 @@ class Entity {
2709
2740
  }
2710
2741
  }
2711
2742
 
2712
- /* istanbul ignore next */
2713
- _queryParams(state = {}, options = {}) {
2714
- const indexKeys = this._makeQueryKeys(state, options);
2715
- let parameters = {};
2743
+
2744
+ _compositeQueryParams(state = {}, options = {}) {
2745
+ // todo: review "_consolidateQueryFacets"
2746
+ const consolidated = this._consolidateQueryFacets(
2747
+ state.query.keys.sk,
2748
+ ) || [];
2749
+
2750
+ const pkAttributes = state.query.keys.pk;
2751
+ const skAttributes = consolidated[0] || {};
2752
+
2753
+ // provided has length, isArray?
2754
+ const provided = state.query.keys.provided;
2755
+ const all = state.query.facets.all || [];
2756
+ const queryType = state.query.type;
2757
+ const expressionState = new ExpressionState({ prefix: "k_" });
2758
+
2759
+ const expressions = [];
2760
+ if (queryType === QueryTypes.between) {
2761
+ for (const [name, value] of Object.entries(pkAttributes)) {
2762
+ const field = this.model.schema.getFieldName(name);
2763
+ const nameRef = expressionState.setName({}, name, field);
2764
+ const valueRef = expressionState.setValue(name, value);
2765
+ expressions.push(`${nameRef.expression} = ${valueRef}`);
2766
+ }
2767
+ let is = {}
2768
+ let start = {}
2769
+ let end = {};
2770
+ (state.query.keys.sk || []).forEach(({type, facets}) => {
2771
+ if (type === QueryTypes.is || type === QueryTypes.composite_collection) {
2772
+ is = facets;
2773
+ } else if (type === QueryTypes.between) {
2774
+ start = facets;
2775
+ } else if (type === QueryTypes.and) {
2776
+ end = facets;
2777
+ } else {
2778
+ // todo: improve error handling
2779
+ throw new Error('Internal error: Invalid sort key type in composite between query');
2780
+ }
2781
+ });
2782
+
2783
+ let lastFound;
2784
+ const skNames = state.query.facets.sk || [];
2785
+ for (const name of skNames) {
2786
+ if (is[name] !== undefined) {
2787
+ lastFound = name;
2788
+ } else if (start[name] !== undefined && end[name] !== undefined) {
2789
+ lastFound = name;
2790
+ } else if (start[name] !== undefined || end[name] !== undefined) {
2791
+ throw new e.ElectroError(
2792
+ e.ErrorCodes.InvalidQueryParameters,
2793
+ `Invalid attribute combination provided to between query. Between queries on composite indexes must have the same attribute for start and end values until the last sort key attribute. The provided attribute ${name} is missing ${start[name] !== undefined ? 'an end' : 'a start'} value. This is a DynamoDB constraint.`
2794
+ );
2795
+ } else {
2796
+ break;
2797
+ }
2798
+ }
2799
+
2800
+ for (let i = 0; i < skNames.length; i++) {
2801
+ const name = skNames[i];
2802
+ if (lastFound === name) {
2803
+ const startValue = start[name];
2804
+ const endValue = end[name];
2805
+ const field = this.model.schema.getFieldName(name);
2806
+ const nameRef = expressionState.setName({}, name, field);
2807
+ const startValueRef = expressionState.setValue(name, startValue);
2808
+ const endValueRef = expressionState.setValue(name, endValue);
2809
+ expressions.push(`${nameRef.expression} BETWEEN ${startValueRef} AND ${endValueRef}`);
2810
+ } else if (is[name] !== undefined) {
2811
+ const value = is[name];
2812
+ const field = this.model.schema.getFieldName(name);
2813
+ const nameRef = expressionState.setName({}, name, field);
2814
+ const valueRef = expressionState.setValue(name, value);
2815
+ expressions.push(`${nameRef.expression} = ${valueRef}`);
2816
+ } else if (start[name] !== undefined && end[name] !== undefined) {
2817
+ if (start[name] !== end[name]) {
2818
+ throw new e.ElectroError(
2819
+ e.ErrorCodes.InvalidQueryParameters,
2820
+ `Invalid attribute combination provided to between query. Between queries on composite indexes must have the same attribute for start and end values until the last sort key attribute. The provided attribute ${name} has different start and end values. This is a DynamoDB constraint.`
2821
+ );
2822
+ }
2823
+ const value = start[name];
2824
+ const field = this.model.schema.getFieldName(name);
2825
+ const nameRef = expressionState.setName({}, name, field);
2826
+ const valueRef = expressionState.setValue(name, value);
2827
+ expressions.push(`${nameRef.expression} = ${valueRef}`);
2828
+ }
2829
+ }
2830
+ } else {
2831
+ const attrs = [];
2832
+ for (const { type, name } of all) {
2833
+ const value = type === "pk" ? pkAttributes[name] : skAttributes[name];
2834
+ if (value === undefined) {
2835
+ break;
2836
+ }
2837
+ attrs.push({type, name, value});
2838
+ }
2839
+ for (let i = 0; i < attrs.length; i++) {
2840
+ const { type, name, value } = attrs[i];
2841
+ const field = this.model.schema.getFieldName(name);
2842
+ const nameRef = expressionState.setName({}, name, field);
2843
+ const valueRef = expressionState.setValue(name, value);
2844
+ const shouldApplyEq = !(type === "sk" && i === attrs.length - 1)
2845
+ if (shouldApplyEq) {
2846
+ expressions.push(`${nameRef.expression} = ${valueRef}`);
2847
+ continue;
2848
+ }
2849
+ switch (queryType) {
2850
+ case QueryTypes.is:
2851
+ case QueryTypes.eq:
2852
+ case QueryTypes.collection:
2853
+ case QueryTypes.composite_collection:
2854
+ expressions.push(`${nameRef.expression} = ${valueRef}`);
2855
+ break;
2856
+ case QueryTypes.begins:
2857
+ expressions.push(`begins_with(${nameRef.expression}, ${valueRef})`);
2858
+ break;
2859
+ case QueryTypes.gt:
2860
+ expressions.push(`${nameRef.expression} > ${valueRef}`);
2861
+ break;
2862
+ case QueryTypes.gte:
2863
+ expressions.push(`${nameRef.expression} >= ${valueRef}`);
2864
+ break;
2865
+ case QueryTypes.lt:
2866
+ expressions.push(`${nameRef.expression} < ${valueRef}`);
2867
+ break;
2868
+ case QueryTypes.lte:
2869
+ expressions.push(`${nameRef.expression} <= ${valueRef}`);
2870
+ break;
2871
+ case QueryTypes.between: {
2872
+ const second = consolidated[consolidated.length - 1];
2873
+ const value2 = second[name];
2874
+ const valueRef2 = expressionState.setValue(name, value2);
2875
+ expressions.push(`${nameRef.expression} BETWEEN ${valueRef} AND ${valueRef2}`);
2876
+ break;
2877
+ }
2878
+ // todo: clean up here
2879
+ case QueryTypes.clustered_collection:
2880
+ default:
2881
+ // todo: improve error handling
2882
+ throw new Error('Not supported')
2883
+ }
2884
+ }
2885
+ }
2886
+
2887
+ const filter = state.query.filter[ExpressionTypes.FilterExpression];
2888
+ const customExpressions = {
2889
+ names: (state.query.options.expressions && state.query.options.expressions.names) || {},
2890
+ values: (state.query.options.expressions && state.query.options.expressions.values) || {},
2891
+ expression: (state.query.options.expressions && state.query.options.expressions.expression) || "",
2892
+ };
2893
+
2894
+ // identifiers are added via custom expressions on collection queries inside `clauses/handleNonIsolatedCollection`
2895
+ // Don't duplicate filters if they are provided.
2896
+ const identifierExpression = !options.ignoreOwnership && !customExpressions.expression ? this.applyIdentifierExpressionState(expressionState) : '';
2897
+
2898
+ const params = {
2899
+ IndexName: state.query.index,
2900
+ KeyConditionExpression: expressions.join(' AND '),
2901
+ TableName: this.getTableName(),
2902
+ ExpressionAttributeNames: this._mergeExpressionsAttributes(
2903
+ filter.getNames(),
2904
+ expressionState.getNames(),
2905
+ customExpressions.names,
2906
+ ),
2907
+ ExpressionAttributeValues: this._mergeExpressionsAttributes(
2908
+ filter.getValues(),
2909
+ expressionState.getValues(),
2910
+ customExpressions.values,
2911
+ ),
2912
+ }
2913
+
2914
+ let filerExpressions = [customExpressions.expression || "", filter.build(), identifierExpression]
2915
+ .map(s => s.trim())
2916
+ .filter(Boolean)
2917
+ .join(" AND ");
2918
+
2919
+ if (filerExpressions.length) {
2920
+ params.FilterExpression = filerExpressions;
2921
+ }
2922
+
2923
+ return params;
2924
+ }
2925
+
2926
+ _makeQueryParams(state = {}, options = {}, indexKeys) {
2716
2927
  switch (state.query.type) {
2717
2928
  case QueryTypes.is:
2718
- parameters = this._makeIsQueryParams(
2929
+ return this._makeIsQueryParams(
2719
2930
  state.query,
2720
2931
  state.query.index,
2721
2932
  state.query.filter[ExpressionTypes.FilterExpression],
2722
2933
  indexKeys.pk,
2723
2934
  ...indexKeys.sk,
2724
2935
  );
2725
- break;
2726
2936
  case QueryTypes.begins:
2727
- parameters = this._makeBeginsWithQueryParams(
2937
+ return this._makeBeginsWithQueryParams(
2728
2938
  state.query.options,
2729
2939
  state.query.index,
2730
2940
  state.query.filter[ExpressionTypes.FilterExpression],
2731
2941
  indexKeys.pk,
2732
2942
  ...indexKeys.sk,
2733
2943
  );
2734
- break;
2735
2944
  case QueryTypes.collection:
2736
- parameters = this._makeBeginsWithQueryParams(
2945
+ return this._makeBeginsWithQueryParams(
2737
2946
  state.query.options,
2738
2947
  state.query.index,
2739
2948
  state.query.filter[ExpressionTypes.FilterExpression],
2740
2949
  indexKeys.pk,
2741
2950
  this._getCollectionSk(state.query.collection),
2742
2951
  );
2743
- break;
2744
2952
  case QueryTypes.clustered_collection:
2745
- parameters = this._makeBeginsWithQueryParams(
2953
+ return this._makeBeginsWithQueryParams(
2746
2954
  state.query.options,
2747
2955
  state.query.index,
2748
2956
  state.query.filter[ExpressionTypes.FilterExpression],
2749
2957
  indexKeys.pk,
2750
2958
  ...indexKeys.sk,
2751
2959
  );
2752
- break;
2753
2960
  case QueryTypes.between:
2754
- parameters = this._makeBetweenQueryParams(
2961
+ return this._makeBetweenQueryParams(
2755
2962
  state.query.options,
2756
2963
  state.query.index,
2757
2964
  state.query.filter[ExpressionTypes.FilterExpression],
2758
2965
  indexKeys.pk,
2759
2966
  ...indexKeys.sk,
2760
2967
  );
2761
- break;
2762
2968
  case QueryTypes.gte:
2763
2969
  case QueryTypes.gt:
2764
2970
  case QueryTypes.lte:
2765
2971
  case QueryTypes.lt:
2766
- parameters = this._makeComparisonQueryParams(
2972
+ return this._makeComparisonQueryParams(
2767
2973
  state.query.index,
2768
2974
  state.query.type,
2769
2975
  state.query.filter[ExpressionTypes.FilterExpression],
@@ -2771,10 +2977,20 @@ class Entity {
2771
2977
  options,
2772
2978
  state.query.options,
2773
2979
  );
2774
- break;
2775
2980
  default:
2776
2981
  throw new Error(`Invalid query type: ${state.query.type}`);
2777
2982
  }
2983
+ }
2984
+
2985
+ /* istanbul ignore next */
2986
+ _queryParams(state = {}, options = {}) {
2987
+ const indexKeys = this._makeQueryKeys(state, options);
2988
+ let parameters;
2989
+ if (state.query.options.indexType !== IndexTypes.composite) {
2990
+ parameters = this._makeQueryParams(state, options, indexKeys);
2991
+ } else {
2992
+ parameters = this._compositeQueryParams(state, options);
2993
+ }
2778
2994
 
2779
2995
  const appliedParameters = this._applyParameterOptions({
2780
2996
  params: parameters,
@@ -3064,8 +3280,12 @@ class Entity {
3064
3280
  _makeKeysFromAttributes(indexes, attributes, conditions) {
3065
3281
  let indexKeys = {};
3066
3282
  for (let [index, keyTypes] of Object.entries(indexes)) {
3283
+ if (this.model.lookup.compositeIndexes.has(index)) {
3284
+ continue;
3285
+ }
3286
+
3067
3287
  const shouldMakeKeys =
3068
- !this._indexConditionIsDefined(index) || conditions[index];
3288
+ (!this._indexConditionIsDefined(index) || conditions[index]);
3069
3289
  if (!shouldMakeKeys && index !== TableIndex) {
3070
3290
  continue;
3071
3291
  }
@@ -3285,6 +3505,11 @@ class Entity {
3285
3505
  if (attributes[attribute] !== undefined) {
3286
3506
  facets[attribute] = attributes[attribute];
3287
3507
  indexes.forEach((definition) => {
3508
+ // composite indexes do not have keys
3509
+ if (definition.type === IndexTypes.composite) {
3510
+ return;
3511
+ }
3512
+
3288
3513
  const { index, type } = definition;
3289
3514
  impactedIndexes[index] = impactedIndexes[index] || {};
3290
3515
  impactedIndexes[index][type] = impactedIndexes[index][type] || [];
@@ -3303,9 +3528,12 @@ class Entity {
3303
3528
 
3304
3529
  // this function is used to determine key impact for update `set`, update `delete`, and `put`. This block is currently only used by update `set`
3305
3530
  if (utilizeIncludedOnlyIndexes) {
3306
- for (const [index, { pk, sk }] of Object.entries(
3531
+ for (const [index, { pk, sk, type }] of Object.entries(
3307
3532
  this.model.facets.byIndex,
3308
3533
  )) {
3534
+ if (type === IndexTypes.composite) {
3535
+ continue;
3536
+ }
3309
3537
  // The main table index is handled somewhere else (messy I know), and we only want to do this processing if an
3310
3538
  // index condition is defined for backwards compatibility. Backwards compatibility is not required for this
3311
3539
  // change, but I have paranoid concerns of breaking changes around sparse indexes.
@@ -3367,10 +3595,12 @@ class Entity {
3367
3595
  }
3368
3596
  }
3369
3597
 
3370
- let indexesWithMissingComposites = Object.entries(
3371
- this.model.facets.byIndex,
3372
- ).map(([index, definition]) => {
3373
- const { pk, sk } = definition;
3598
+ let indexesWithMissingComposites = [];
3599
+ for (const [index, definition] of Object.entries(this.model.facets.byIndex)) {
3600
+ const { pk, sk, type } = definition;
3601
+ if (type === IndexTypes.composite) {
3602
+ continue;
3603
+ }
3374
3604
  let impacted = impactedIndexes[index];
3375
3605
  let impact = {
3376
3606
  index,
@@ -3408,8 +3638,8 @@ class Entity {
3408
3638
  }
3409
3639
  }
3410
3640
 
3411
- return impact;
3412
- });
3641
+ indexesWithMissingComposites.push(impact);
3642
+ }
3413
3643
 
3414
3644
  let incomplete = [];
3415
3645
  for (const { index, missing, definition } of indexesWithMissingComposites) {
@@ -3536,7 +3766,11 @@ class Entity {
3536
3766
  }
3537
3767
  }
3538
3768
 
3539
- _findProperties(obj, properties) {
3769
+ _findFacets(obj, properties) {
3770
+ return Object.fromEntries(this._findProperties(obj, properties));
3771
+ }
3772
+
3773
+ _findProperties(obj, properties = []) {
3540
3774
  return properties.map((name) => [name, obj[name]]);
3541
3775
  }
3542
3776
 
@@ -3707,8 +3941,8 @@ class Entity {
3707
3941
  e.ErrorCodes.IncompatibleKeyCasing,
3708
3942
  `Partition Key (pk) on Access Pattern '${u.formatIndexNameForDisplay(
3709
3943
  tableIndex.index,
3710
- )}' is defined with the casing ${keys.pk.casing}, but the accessPattern '${u.formatIndexNameForDisplay(
3711
- previouslyDefinedPk.indexName,
3944
+ )}' is defined with the casing ${keys.pk.casing}, but the Access Pattern '${u.formatIndexNameForDisplay(
3945
+ previouslyDefinedPk.definition.accessPattern,
3712
3946
  )}' defines the same index field with the ${previouslyDefinedPk.definition.casing === DefaultKeyCasing ? '(default)' : ''} casing ${previouslyDefinedPk.definition.casing}. Key fields must have the same casing definitions across all indexes they are involved with.`,
3713
3947
  );
3714
3948
  }
@@ -3723,8 +3957,8 @@ class Entity {
3723
3957
  e.ErrorCodes.IncompatibleKeyCasing,
3724
3958
  `Sort Key (sk) on Access Pattern '${u.formatIndexNameForDisplay(
3725
3959
  tableIndex.index,
3726
- )}' is defined with the casing ${keys.sk.casing}, but the accessPattern '${u.formatIndexNameForDisplay(
3727
- previouslyDefinedSk.indexName,
3960
+ )}' is defined with the casing ${keys.sk.casing}, but the Access Pattern '${u.formatIndexNameForDisplay(
3961
+ previouslyDefinedSk.definition.accessPattern,
3728
3962
  )}' defines the same index field with the ${previouslyDefinedSk.definition.casing === DefaultKeyCasing ? '(default)' : ''} casing ${previouslyDefinedSk.definition.casing}. Key fields must have the same casing definitions across all indexes they are involved with.`,
3729
3963
  );
3730
3964
  }
@@ -3832,7 +4066,6 @@ class Entity {
3832
4066
  this.model.facets.labels[index] &&
3833
4067
  Array.isArray(this.model.facets.labels[index].sk);
3834
4068
  let labels = hasLabels ? this.model.facets.labels[index].sk : [];
3835
- // const hasFacets = Object.keys(skFacet).length > 0;
3836
4069
  let sortKey = this._makeKey(prefixes.sk, facets.sk, skFacet, labels, {
3837
4070
  excludeLabelTail: true,
3838
4071
  excludePostfix,
@@ -4277,6 +4510,7 @@ class Entity {
4277
4510
  let indexHasSortKeys = {};
4278
4511
  let indexHasSubCollections = {};
4279
4512
  let clusteredIndexes = new Set();
4513
+ let compositeIndexes = new Set();
4280
4514
  let indexAccessPatternTransaction = {
4281
4515
  fromAccessPatternToIndex: {},
4282
4516
  fromIndexToAccessPattern: {},
@@ -4320,9 +4554,48 @@ class Entity {
4320
4554
  let conditionDefined = v.isFunction(index.condition);
4321
4555
  let indexCondition = index.condition || (() => true);
4322
4556
 
4323
- if (indexType === "clustered") {
4557
+ if (indexType === IndexTypes.clustered) {
4558
+ // todo: make contents consistent with "compositeIndexes" below
4559
+ // this is not consistent with "compositeIndexes" (which uses the index name), this should be fixed in the future.
4324
4560
  clusteredIndexes.add(accessPattern);
4561
+ } else if (indexType === IndexTypes.composite) {
4562
+ if (indexName === TableIndex) {
4563
+ throw new e.ElectroError(
4564
+ e.ErrorCodes.InvalidIndexDefinition,
4565
+ `The Access Pattern "${accessPattern}" cannot be defined as a composite index. AWS DynamoDB does not allow for composite indexes on the main table index.`,
4566
+ );
4567
+ }
4568
+ if (conditionDefined) {
4569
+ throw new e.ElectroError(
4570
+ e.ErrorCodes.InvalidIndexCondition,
4571
+ `The Access Pattern "${accessPattern}" is defined as a "${indexType}" index, but a condition callback is defined. Composite indexes do not support the use of a condition callback.`,
4572
+ );
4573
+ }
4574
+ if (index.scope !== undefined) {
4575
+ throw new e.ElectroError(
4576
+ e.ErrorCodes.InvalidIndexCondition,
4577
+ `The Access Pattern "${accessPattern}" is defined as a "${indexType}" index, but a "scope" value was defined. Composite indexes do not support the use of scope.`,
4578
+ );
4579
+ }
4580
+ if (index.pk.field !== undefined || (index.sk && index.sk.field !== undefined)) {
4581
+ throw new e.ElectroError(
4582
+ e.ErrorCodes.InvalidIndexDefinition,
4583
+ `The Access Pattern "${accessPattern}" is defined as a "${indexType}" index, but the Partition Key or Sort Key is defined with a field property. Composite indexes do not support the use of a field property, their attributes defined in the composite array define the indexes member attributes.`,
4584
+ );
4585
+ }
4586
+ // this is not consistent with "clusteredIndexes" (which uses the access pattern name), but it is more correct given the naming.
4587
+ compositeIndexes.add(indexName);
4588
+ }
4589
+
4590
+ if (indexType !== IndexTypes.composite) {
4591
+ if (index.pk.field === undefined || (index.sk && index.sk.field === undefined)) {
4592
+ throw new e.ElectroError(
4593
+ e.ErrorCodes.InvalidIndexDefinition,
4594
+ `The Access Pattern "${accessPattern}" is defined as a "${indexType}" index, but the Partition Key or Sort Key is defined without a field property. Unless using composite attributes, indexes must be defined with a field property that maps to the field name on the DynamoDB table KeySchema.`,
4595
+ );
4596
+ }
4325
4597
  }
4598
+
4326
4599
  if (seenIndexes[indexName] !== undefined) {
4327
4600
  if (indexName === TableIndex) {
4328
4601
  throw new e.ElectroError(
@@ -4349,6 +4622,22 @@ class Entity {
4349
4622
  )}', contains a collection definition without a defined SK. Collections can only be defined on indexes with a defined SK.`,
4350
4623
  );
4351
4624
  }
4625
+
4626
+ if (indexType !== IndexTypes.composite) {
4627
+ if (hasSk && index.sk.field === undefined) {
4628
+ throw new e.ElectroError(
4629
+ e.ErrorCodes.InvalidIndexCompositeAttributes,
4630
+ `The ${accessPattern} Access pattern is defined as a "${indexType}" index, but a Sort Key is defined without a Range Key field mapping.`,
4631
+ );
4632
+ }
4633
+ if (index.pk.field === undefined) {
4634
+ throw new e.ElectroError(
4635
+ e.ErrorCodes.InvalidIndexCompositeAttributes,
4636
+ `The ${accessPattern} Access pattern is defined as a "${indexType}" index, but a Partition Key is defined without a HasKey field mapping.`,
4637
+ );
4638
+ }
4639
+ }
4640
+
4352
4641
  let collection = index.collection || "";
4353
4642
  let customFacets = {
4354
4643
  pk: false,
@@ -4522,13 +4811,16 @@ class Entity {
4522
4811
  facets.attributes = [...facets.attributes, ...attributes];
4523
4812
  facets.projections = [...facets.projections, ...projections];
4524
4813
 
4525
- facets.fields.push(pk.field);
4814
+ if (definition.type !== IndexTypes.composite) {
4815
+ facets.fields.push(pk.field);
4816
+ }
4526
4817
 
4527
4818
  facets.byIndex[indexName] = {
4528
4819
  customFacets,
4529
4820
  pk: pk.facets,
4530
4821
  sk: sk.facets,
4531
4822
  all: attributes,
4823
+ type: index.type,
4532
4824
  collection: index.collection,
4533
4825
  hasSortKeys: !!indexHasSortKeys[indexName],
4534
4826
  hasSubCollections: !!indexHasSubCollections[indexName],
@@ -4538,110 +4830,111 @@ class Entity {
4538
4830
  },
4539
4831
  };
4540
4832
 
4541
- facets.byField = facets.byField || {};
4542
- facets.byField[pk.field] = facets.byField[pk.field] || {};
4543
- facets.byField[pk.field][indexName] = pk;
4544
- if (sk.field) {
4545
- facets.byField[sk.field] = facets.byField[sk.field] || {};
4546
- facets.byField[sk.field][indexName] = sk;
4547
- }
4548
-
4549
- if (seenIndexFields[pk.field] !== undefined) {
4550
- const definition = Object.values(facets.byField[pk.field]).find(
4551
- (definition) => definition.index !== indexName,
4552
- );
4553
-
4554
- const definitionsMatch = validations.stringArrayMatch(
4555
- pk.facets,
4556
- definition.facets,
4557
- );
4558
-
4559
- if (!definitionsMatch) {
4560
- throw new e.ElectroError(
4561
- e.ErrorCodes.InconsistentIndexDefinition,
4562
- `Partition Key (pk) on Access Pattern '${u.formatIndexNameForDisplay(
4563
- accessPattern,
4564
- )}' is defined with the composite attribute(s) ${u.commaSeparatedString(
4565
- pk.facets,
4566
- )}, but the accessPattern '${u.formatIndexNameForDisplay(
4567
- definition.index,
4568
- )}' defines this field with the composite attributes ${u.commaSeparatedString(
4569
- definition.facets,
4570
- )}'. Key fields must have the same composite attribute definitions across all indexes they are involved with`,
4571
- );
4572
- }
4573
-
4574
- const keyTemplatesMatch = pk.template === definition.template
4575
-
4576
- if (!keyTemplatesMatch) {
4577
- throw new e.ElectroError(
4578
- e.ErrorCodes.IncompatibleKeyCompositeAttributeTemplate,
4579
- `Partition Key (pk) on Access Pattern '${u.formatIndexNameForDisplay(
4580
- accessPattern,
4581
- )}' is defined with the template ${pk.template || '(undefined)'}, but the accessPattern '${u.formatIndexNameForDisplay(
4582
- definition.index,
4583
- )}' defines this field with the key labels ${definition.template || '(undefined)'}'. Key fields must have the same template definitions across all indexes they are involved with`,
4584
- );
4833
+ if (definition.type !== IndexTypes.composite) {
4834
+ facets.byField = facets.byField || {};
4835
+ facets.byField[pk.field] = facets.byField[pk.field] || {};
4836
+ facets.byField[pk.field][indexName] = pk;
4837
+ if (sk.field) {
4838
+ facets.byField[sk.field] = facets.byField[sk.field] || {};
4839
+ facets.byField[sk.field][indexName] = sk;
4585
4840
  }
4586
4841
 
4587
- seenIndexFields[pk.field].push({ accessPattern, type: "pk" });
4588
- } else {
4589
- seenIndexFields[pk.field] = [];
4590
- seenIndexFields[pk.field].push({ accessPattern, type: "pk" });
4591
- }
4592
-
4593
- if (sk.field) {
4594
- if (sk.field === pk.field) {
4595
- throw new e.ElectroError(
4596
- e.ErrorCodes.DuplicateIndexFields,
4597
- `The Access Pattern '${u.formatIndexNameForDisplay(
4598
- accessPattern,
4599
- )}' references the field '${
4600
- sk.field
4601
- }' as the field name for both the PK and SK. Fields used for indexes need to be unique to avoid conflicts.`,
4602
- );
4603
- } else if (seenIndexFields[sk.field] !== undefined) {
4604
- const definition = Object.values(facets.byField[sk.field]).find(
4842
+ if (seenIndexFields[pk.field] !== undefined) {
4843
+ const definition = Object.values(facets.byField[pk.field]).find(
4605
4844
  (definition) => definition.index !== indexName,
4606
4845
  );
4607
4846
 
4608
4847
  const definitionsMatch = validations.stringArrayMatch(
4609
- sk.facets,
4848
+ pk.facets,
4610
4849
  definition.facets,
4611
- )
4612
-
4850
+ );
4613
4851
  if (!definitionsMatch) {
4614
4852
  throw new e.ElectroError(
4615
- e.ErrorCodes.DuplicateIndexFields,
4616
- `Sort Key (sk) on Access Pattern '${u.formatIndexNameForDisplay(
4853
+ e.ErrorCodes.InconsistentIndexDefinition,
4854
+ `Partition Key (pk) on Access Pattern '${u.formatIndexNameForDisplay(
4617
4855
  accessPattern,
4618
4856
  )}' is defined with the composite attribute(s) ${u.commaSeparatedString(
4619
- sk.facets,
4620
- )}, but the accessPattern '${u.formatIndexNameForDisplay(
4621
- definition.index,
4857
+ pk.facets,
4858
+ )}, but the Access Pattern '${u.formatIndexNameForDisplay(
4859
+ definition.accessPattern,
4622
4860
  )}' defines this field with the composite attributes ${u.commaSeparatedString(
4623
4861
  definition.facets,
4624
4862
  )}'. Key fields must have the same composite attribute definitions across all indexes they are involved with`,
4625
4863
  );
4626
4864
  }
4627
4865
 
4628
- const keyTemplatesMatch = sk.template === definition.template
4866
+ const keyTemplatesMatch = pk.template === definition.template
4629
4867
 
4630
4868
  if (!keyTemplatesMatch) {
4631
4869
  throw new e.ElectroError(
4632
4870
  e.ErrorCodes.IncompatibleKeyCompositeAttributeTemplate,
4633
- `Sort Key (sk) on Access Pattern '${u.formatIndexNameForDisplay(
4871
+ `Partition Key (pk) on Access Pattern '${u.formatIndexNameForDisplay(
4634
4872
  accessPattern,
4635
- )}' is defined with the template ${sk.template || '(undefined)'}, but the accessPattern '${u.formatIndexNameForDisplay(
4636
- definition.index,
4873
+ )}' is defined with the template ${pk.template || '(undefined)'}, but the Access Pattern '${u.formatIndexNameForDisplay(
4874
+ definition.accessPattern,
4637
4875
  )}' defines this field with the key labels ${definition.template || '(undefined)'}'. Key fields must have the same template definitions across all indexes they are involved with`,
4638
4876
  );
4639
4877
  }
4640
4878
 
4641
- seenIndexFields[sk.field].push({ accessPattern, type: "sk" });
4879
+ seenIndexFields[pk.field].push({ accessPattern, type: "pk" });
4642
4880
  } else {
4643
- seenIndexFields[sk.field] = [];
4644
- seenIndexFields[sk.field].push({ accessPattern, type: "sk" });
4881
+ seenIndexFields[pk.field] = [];
4882
+ seenIndexFields[pk.field].push({ accessPattern, type: "pk" });
4883
+ }
4884
+
4885
+ if (sk.field) {
4886
+ if (sk.field === pk.field) {
4887
+ throw new e.ElectroError(
4888
+ e.ErrorCodes.DuplicateIndexFields,
4889
+ `The Access Pattern '${u.formatIndexNameForDisplay(
4890
+ accessPattern,
4891
+ )}' references the field '${
4892
+ sk.field
4893
+ }' as the field name for both the PK and SK. Fields used for indexes need to be unique to avoid conflicts.`,
4894
+ );
4895
+ } else if (seenIndexFields[sk.field] !== undefined) {
4896
+ const definition = Object.values(facets.byField[sk.field]).find(
4897
+ (definition) => definition.index !== indexName,
4898
+ );
4899
+
4900
+ const definitionsMatch = validations.stringArrayMatch(
4901
+ sk.facets,
4902
+ definition.facets,
4903
+ )
4904
+
4905
+ if (!definitionsMatch) {
4906
+ throw new e.ElectroError(
4907
+ e.ErrorCodes.DuplicateIndexFields,
4908
+ `Sort Key (sk) on Access Pattern '${u.formatIndexNameForDisplay(
4909
+ accessPattern,
4910
+ )}' is defined with the composite attribute(s) ${u.commaSeparatedString(
4911
+ sk.facets,
4912
+ )}, but the Access Pattern '${u.formatIndexNameForDisplay(
4913
+ definition.accessPattern,
4914
+ )}' defines this field with the composite attributes ${u.commaSeparatedString(
4915
+ definition.facets,
4916
+ )}'. Key fields must have the same composite attribute definitions across all indexes they are involved with`,
4917
+ );
4918
+ }
4919
+
4920
+ const keyTemplatesMatch = sk.template === definition.template
4921
+
4922
+ if (!keyTemplatesMatch) {
4923
+ throw new e.ElectroError(
4924
+ e.ErrorCodes.IncompatibleKeyCompositeAttributeTemplate,
4925
+ `Sort Key (sk) on Access Pattern '${u.formatIndexNameForDisplay(
4926
+ accessPattern,
4927
+ )}' is defined with the template ${sk.template || '(undefined)'}, but the Access Pattern '${u.formatIndexNameForDisplay(
4928
+ definition.accessPattern,
4929
+ )}' defines this field with the key labels ${definition.template || '(undefined)'}'. Key fields must have the same template definitions across all indexes they are involved with`,
4930
+ );
4931
+ }
4932
+
4933
+ seenIndexFields[sk.field].push({ accessPattern, type: "sk" });
4934
+ } else {
4935
+ seenIndexFields[sk.field] = [];
4936
+ seenIndexFields[sk.field].push({ accessPattern, type: "sk" });
4937
+ }
4645
4938
  }
4646
4939
  }
4647
4940
 
@@ -4713,6 +5006,7 @@ class Entity {
4713
5006
  subCollections,
4714
5007
  indexHasSortKeys,
4715
5008
  clusteredIndexes,
5009
+ compositeIndexes,
4716
5010
  indexHasSubCollections,
4717
5011
  indexes: normalized,
4718
5012
  indexField: indexFieldTranslation,
@@ -4918,6 +5212,7 @@ class Entity {
4918
5212
  subCollections,
4919
5213
  indexCollection,
4920
5214
  clusteredIndexes,
5215
+ compositeIndexes,
4921
5216
  indexHasSortKeys,
4922
5217
  indexAccessPattern,
4923
5218
  indexHasSubCollections,
@@ -4991,6 +5286,7 @@ class Entity {
4991
5286
  modelVersion,
4992
5287
  subCollections,
4993
5288
  lookup: {
5289
+ compositeIndexes,
4994
5290
  clusteredIndexes,
4995
5291
  indexHasSortKeys,
4996
5292
  indexHasSubCollections,