electrodb 3.6.2 → 3.7.0

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 = {}) {
@@ -1035,12 +1035,13 @@ class Entity {
1035
1035
  let keys = {};
1036
1036
  const secondaryIndexStrictMode =
1037
1037
  options.strict === "all" || options.strict === "pk" ? "pk" : "none";
1038
- for (const { index } of Object.values(this.model.indexes)) {
1038
+ for (const index of Object.values(this.model.indexes)) {
1039
+ const indexName = index.index;
1039
1040
  const indexKeys = this._fromCompositeToKeysByIndex(
1040
- { indexName: index, provided },
1041
+ { indexName, provided },
1041
1042
  {
1042
1043
  strict:
1043
- index === TableIndex ? options.strict : secondaryIndexStrictMode,
1044
+ indexName === TableIndex ? options.strict : secondaryIndexStrictMode,
1044
1045
  },
1045
1046
  );
1046
1047
  if (indexKeys) {
@@ -1224,21 +1225,28 @@ class Entity {
1224
1225
  ) {
1225
1226
  let allKeys = {};
1226
1227
 
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
- );
1228
+ if (this._getIndexType(indexName) === IndexTypes.composite) {
1229
+ const item = this.model.schema.translateFromFields(provided);
1230
+ allKeys = {
1231
+ ...this._findFacets(item, this.model.facets.byIndex[indexName].pk),
1232
+ ...this._findFacets(item, this.model.facets.byIndex[indexName].sk),
1233
+ }
1234
+ } else {
1235
+ const indexKeys = this._deconstructIndex({
1236
+ index: indexName,
1237
+ keys: provided,
1238
+ });
1239
+ if (!indexKeys) {
1240
+ throw new e.ElectroError(
1241
+ e.ErrorCodes.InvalidConversionKeysProvided,
1242
+ `Provided keys did not include valid properties for the index "${indexName}"`,
1243
+ );
1244
+ }
1245
+ allKeys = {
1246
+ ...indexKeys,
1247
+ };
1236
1248
  }
1237
1249
 
1238
- allKeys = {
1239
- ...indexKeys,
1240
- };
1241
-
1242
1250
  let tableKeys;
1243
1251
  if (indexName !== TableIndex) {
1244
1252
  tableKeys = this._deconstructIndex({ index: TableIndex, keys: provided });
@@ -1283,10 +1291,17 @@ class Entity {
1283
1291
  );
1284
1292
  }
1285
1293
 
1294
+ _getIndexType(indexName) {
1295
+ return this.model.facets.byIndex[indexName].type;
1296
+ }
1297
+
1286
1298
  _trimKeysToIndex({ indexName = TableIndex, provided }) {
1287
1299
  if (!provided) {
1288
1300
  return null;
1289
1301
  }
1302
+ if (this.model.facets.byIndex[indexName].type === IndexTypes.composite) {
1303
+ return provided;
1304
+ }
1290
1305
 
1291
1306
  const pkName = this.model.translations.keys[indexName].pk;
1292
1307
  const skName = this.model.translations.keys[indexName].sk;
@@ -1315,13 +1330,17 @@ class Entity {
1315
1330
  );
1316
1331
  }
1317
1332
 
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
- );
1333
+ if (this._getIndexType(indexName) === IndexTypes.composite) {
1334
+ const item = this.model.schema.translateFromFields(provided);
1335
+ return this.model.facets.byIndex[indexName].pk.every((attr) => item[attr] !== undefined);
1336
+ } else {
1337
+ const pkName = this.model.translations.keys[indexName].pk;
1338
+ const skName = this.model.translations.keys[indexName].sk;
1339
+ return (
1340
+ provided[pkName] !== undefined &&
1341
+ (!skName || provided[skName] !== undefined)
1342
+ );
1343
+ }
1325
1344
  }
1326
1345
 
1327
1346
  _formatReturnPager(config, lastEvaluatedKey) {
@@ -1585,12 +1604,19 @@ class Entity {
1585
1604
 
1586
1605
  _constructPagerIndex(index = TableIndex, item, options = {}) {
1587
1606
  let pkAttributes = options.relaxedPk
1588
- ? item
1607
+ ? this._findFacets(item, this.model.facets.byIndex[index].pk)
1589
1608
  : this._expectFacets(item, this.model.facets.byIndex[index].pk);
1590
1609
  let skAttributes = options.relaxedSk
1591
- ? item
1610
+ ? this._findFacets(item, this.model.facets.byIndex[index].sk)
1592
1611
  : this._expectFacets(item, this.model.facets.byIndex[index].sk);
1593
1612
 
1613
+ if (this._getIndexType(index) === IndexTypes.composite) {
1614
+ return this.model.schema.translateToFields({
1615
+ ...pkAttributes,
1616
+ ...skAttributes,
1617
+ });
1618
+ }
1619
+
1594
1620
  let keys = this._makeIndexKeys({
1595
1621
  index,
1596
1622
  pkAttributes,
@@ -2257,20 +2283,20 @@ class Entity {
2257
2283
  return key;
2258
2284
  }
2259
2285
 
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
- };
2286
+ applyIdentifierExpressionState(expressionState, alias) {
2287
+ const name = this.getName();
2288
+ const version = this.getVersion();
2289
+ const nameRef = expressionState.setName({}, this.identifiers.entity, this.identifiers.entity);
2290
+ const versionRef = expressionState.setName({}, this.identifiers.version, this.identifiers.version);
2291
+ const nameVal = expressionState.setValue(
2292
+ `${this.identifiers.entity}_${alias || name}`,
2293
+ name,
2294
+ );
2295
+ const versionVal = expressionState.setValue(
2296
+ `${this.identifiers.version}_${alias || name}`,
2297
+ version,
2298
+ );
2299
+ return `(${nameRef.expression} = ${nameVal} AND ${versionRef.expression} = ${versionVal})`;
2274
2300
  }
2275
2301
 
2276
2302
  /* istanbul ignore next */
@@ -2682,7 +2708,7 @@ class Entity {
2682
2708
  }
2683
2709
  expressions.UpdateExpression = `${operation.toUpperCase()} ${expressions.UpdateExpression.join(
2684
2710
  ", ",
2685
- )}`;
2711
+ )}`.trim();
2686
2712
  return expressions;
2687
2713
  }
2688
2714
 
@@ -2709,61 +2735,236 @@ class Entity {
2709
2735
  }
2710
2736
  }
2711
2737
 
2712
- /* istanbul ignore next */
2713
- _queryParams(state = {}, options = {}) {
2714
- const indexKeys = this._makeQueryKeys(state, options);
2715
- let parameters = {};
2738
+
2739
+ _compositeQueryParams(state = {}, options = {}) {
2740
+ // todo: review "_consolidateQueryFacets"
2741
+ const consolidated = this._consolidateQueryFacets(
2742
+ state.query.keys.sk,
2743
+ ) || [];
2744
+
2745
+ const pkAttributes = state.query.keys.pk;
2746
+ const skAttributes = consolidated[0] || {};
2747
+
2748
+ // provided has length, isArray?
2749
+ const provided = state.query.keys.provided;
2750
+ const all = state.query.facets.all || [];
2751
+ const queryType = state.query.type;
2752
+ const expressionState = new ExpressionState({ prefix: "k_" });
2753
+
2754
+ const expressions = [];
2755
+ if (queryType === QueryTypes.between) {
2756
+ for (const [name, value] of Object.entries(pkAttributes)) {
2757
+ const field = this.model.schema.getFieldName(name);
2758
+ const nameRef = expressionState.setName({}, name, field);
2759
+ const valueRef = expressionState.setValue(name, value);
2760
+ expressions.push(`${nameRef.expression} = ${valueRef}`);
2761
+ }
2762
+ let is = {}
2763
+ let start = {}
2764
+ let end = {};
2765
+ (state.query.keys.sk ?? []).forEach(({type, facets}) => {
2766
+ if (type === QueryTypes.is || type === QueryTypes.composite_collection) {
2767
+ is = facets;
2768
+ } else if (type === QueryTypes.between) {
2769
+ start = facets;
2770
+ } else if (type === QueryTypes.and) {
2771
+ end = facets;
2772
+ } else {
2773
+ // todo: improve error handling
2774
+ throw new Error('Internal error: Invalid sort key type in composite between query');
2775
+ }
2776
+ });
2777
+
2778
+ let lastFound;
2779
+ const skNames = state.query.facets.sk || [];
2780
+ for (const name of skNames) {
2781
+ if (is[name] !== undefined) {
2782
+ lastFound = name;
2783
+ } else if (start[name] !== undefined && end[name] !== undefined) {
2784
+ lastFound = name;
2785
+ } else if (start[name] !== undefined || end[name] !== undefined) {
2786
+ throw new e.ElectroError(
2787
+ e.ErrorCodes.InvalidQueryParameters,
2788
+ `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.`
2789
+ );
2790
+ } else {
2791
+ break;
2792
+ }
2793
+ }
2794
+
2795
+ for (let i = 0; i < skNames.length; i++) {
2796
+ const name = skNames[i];
2797
+ if (lastFound === name) {
2798
+ const startValue = start[name];
2799
+ const endValue = end[name];
2800
+ const field = this.model.schema.getFieldName(name);
2801
+ const nameRef = expressionState.setName({}, name, field);
2802
+ const startValueRef = expressionState.setValue(name, startValue);
2803
+ const endValueRef = expressionState.setValue(name, endValue);
2804
+ expressions.push(`${nameRef.expression} BETWEEN ${startValueRef} AND ${endValueRef}`);
2805
+ } else if (is[name] !== undefined) {
2806
+ const value = is[name];
2807
+ const field = this.model.schema.getFieldName(name);
2808
+ const nameRef = expressionState.setName({}, name, field);
2809
+ const valueRef = expressionState.setValue(name, value);
2810
+ expressions.push(`${nameRef.expression} = ${valueRef}`);
2811
+ } else if (start[name] !== undefined && end[name] !== undefined) {
2812
+ if (start[name] !== end[name]) {
2813
+ throw new e.ElectroError(
2814
+ e.ErrorCodes.InvalidQueryParameters,
2815
+ `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.`
2816
+ );
2817
+ }
2818
+ const value = start[name];
2819
+ const field = this.model.schema.getFieldName(name);
2820
+ const nameRef = expressionState.setName({}, name, field);
2821
+ const valueRef = expressionState.setValue(name, value);
2822
+ expressions.push(`${nameRef.expression} = ${valueRef}`);
2823
+ }
2824
+ }
2825
+ } else {
2826
+ const attrs = [];
2827
+ for (const { type, name } of all) {
2828
+ const value = type === "pk" ? pkAttributes[name] : skAttributes[name];
2829
+ if (value === undefined) {
2830
+ break;
2831
+ }
2832
+ attrs.push({type, name, value});
2833
+ }
2834
+ for (let i = 0; i < attrs.length; i++) {
2835
+ const { type, name, value } = attrs[i];
2836
+ const field = this.model.schema.getFieldName(name);
2837
+ const nameRef = expressionState.setName({}, name, field);
2838
+ const valueRef = expressionState.setValue(name, value);
2839
+ const shouldApplyEq = !(type === "sk" && i === attrs.length - 1)
2840
+ if (shouldApplyEq) {
2841
+ expressions.push(`${nameRef.expression} = ${valueRef}`);
2842
+ continue;
2843
+ }
2844
+ switch (queryType) {
2845
+ case QueryTypes.is:
2846
+ case QueryTypes.eq:
2847
+ case QueryTypes.collection:
2848
+ case QueryTypes.composite_collection:
2849
+ expressions.push(`${nameRef.expression} = ${valueRef}`);
2850
+ break;
2851
+ case QueryTypes.begins:
2852
+ expressions.push(`begins_with(${nameRef.expression}, ${valueRef})`);
2853
+ break;
2854
+ case QueryTypes.gt:
2855
+ expressions.push(`${nameRef.expression} > ${valueRef}`);
2856
+ break;
2857
+ case QueryTypes.gte:
2858
+ expressions.push(`${nameRef.expression} >= ${valueRef}`);
2859
+ break;
2860
+ case QueryTypes.lt:
2861
+ expressions.push(`${nameRef.expression} < ${valueRef}`);
2862
+ break;
2863
+ case QueryTypes.lte:
2864
+ expressions.push(`${nameRef.expression} <= ${valueRef}`);
2865
+ break;
2866
+ case QueryTypes.between: {
2867
+ const second = consolidated[consolidated.length - 1];
2868
+ const value2 = second[name];
2869
+ const valueRef2 = expressionState.setValue(name, value2);
2870
+ expressions.push(`${nameRef.expression} BETWEEN ${valueRef} AND ${valueRef2}`);
2871
+ break;
2872
+ }
2873
+ // todo: clean up here
2874
+ case QueryTypes.clustered_collection:
2875
+ default:
2876
+ // todo: improve error handling
2877
+ throw new Error('Not supported')
2878
+ }
2879
+ }
2880
+ }
2881
+
2882
+ const filter = state.query.filter[ExpressionTypes.FilterExpression];
2883
+ const customExpressions = {
2884
+ names: (state.query.options.expressions && state.query.options.expressions.names) || {},
2885
+ values: (state.query.options.expressions && state.query.options.expressions.values) || {},
2886
+ expression: (state.query.options.expressions && state.query.options.expressions.expression) || "",
2887
+ };
2888
+
2889
+ // identifiers are added via custom expressions on collection queries inside `clauses/handleNonIsolatedCollection`
2890
+ // Don't duplicate filters if they are provided.
2891
+ const identifierExpression = !options.ignoreOwnership && !customExpressions.expression ? this.applyIdentifierExpressionState(expressionState) : '';
2892
+
2893
+ const params = {
2894
+ IndexName: state.query.index,
2895
+ KeyConditionExpression: expressions.join(' AND '),
2896
+ TableName: this.getTableName(),
2897
+ ExpressionAttributeNames: this._mergeExpressionsAttributes(
2898
+ filter.getNames(),
2899
+ expressionState.getNames(),
2900
+ customExpressions.names,
2901
+ ),
2902
+ ExpressionAttributeValues: this._mergeExpressionsAttributes(
2903
+ filter.getValues(),
2904
+ expressionState.getValues(),
2905
+ customExpressions.values,
2906
+ ),
2907
+ }
2908
+
2909
+ let filerExpressions = [customExpressions.expression || "", filter.build(), identifierExpression]
2910
+ .map(s => s.trim())
2911
+ .filter(Boolean)
2912
+ .join(" AND ");
2913
+
2914
+ if (filerExpressions.length) {
2915
+ params.FilterExpression = filerExpressions;
2916
+ }
2917
+
2918
+ return params;
2919
+ }
2920
+
2921
+ _makeQueryParams(state = {}, options = {}, indexKeys) {
2716
2922
  switch (state.query.type) {
2717
2923
  case QueryTypes.is:
2718
- parameters = this._makeIsQueryParams(
2924
+ return this._makeIsQueryParams(
2719
2925
  state.query,
2720
2926
  state.query.index,
2721
2927
  state.query.filter[ExpressionTypes.FilterExpression],
2722
2928
  indexKeys.pk,
2723
2929
  ...indexKeys.sk,
2724
2930
  );
2725
- break;
2726
2931
  case QueryTypes.begins:
2727
- parameters = this._makeBeginsWithQueryParams(
2932
+ return this._makeBeginsWithQueryParams(
2728
2933
  state.query.options,
2729
2934
  state.query.index,
2730
2935
  state.query.filter[ExpressionTypes.FilterExpression],
2731
2936
  indexKeys.pk,
2732
2937
  ...indexKeys.sk,
2733
2938
  );
2734
- break;
2735
2939
  case QueryTypes.collection:
2736
- parameters = this._makeBeginsWithQueryParams(
2940
+ return this._makeBeginsWithQueryParams(
2737
2941
  state.query.options,
2738
2942
  state.query.index,
2739
2943
  state.query.filter[ExpressionTypes.FilterExpression],
2740
2944
  indexKeys.pk,
2741
2945
  this._getCollectionSk(state.query.collection),
2742
2946
  );
2743
- break;
2744
2947
  case QueryTypes.clustered_collection:
2745
- parameters = this._makeBeginsWithQueryParams(
2948
+ return this._makeBeginsWithQueryParams(
2746
2949
  state.query.options,
2747
2950
  state.query.index,
2748
2951
  state.query.filter[ExpressionTypes.FilterExpression],
2749
2952
  indexKeys.pk,
2750
2953
  ...indexKeys.sk,
2751
2954
  );
2752
- break;
2753
2955
  case QueryTypes.between:
2754
- parameters = this._makeBetweenQueryParams(
2956
+ return this._makeBetweenQueryParams(
2755
2957
  state.query.options,
2756
2958
  state.query.index,
2757
2959
  state.query.filter[ExpressionTypes.FilterExpression],
2758
2960
  indexKeys.pk,
2759
2961
  ...indexKeys.sk,
2760
2962
  );
2761
- break;
2762
2963
  case QueryTypes.gte:
2763
2964
  case QueryTypes.gt:
2764
2965
  case QueryTypes.lte:
2765
2966
  case QueryTypes.lt:
2766
- parameters = this._makeComparisonQueryParams(
2967
+ return this._makeComparisonQueryParams(
2767
2968
  state.query.index,
2768
2969
  state.query.type,
2769
2970
  state.query.filter[ExpressionTypes.FilterExpression],
@@ -2771,10 +2972,20 @@ class Entity {
2771
2972
  options,
2772
2973
  state.query.options,
2773
2974
  );
2774
- break;
2775
2975
  default:
2776
2976
  throw new Error(`Invalid query type: ${state.query.type}`);
2777
2977
  }
2978
+ }
2979
+
2980
+ /* istanbul ignore next */
2981
+ _queryParams(state = {}, options = {}) {
2982
+ const indexKeys = this._makeQueryKeys(state, options);
2983
+ let parameters;
2984
+ if (state.query.options.indexType !== IndexTypes.composite) {
2985
+ parameters = this._makeQueryParams(state, options, indexKeys);
2986
+ } else {
2987
+ parameters = this._compositeQueryParams(state, options);
2988
+ }
2778
2989
 
2779
2990
  const appliedParameters = this._applyParameterOptions({
2780
2991
  params: parameters,
@@ -3064,8 +3275,12 @@ class Entity {
3064
3275
  _makeKeysFromAttributes(indexes, attributes, conditions) {
3065
3276
  let indexKeys = {};
3066
3277
  for (let [index, keyTypes] of Object.entries(indexes)) {
3278
+ if (this.model.lookup.compositeIndexes.has(index)) {
3279
+ continue;
3280
+ }
3281
+
3067
3282
  const shouldMakeKeys =
3068
- !this._indexConditionIsDefined(index) || conditions[index];
3283
+ (!this._indexConditionIsDefined(index) || conditions[index]);
3069
3284
  if (!shouldMakeKeys && index !== TableIndex) {
3070
3285
  continue;
3071
3286
  }
@@ -3285,6 +3500,11 @@ class Entity {
3285
3500
  if (attributes[attribute] !== undefined) {
3286
3501
  facets[attribute] = attributes[attribute];
3287
3502
  indexes.forEach((definition) => {
3503
+ // composite indexes do not have keys
3504
+ if (definition.type === IndexTypes.composite) {
3505
+ return;
3506
+ }
3507
+
3288
3508
  const { index, type } = definition;
3289
3509
  impactedIndexes[index] = impactedIndexes[index] || {};
3290
3510
  impactedIndexes[index][type] = impactedIndexes[index][type] || [];
@@ -3303,9 +3523,12 @@ class Entity {
3303
3523
 
3304
3524
  // 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
3525
  if (utilizeIncludedOnlyIndexes) {
3306
- for (const [index, { pk, sk }] of Object.entries(
3526
+ for (const [index, { pk, sk, type }] of Object.entries(
3307
3527
  this.model.facets.byIndex,
3308
3528
  )) {
3529
+ if (type === IndexTypes.composite) {
3530
+ continue;
3531
+ }
3309
3532
  // The main table index is handled somewhere else (messy I know), and we only want to do this processing if an
3310
3533
  // index condition is defined for backwards compatibility. Backwards compatibility is not required for this
3311
3534
  // change, but I have paranoid concerns of breaking changes around sparse indexes.
@@ -3367,10 +3590,12 @@ class Entity {
3367
3590
  }
3368
3591
  }
3369
3592
 
3370
- let indexesWithMissingComposites = Object.entries(
3371
- this.model.facets.byIndex,
3372
- ).map(([index, definition]) => {
3373
- const { pk, sk } = definition;
3593
+ let indexesWithMissingComposites = [];
3594
+ for (const [index, definition] of Object.entries(this.model.facets.byIndex)) {
3595
+ const { pk, sk, type } = definition;
3596
+ if (type === IndexTypes.composite) {
3597
+ continue;
3598
+ }
3374
3599
  let impacted = impactedIndexes[index];
3375
3600
  let impact = {
3376
3601
  index,
@@ -3408,8 +3633,8 @@ class Entity {
3408
3633
  }
3409
3634
  }
3410
3635
 
3411
- return impact;
3412
- });
3636
+ indexesWithMissingComposites.push(impact);
3637
+ }
3413
3638
 
3414
3639
  let incomplete = [];
3415
3640
  for (const { index, missing, definition } of indexesWithMissingComposites) {
@@ -3536,7 +3761,11 @@ class Entity {
3536
3761
  }
3537
3762
  }
3538
3763
 
3539
- _findProperties(obj, properties) {
3764
+ _findFacets(obj, properties) {
3765
+ return Object.fromEntries(this._findProperties(obj, properties));
3766
+ }
3767
+
3768
+ _findProperties(obj, properties = []) {
3540
3769
  return properties.map((name) => [name, obj[name]]);
3541
3770
  }
3542
3771
 
@@ -3707,8 +3936,8 @@ class Entity {
3707
3936
  e.ErrorCodes.IncompatibleKeyCasing,
3708
3937
  `Partition Key (pk) on Access Pattern '${u.formatIndexNameForDisplay(
3709
3938
  tableIndex.index,
3710
- )}' is defined with the casing ${keys.pk.casing}, but the accessPattern '${u.formatIndexNameForDisplay(
3711
- previouslyDefinedPk.indexName,
3939
+ )}' is defined with the casing ${keys.pk.casing}, but the Access Pattern '${u.formatIndexNameForDisplay(
3940
+ previouslyDefinedPk.definition.accessPattern,
3712
3941
  )}' 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
3942
  );
3714
3943
  }
@@ -3723,8 +3952,8 @@ class Entity {
3723
3952
  e.ErrorCodes.IncompatibleKeyCasing,
3724
3953
  `Sort Key (sk) on Access Pattern '${u.formatIndexNameForDisplay(
3725
3954
  tableIndex.index,
3726
- )}' is defined with the casing ${keys.sk.casing}, but the accessPattern '${u.formatIndexNameForDisplay(
3727
- previouslyDefinedSk.indexName,
3955
+ )}' is defined with the casing ${keys.sk.casing}, but the Access Pattern '${u.formatIndexNameForDisplay(
3956
+ previouslyDefinedSk.definition.accessPattern,
3728
3957
  )}' 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
3958
  );
3730
3959
  }
@@ -3832,7 +4061,6 @@ class Entity {
3832
4061
  this.model.facets.labels[index] &&
3833
4062
  Array.isArray(this.model.facets.labels[index].sk);
3834
4063
  let labels = hasLabels ? this.model.facets.labels[index].sk : [];
3835
- // const hasFacets = Object.keys(skFacet).length > 0;
3836
4064
  let sortKey = this._makeKey(prefixes.sk, facets.sk, skFacet, labels, {
3837
4065
  excludeLabelTail: true,
3838
4066
  excludePostfix,
@@ -4277,6 +4505,7 @@ class Entity {
4277
4505
  let indexHasSortKeys = {};
4278
4506
  let indexHasSubCollections = {};
4279
4507
  let clusteredIndexes = new Set();
4508
+ let compositeIndexes = new Set();
4280
4509
  let indexAccessPatternTransaction = {
4281
4510
  fromAccessPatternToIndex: {},
4282
4511
  fromIndexToAccessPattern: {},
@@ -4320,9 +4549,48 @@ class Entity {
4320
4549
  let conditionDefined = v.isFunction(index.condition);
4321
4550
  let indexCondition = index.condition || (() => true);
4322
4551
 
4323
- if (indexType === "clustered") {
4552
+ if (indexType === IndexTypes.clustered) {
4553
+ // todo: make contents consistent with "compositeIndexes" below
4554
+ // this is not consistent with "compositeIndexes" (which uses the index name), this should be fixed in the future.
4324
4555
  clusteredIndexes.add(accessPattern);
4556
+ } else if (indexType === IndexTypes.composite) {
4557
+ if (indexName === TableIndex) {
4558
+ throw new e.ElectroError(
4559
+ e.ErrorCodes.InvalidIndexDefinition,
4560
+ `The Access Pattern "${accessPattern}" cannot be defined as a composite index. AWS DynamoDB does not allow for composite indexes on the main table index.`,
4561
+ );
4562
+ }
4563
+ if (conditionDefined) {
4564
+ throw new e.ElectroError(
4565
+ e.ErrorCodes.InvalidIndexCondition,
4566
+ `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.`,
4567
+ );
4568
+ }
4569
+ if (index.scope !== undefined) {
4570
+ throw new e.ElectroError(
4571
+ e.ErrorCodes.InvalidIndexCondition,
4572
+ `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.`,
4573
+ );
4574
+ }
4575
+ if (index.pk.field !== undefined || (index.sk && index.sk.field !== undefined)) {
4576
+ throw new e.ElectroError(
4577
+ e.ErrorCodes.InvalidIndexDefinition,
4578
+ `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.`,
4579
+ );
4580
+ }
4581
+ // this is not consistent with "clusteredIndexes" (which uses the access pattern name), but it is more correct given the naming.
4582
+ compositeIndexes.add(indexName);
4583
+ }
4584
+
4585
+ if (indexType !== IndexTypes.composite) {
4586
+ if (index.pk.field === undefined || (index.sk && index.sk.field === undefined)) {
4587
+ throw new e.ElectroError(
4588
+ e.ErrorCodes.InvalidIndexDefinition,
4589
+ `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.`,
4590
+ );
4591
+ }
4325
4592
  }
4593
+
4326
4594
  if (seenIndexes[indexName] !== undefined) {
4327
4595
  if (indexName === TableIndex) {
4328
4596
  throw new e.ElectroError(
@@ -4349,6 +4617,22 @@ class Entity {
4349
4617
  )}', contains a collection definition without a defined SK. Collections can only be defined on indexes with a defined SK.`,
4350
4618
  );
4351
4619
  }
