electrodb 2.10.7 → 2.12.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/index.d.ts CHANGED
@@ -3216,6 +3216,27 @@ export interface MapAttribute {
3216
3216
  readonly watch?: ReadonlyArray<string> | "*";
3217
3217
  }
3218
3218
 
3219
+ export interface NestedCustomListAttribute {
3220
+ readonly type: "list";
3221
+ readonly items: CustomAttribute;
3222
+ readonly required?: boolean;
3223
+ readonly hidden?: boolean;
3224
+ readonly readOnly?: boolean;
3225
+ readonly get?: (
3226
+ val: Array<any>,
3227
+ item: any,
3228
+ ) => Array<string> | undefined | void;
3229
+ readonly set?: (
3230
+ val?: Array<any>,
3231
+ item?: any,
3232
+ ) => Array<any> | undefined | void;
3233
+ readonly default?: Array<any> | (() => Array<any>);
3234
+ readonly validate?:
3235
+ | ((val: Array<any>) => boolean)
3236
+ | ((val: Array<any>) => void)
3237
+ | ((val: Array<any>) => string | void);
3238
+ }
3239
+
3219
3240
  export interface NestedStringListAttribute {
3220
3241
  readonly type: "list";
3221
3242
  readonly items: {
@@ -3354,6 +3375,28 @@ export interface NestedMapListAttribute {
3354
3375
  readonly field?: string;
3355
3376
  }
3356
3377
 
3378
+ export interface NestedAnyListAttribute {
3379
+ readonly type: "list";
3380
+ readonly items: NestedAnyAttribute;
3381
+ readonly required?: boolean;
3382
+ readonly hidden?: boolean;
3383
+ readonly readOnly?: boolean;
3384
+ readonly get?: (
3385
+ val: Record<string, any>[],
3386
+ item: any,
3387
+ ) => Record<string, any>[] | undefined | void;
3388
+ readonly set?: (
3389
+ val?: Record<string, any>[],
3390
+ item?: any,
3391
+ ) => Record<string, any>[] | undefined | void;
3392
+ readonly default?: Record<string, any>[] | (() => Record<string, any>[]);
3393
+ readonly validate?:
3394
+ | ((val: Record<string, any>[]) => boolean)
3395
+ | ((val: Record<string, any>[]) => void)
3396
+ | ((val: Record<string, any>[]) => string | void);
3397
+ readonly field?: string;
3398
+ }
3399
+
3357
3400
  export interface MapListAttribute {
3358
3401
  readonly type: "list";
3359
3402
  readonly items: NestedMapAttribute;
@@ -3377,6 +3420,52 @@ export interface MapListAttribute {
3377
3420
  readonly watch?: ReadonlyArray<string> | "*";
3378
3421
  }
3379
3422
 
3423
+ export interface CustomListAttribute {
3424
+ readonly type: "list";
3425
+ readonly items: NestedCustomAttribute;
3426
+ readonly required?: boolean;
3427
+ readonly hidden?: boolean;
3428
+ readonly readOnly?: boolean;
3429
+ readonly get?: (
3430
+ val: Array<any>,
3431
+ item: any,
3432
+ ) => Array<any> | undefined | void;
3433
+ readonly set?: (
3434
+ val?: Array<any>,
3435
+ item?: any,
3436
+ ) => Array<any> | undefined | void;
3437
+ readonly default?: Array<any> | (() => Array<any>);
3438
+ readonly validate?:
3439
+ | ((val: Array<any>) => boolean)
3440
+ | ((val: Array<any>) => void)
3441
+ | ((val: Array<any>) => string | void);
3442
+ readonly field?: string;
3443
+ readonly watch?: ReadonlyArray<string> | "*";
3444
+ }
3445
+
3446
+ export interface AnyListAttribute {
3447
+ readonly type: "list";
3448
+ readonly items: NestedAnyAttribute;
3449
+ readonly required?: boolean;
3450
+ readonly hidden?: boolean;
3451
+ readonly readOnly?: boolean;
3452
+ readonly get?: (
3453
+ val: Array<any>,
3454
+ item: any,
3455
+ ) => Array<any> | undefined | void;
3456
+ readonly set?: (
3457
+ val?: Array<any>,
3458
+ item?: any,
3459
+ ) => Array<any> | undefined | void;
3460
+ readonly default?: Array<any> | (() => Array<any>);
3461
+ readonly validate?:
3462
+ | ((val: Array<any>) => boolean)
3463
+ | ((val: Array<any>) => void)
3464
+ | ((val: Array<any>) => string | void);
3465
+ readonly field?: string;
3466
+ readonly watch?: ReadonlyArray<string> | "*";
3467
+ }
3468
+
3380
3469
  export interface NestedStringSetAttribute {
3381
3470
  readonly type: "set";
3382
3471
  readonly items: "string";
@@ -3563,13 +3652,6 @@ export interface NumberSetAttribute {
3563
3652
  readonly watch?: ReadonlyArray<string> | "*";
3564
3653
  }
3565
3654
 
3566
- type StaticAttribute = {
3567
- readonly type: "static";
3568
- readonly hidden?: boolean;
3569
- value: string | number | boolean;
3570
- readonly field?: string;
3571
- };
3572
-
3573
3655
  type CustomAttributeTypeName<T> = { readonly [CustomAttributeSymbol]: T };
3574
3656
 
3575
3657
  type OpaquePrimitiveTypeName<T extends string | number | boolean> =
@@ -3597,6 +3679,21 @@ type CustomAttribute = {
3597
3679
  readonly watch?: ReadonlyArray<string> | "*";
3598
3680
  };
3599
3681
 
3682
+ type NestedCustomAttribute = {
3683
+ readonly type: CustomAttributeTypeName<any> | OpaquePrimitiveTypeName<any>;
3684
+ readonly required?: boolean;
3685
+ readonly hidden?: boolean;
3686
+ readonly readOnly?: boolean;
3687
+ readonly get?: (val: any, item: any) => any | undefined | void;
3688
+ readonly set?: (val?: any, item?: any) => any | undefined | void;
3689
+ readonly default?: any | (() => any);
3690
+ readonly validate?:
3691
+ | ((val: any) => boolean)
3692
+ | ((val: any) => void)
3693
+ | ((val: any) => string | void);
3694
+ readonly field?: string;
3695
+ };
3696
+
3600
3697
  export type Attribute =
3601
3698
  | BooleanAttribute
3602
3699
  | NumberAttribute
@@ -3609,7 +3706,8 @@ export type Attribute =
3609
3706
  | StringListAttribute
3610
3707
  | NumberListAttribute
3611
3708
  | MapListAttribute
3612
- | StaticAttribute
3709
+ | AnyListAttribute
3710
+ | CustomListAttribute
3613
3711
  | CustomAttribute
3614
3712
  | EnumNumberSetAttribute
3615
3713
  | EnumStringSetAttribute;
@@ -3621,12 +3719,14 @@ export type NestedAttributes =
3621
3719
  | NestedAnyAttribute
3622
3720
  | NestedMapAttribute
3623
3721
  | NestedStringListAttribute
3722
+ | NestedCustomListAttribute
3624
3723
  | NestedNumberListAttribute
3625
3724
  | NestedMapListAttribute
3725
+ | NestedAnyListAttribute
3626
3726
  | NestedStringSetAttribute
3627
3727
  | NestedNumberSetAttribute
3628
3728
  | NestedEnumAttribute
3629
- | NestedEnumAttribute
3729
+ | NestedCustomAttribute
3630
3730
  | NestedEnumNumberSetAttribute
3631
3731
  | NestedEnumStringSetAttribute;
3632
3732
 
@@ -3655,8 +3755,10 @@ export interface Schema<A extends string, F extends string, C extends string> {
3655
3755
  [accessPattern: string]: {
3656
3756
  readonly project?: "keys_only";
3657
3757
  readonly index?: string;
3758
+ readonly scope?: string;
3658
3759
  readonly type?: "clustered" | "isolated";
3659
3760
  readonly collection?: AccessPatternCollection<C>;
3761
+ readonly condition?: (composite: Record<string, unknown>) => boolean;
3660
3762
  readonly pk: {
3661
3763
  readonly casing?: "upper" | "lower" | "none" | "default";
3662
3764
  readonly field: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrodb",
3
- "version": "2.10.7",
3
+ "version": "2.12.0",
4
4
  "description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/entity.js CHANGED
@@ -2412,7 +2412,7 @@ class Entity {
2412
2412
  (!attribute || !attribute.indexes || attribute.indexes.length === 0)
2413
2413
  ) {
2414
2414
  /*
2415
- // this should be considered but is likely overkill at best and unexpected at worst.
2415
+ // this should be considered but is likely overkill at best and unexpected at worst.
2416
2416
  // It also is likely symbolic of a deeper issue. That said maybe it could be helpful
2417
2417
  // in the future? It is unclear, if this were added, whether this should get the
2418
2418
  // default value and then call the setter on the defaultValue. That would at least
@@ -2880,12 +2880,18 @@ class Entity {
2880
2880
  )}. If a composite attribute is readOnly and cannot be set, use the 'composite' chain method on update to supply the value for key formatting purposes.`,
2881
2881
  );
2882
2882
  }
2883
+
2883
2884
  return complete;
2884
2885
  }
2885
2886
 
2886
2887
  _makeKeysFromAttributes(indexes, attributes) {
2887
2888
  let indexKeys = {};
2888
2889
  for (let [index, keyTypes] of Object.entries(indexes)) {
2890
+ const shouldMakeKeys = this.model.indexes[this.model.translations.indexes.fromIndexToAccessPattern[index]].condition(attributes);
2891
+ if (!shouldMakeKeys) {
2892
+ continue;
2893
+ }
2894
+
2889
2895
  let keys = this._makeIndexKeys({
2890
2896
  index,
2891
2897
  pkAttributes: attributes,
@@ -2912,6 +2918,10 @@ class Entity {
2912
2918
  _makePutKeysFromAttributes(indexes, attributes) {
2913
2919
  let indexKeys = {};
2914
2920
  for (let index of indexes) {
2921
+ const shouldMakeKeys = this.model.indexes[this.model.translations.indexes.fromIndexToAccessPattern[index]].condition(attributes);
2922
+ if (!shouldMakeKeys) {
2923
+ continue;
2924
+ }
2915
2925
  indexKeys[index] = this._makeIndexKeys({
2916
2926
  index,
2917
2927
  pkAttributes: attributes,
@@ -2984,6 +2994,7 @@ class Entity {
2984
2994
  completeFacets.impactedIndexTypes,
2985
2995
  { ...set, ...keyAttributes },
2986
2996
  );
2997
+
2987
2998
  let updatedKeys = {};
2988
2999
  let deletedKeys = [];
2989
3000
  let indexKey = {};
@@ -3027,6 +3038,7 @@ class Entity {
3027
3038
  _getIndexImpact(attributes = {}, included = {}) {
3028
3039
  let includedFacets = Object.keys(included);
3029
3040
  let impactedIndexes = {};
3041
+ let skippedIndexes = new Set();
3030
3042
  let impactedIndexTypes = {};
3031
3043
  let completedIndexes = [];
3032
3044
  let facets = {};
@@ -3034,21 +3046,32 @@ class Entity {
3034
3046
  if (attributes[attribute] !== undefined) {
3035
3047
  facets[attribute] = attributes[attribute];
3036
3048
  indexes.forEach(({ index, type }) => {
3037
- impactedIndexes[index] = impactedIndexes[index] || {};
3038
- impactedIndexes[index][type] = impactedIndexes[index][type] || [];
3039
- impactedIndexes[index][type].push(attribute);
3040
- impactedIndexTypes[index] = impactedIndexTypes[index] || {};
3041
- impactedIndexTypes[index][type] =
3042
- this.model.translations.keys[index][type];
3049
+ impactedIndexes[index] = impactedIndexes[index] || {};
3050
+ impactedIndexes[index][type] = impactedIndexes[index][type] || [];
3051
+ impactedIndexes[index][type].push(attribute);
3052
+ impactedIndexTypes[index] = impactedIndexTypes[index] || {};
3053
+ impactedIndexTypes[index][type] =
3054
+ this.model.translations.keys[index][type];
3043
3055
  });
3044
3056
  }
3045
3057
  }
3046
3058
 
3059
+ for (const indexName in impactedIndexes) {
3060
+ const accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[indexName];
3061
+ const shouldMakeKeys = this.model.indexes[accessPattern].condition({ ...attributes, ...included });
3062
+ if (!shouldMakeKeys) {
3063
+ skippedIndexes.add(indexName);
3064
+ }
3065
+ }
3066
+
3047
3067
  let incomplete = Object.entries(this.model.facets.byIndex)
3048
3068
  .map(([index, { pk, sk }]) => {
3049
3069
  let impacted = impactedIndexes[index];
3050
- let impact = { index, missing: [] };
3051
- if (impacted) {
3070
+ let impact = {
3071
+ index,
3072
+ missing: []
3073
+ };
3074
+ if (impacted && !skippedIndexes.has(index)) {
3052
3075
  let missingPk =
3053
3076
  impacted[KeyTypes.pk] && impacted[KeyTypes.pk].length !== pk.length;
3054
3077
  let missingSk =
@@ -3222,6 +3245,9 @@ class Entity {
3222
3245
 
3223
3246
  // If keys are not custom, set the prefixes
3224
3247
  if (!keys.pk.isCustom) {
3248
+ if (tableIndex.scope) {
3249
+ pk = `${pk}_${tableIndex.scope}`;
3250
+ }
3225
3251
  keys.pk.prefix = u.formatKeyCasing(pk, tableIndex.pk.casing);
3226
3252
  }
3227
3253
 
@@ -3842,6 +3868,15 @@ class Entity {
3842
3868
  let indexName = index.index || TableIndex;
3843
3869
  let indexType =
3844
3870
  typeof index.type === "string" ? index.type : IndexTypes.isolated;
3871
+ let indexScope = index.scope || "";
3872
+ if (index.index === undefined && v.isFunction(index.condition)) {
3873
+ throw new e.ElectroError(
3874
+ e.ErrorCodes.InvalidIndexCondition,
3875
+ `The index option 'condition' is only allowed on secondary indexes`,
3876
+ );
3877
+ }
3878
+ let indexCondition = index.condition || (() => true);
3879
+
3845
3880
  if (indexType === "clustered") {
3846
3881
  clusteredIndexes.add(accessPattern);
3847
3882
  }
@@ -3940,14 +3975,16 @@ class Entity {
3940
3975
  }
3941
3976
  }
3942
3977
 
3943
- let definition = {
3978
+ let definition= {
3944
3979
  pk,
3945
3980
  sk,
3946
- collection,
3947
3981
  hasSk,
3982
+ collection,
3948
3983
  customFacets,
3949
- index: indexName,
3950
3984
  type: indexType,
3985
+ index: indexName,
3986
+ scope: indexScope,
3987
+ condition: indexCondition,
3951
3988
  };
3952
3989
 
3953
3990
  indexHasSubCollections[indexName] =
package/src/errors.js CHANGED
@@ -211,6 +211,12 @@ const ErrorCodes = {
211
211
  name: "DuplicateUpdateCompositesProvided",
212
212
  sym: ErrorCode,
213
213
  },
214
+ InvalidIndexCondition: {
215
+ code: 2011,
216
+ section: "invalid-index-option",
217
+ name: "InvalidIndexOption",
218
+ sym: ErrorCode,
219
+ },
214
220
  InvalidAttribute: {
215
221
  code: 3001,
216
222
  section: "invalid-attribute",
package/src/operations.js CHANGED
@@ -313,16 +313,13 @@ class AttributeOperationProxy {
313
313
  return AttributeOperationProxy.pathProxy(() => {
314
314
  const { commit, root, target, builder } = build();
315
315
  const attribute = target.getChild(prop);
316
+ const nestedAny = attribute.type === AttributeTypes.any &&
317
+ // if the name doesn't match that's because we are nested under 'any'
318
+ attribute.name !== prop;
316
319
  let field;
317
320
  if (attribute === undefined) {
318
- throw new Error(
319
- `Invalid attribute "${prop}" at path "${target.path}.${prop}"`,
320
- );
321
- } else if (
322
- attribute === root &&
323
- attribute.type === AttributeTypes.any
324
- ) {
325
- // This function is only called if a nested property is called. If this attribute is ultimately the root, don't use the root's field name
321
+ throw new Error(`Invalid attribute "${prop}" at path "${target.path}.${prop}"`);
322
+ } else if (nestedAny) {
326
323
  field = prop;
327
324
  } else {
328
325
  field = attribute.field;
@@ -331,6 +328,7 @@ class AttributeOperationProxy {
331
328
  return {
332
329
  root,
333
330
  builder,
331
+ nestedAny,
334
332
  target: attribute,
335
333
  commit: () => {
336
334
  const paths = commit();
package/src/schema.js CHANGED
@@ -1496,18 +1496,6 @@ class Schema {
1496
1496
  case AttributeTypes.set:
1497
1497
  normalized[name] = new SetAttribute(definition);
1498
1498
  break;
1499
- case AttributeTypes.any:
1500
- if (attributeHasParent) {
1501
- throw new e.ElectroError(
1502
- e.ErrorCodes.InvalidAttributeDefinition,
1503
- `Invalid attribute "${definition.name}" defined within "${
1504
- parent.parentPath
1505
- }". Attributes with type ${u.commaSeparatedString([
1506
- AttributeTypes.any,
1507
- AttributeTypes.custom,
1508
- ])} are only supported as root level attributes.`,
1509
- );
1510
- }
1511
1499
  default:
1512
1500
  normalized[name] = new Attribute(definition);
1513
1501
  }
package/src/service.js CHANGED
@@ -584,6 +584,7 @@ class Service {
584
584
  let pkFieldMatch = definition.pk.field === providedIndex.pk.field;
585
585
  let pkFacetLengthMatch =
586
586
  definition.pk.facets.length === providedIndex.pk.facets.length;
587
+ let scopeMatch = definition.scope === providedIndex.scope;
587
588
  let mismatchedFacetLabels = [];
588
589
  let collectionDifferences = [];
589
590
  let definitionIndexName = u.formatIndexNameForDisplay(definition.index);
@@ -631,6 +632,16 @@ class Service {
631
632
  }
632
633
  }
633
634
 
635
+ if (!scopeMatch) {
636
+ collectionDifferences.push(
637
+ `The index scope value provided "${
638
+ providedIndex.scope || "undefined"
639
+ }" does not match established index scope value "${
640
+ definition.scope || "undefined"
641
+ }" on index "${providedIndexName}". Index scope options must match across all entities participating in a collection`,
642
+ );
643
+ }
644
+
634
645
  if (!isCustomMatchPK) {
635
646
  collectionDifferences.push(
636
647
  `The usage of key templates the partition key on index ${definitionIndexName} must be consistent across all Entities, some entities provided use template while others do not`,
@@ -119,6 +119,10 @@ const Index = {
119
119
  enum: ["string", "number"],
120
120
  required: false,
121
121
  },
122
+ scope: {
123
+ type: "string",
124
+ required: false,
125
+ }
122
126
  },
123
127
  },
124
128
  sk: {
@@ -170,6 +174,11 @@ const Index = {
170
174
  enum: ["clustered", "isolated"],
171
175
  required: false,
172
176
  },
177
+ condition: {
178
+ type: "any",
179
+ required: false,
180
+ format: "isFunction",
181
+ }
173
182
  },
174
183
  };
175
184