4620
+
4621
+ if (indexType !== IndexTypes.composite) {
4622
+ if (hasSk && index.sk.field === undefined) {
4623
+ throw new e.ElectroError(
4624
+ e.ErrorCodes.InvalidIndexCompositeAttributes,
4625
+ `The ${accessPattern} Access pattern is defined as a "${indexType}" index, but a Sort Key is defined without a Range Key field mapping.`,
4626
+ );
4627
+ }
4628
+ if (index.pk.field === undefined) {
4629
+ throw new e.ElectroError(
4630
+ e.ErrorCodes.InvalidIndexCompositeAttributes,
4631
+ `The ${accessPattern} Access pattern is defined as a "${indexType}" index, but a Partition Key is defined without a HasKey field mapping.`,
4632
+ );
4633
+ }
4634
+ }
4635
+
4352
4636
  let collection = index.collection || "";
4353
4637
  let customFacets = {
4354
4638
  pk: false,
@@ -4522,13 +4806,16 @@ class Entity {
4522
4806
  facets.attributes = [...facets.attributes, ...attributes];
4523
4807
  facets.projections = [...facets.projections, ...projections];
4524
4808
 
4525
- facets.fields.push(pk.field);
4809
+ if (definition.type !== IndexTypes.composite) {
4810
+ facets.fields.push(pk.field);
4811
+ }
4526
4812
 
4527
4813
  facets.byIndex[indexName] = {
4528
4814
  customFacets,
4529
4815
  pk: pk.facets,
4530
4816
  sk: sk.facets,
4531
4817
  all: attributes,
4818
+ type: index.type,
4532
4819
  collection: index.collection,
4533
4820
  hasSortKeys: !!indexHasSortKeys[indexName],
4534
4821
  hasSubCollections: !!indexHasSubCollections[indexName],
@@ -4538,110 +4825,111 @@ class Entity {
4538
4825
  },
4539
4826
  };
4540
4827
 
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
- );
4828
+ if (definition.type !== IndexTypes.composite) {
4829
+ facets.byField = facets.byField || {};
4830
+ facets.byField[pk.field] = facets.byField[pk.field] || {};
4831
+ facets.byField[pk.field][indexName] = pk;
4832
+ if (sk.field) {
4833
+ facets.byField[sk.field] = facets.byField[sk.field] || {};
4834
+ facets.byField[sk.field][indexName] = sk;
4585
4835
  }
4586
4836
 
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(
4837
+ if (seenIndexFields[pk.field] !== undefined) {
4838
+ const definition = Object.values(facets.byField[pk.field]).find(
4605
4839
  (definition) => definition.index !== indexName,
4606
4840
  );
4607
4841
 
4608
4842
  const definitionsMatch = validations.stringArrayMatch(
4609
- sk.facets,
4843
+ pk.facets,
4610
4844
  definition.facets,
4611
- )
4612
-
4845
+ );
4613
4846
  if (!definitionsMatch) {
4614
4847
  throw new e.ElectroError(
4615
- e.ErrorCodes.DuplicateIndexFields,
4616
- `Sort Key (sk) on Access Pattern '${u.formatIndexNameForDisplay(
4848
+ e.ErrorCodes.InconsistentIndexDefinition,
4849
+ `Partition Key (pk) on Access Pattern '${u.formatIndexNameForDisplay(
4617
4850
  accessPattern,
4618
4851
  )}' is defined with the composite attribute(s) ${u.commaSeparatedString(
4619
- sk.facets,
4620
- )}, but the accessPattern '${u.formatIndexNameForDisplay(
4621
- definition.index,
4852
+ pk.facets,
4853
+ )}, but the Access Pattern '${u.formatIndexNameForDisplay(
4854
+ definition.accessPattern,
4622
4855
  )}' defines this field with the composite attributes ${u.commaSeparatedString(
4623
4856
  definition.facets,
4624
4857
  )}'. Key fields must have the same composite attribute definitions across all indexes they are involved with`,
4625
4858
  );
4626
4859
  }
4627
4860
 
4628
- const keyTemplatesMatch = sk.template === definition.template
4861
+ const keyTemplatesMatch = pk.template === definition.template
4629
4862
 
4630
4863
  if (!keyTemplatesMatch) {
4631
4864
  throw new e.ElectroError(
4632
4865
  e.ErrorCodes.IncompatibleKeyCompositeAttributeTemplate,
4633
- `Sort Key (sk) on Access Pattern '${u.formatIndexNameForDisplay(
4866
+ `Partition Key (pk) on Access Pattern '${u.formatIndexNameForDisplay(
4634
4867
  accessPattern,
4635
- )}' is defined with the template ${sk.template || '(undefined)'}, but the accessPattern '${u.formatIndexNameForDisplay(
4636
- definition.index,
4868
+ )}' is defined with the template ${pk.template || '(undefined)'}, but the Access Pattern '${u.formatIndexNameForDisplay(
4869
+ definition.accessPattern,
4637
4870
  )}' 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
4871
  );
4639
4872
  }
4640
4873
 
4641
- seenIndexFields[sk.field].push({ accessPattern, type: "sk" });
4874
+ seenIndexFields[pk.field].push({ accessPattern, type: "pk" });
4642
4875
  } else {
4643
- seenIndexFields[sk.field] = [];
4644
- seenIndexFields[sk.field].push({ accessPattern, type: "sk" });
4876
+ seenIndexFields[pk.field] = [];
4877
+ seenIndexFields[pk.field].push({ accessPattern, type: "pk" });
4878
+ }
4879
+
4880
+ if (sk.field) {
4881
+ if (sk.field === pk.field) {
4882
+ throw new e.ElectroError(
4883
+ e.ErrorCodes.DuplicateIndexFields,
4884
+ `The Access Pattern '${u.formatIndexNameForDisplay(
4885
+ accessPattern,
4886
+ )}' references the field '${
4887
+ sk.field
4888
+ }' as the field name for both the PK and SK. Fields used for indexes need to be unique to avoid conflicts.`,
4889
+ );
4890
+ } else if (seenIndexFields[sk.field] !== undefined) {
4891
+ const definition = Object.values(facets.byField[sk.field]).find(
4892
+ (definition) => definition.index !== indexName,
4893
+ );
4894
+
4895
+ const definitionsMatch = validations.stringArrayMatch(
4896
+ sk.facets,
4897
+ definition.facets,
4898
+ )
4899
+
4900
+ if (!definitionsMatch) {
4901
+ throw new e.ElectroError(
4902
+ e.ErrorCodes.DuplicateIndexFields,
4903
+ `Sort Key (sk) on Access Pattern '${u.formatIndexNameForDisplay(
4904
+ accessPattern,
4905
+ )}' is defined with the composite attribute(s) ${u.commaSeparatedString(
4906
+ sk.facets,
4907
+ )}, but the Access Pattern '${u.formatIndexNameForDisplay(
4908
+ definition.accessPattern,
4909
+ )}' defines this field with the composite attributes ${u.commaSeparatedString(
4910
+ definition.facets,
4911
+ )}'. Key fields must have the same composite attribute definitions across all indexes they are involved with`,
4912
+ );
4913
+ }
4914
+
4915
+ const keyTemplatesMatch = sk.template === definition.template
4916
+
4917
+ if (!keyTemplatesMatch) {
4918
+ throw new e.ElectroError(
4919
+ e.ErrorCodes.IncompatibleKeyCompositeAttributeTemplate,
4920
+ `Sort Key (sk) on Access Pattern '${u.formatIndexNameForDisplay(
4921
+ accessPattern,
4922
+ )}' is defined with the template ${sk.template || '(undefined)'}, but the Access Pattern '${u.formatIndexNameForDisplay(
4923
+ definition.accessPattern,
4924
+ )}' 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`,
4925
+ );
4926
+ }
4927
+
4928
+ seenIndexFields[sk.field].push({ accessPattern, type: "sk" });
4929
+ } else {
4930
+ seenIndexFields[sk.field] = [];
4931
+ seenIndexFields[sk.field].push({ accessPattern, type: "sk" });
4932
+ }
4645
4933
  }
4646
4934
  }
4647
4935
 
@@ -4713,6 +5001,7 @@ class Entity {
4713
5001
  subCollections,
4714
5002
  indexHasSortKeys,
4715
5003
  clusteredIndexes,
5004
+ compositeIndexes,
4716
5005
  indexHasSubCollections,
4717
5006
  indexes: normalized,
4718
5007
  indexField: indexFieldTranslation,
@@ -4918,6 +5207,7 @@ class Entity {
4918
5207
  subCollections,
4919
5208
  indexCollection,
4920
5209
  clusteredIndexes,
5210
+ compositeIndexes,
4921
5211
  indexHasSortKeys,
4922
5212
  indexAccessPattern,
4923
5213
  indexHasSubCollections,
@@ -4991,6 +5281,7 @@ class Entity {
4991
5281
  modelVersion,
4992
5282
  subCollections,
4993
5283
  lookup: {
5284
+ compositeIndexes,
4994
5285
  clusteredIndexes,
4995
5286
  indexHasSortKeys,
4996
5287
  indexHasSubCollections,