electrodb 2.11.0 → 2.12.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/README.md CHANGED
@@ -46,10 +46,10 @@ _Please submit issues/feedback or reach out on Twitter [@tinkertamper](https://t
46
46
  - [**Easily Query Across Entities**](https://electrodb.dev/en/core-concepts/single-table-relationships) - Define "collections" to create powerful/idiomatic queries that return multiple entities in a single request.
47
47
  - [**Automatic Index Selection**](https://electrodb.dev/en/queries/find/) - Use `.find()` or `.match()` methods to dynamically and efficiently query based on defined sort key structures.
48
48
  - [**Simplified Pagination API**](https://electrodb.dev/en/queries/pagination/) - ElectroDB generates url safe cursors for pagination, allows for fine grain automated pagination, and supports async iteration.
49
- - [**TypeScript Support**](https://electrodb.dev/en/reference/typscript/) - Strong **TypeScript** support for both Entities and Services now in Beta.
49
+ - [**Strong TypeScript Inference**](https://electrodb.dev/en/reference/typescript/) - Strong **TypeScript** support for both Entities and Services now in Beta.
50
50
  - [**Query Directly via the Terminal**](https://github.com/tywalch/electrocli#query-taskapp) - Execute queries against your `Entities`, `Services`, `Models` directly from the command line.
51
51
  - [**Stand Up Rest Server for Entities**](https://github.com/tywalch/electrocli#query-taskapp) - Stand up a REST Server to interact with your `Entities`, `Services`, `Models` for easier prototyping.
52
- - [**Use with your existing tables**](https://electrodb.dev/en/core-concepts/use-electrodb-with-existing-table/) - ElectroDB simplifies building DocumentClient parameters, so you can use it with existing tables/data.
52
+ - [**Use with your existing tables**](https://electrodb.dev/en/recipes/use-electrodb-with-existing-table/) - ElectroDB simplifies building DocumentClient parameters, so you can use it with existing tables/data.
53
53
 
54
54
  ---
55
55
 
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
 
@@ -3658,6 +3758,7 @@ export interface Schema<A extends string, F extends string, C extends string> {
3658
3758
  readonly scope?: string;
3659
3759
  readonly type?: "clustered" | "isolated";
3660
3760
  readonly collection?: AccessPatternCollection<C>;
3761
+ readonly condition?: (composite: Record<string, unknown>) => boolean;
3661
3762
  readonly pk: {
3662
3763
  readonly casing?: "upper" | "lower" | "none" | "default";
3663
3764
  readonly field: string;
@@ -3813,51 +3914,53 @@ type PartialDefinedKeys<T> = {
3813
3914
  };
3814
3915
 
3815
3916
  export type ItemAttribute<A extends Attribute> =
3816
- A["type"] extends OpaquePrimitiveTypeName<infer T>
3817
- ? T
3818
- : A["type"] extends CustomAttributeTypeName<infer T>
3819
- ? T
3820
- : A["type"] extends infer R
3821
- ? R extends "string"
3822
- ? string
3823
- : R extends "number"
3824
- ? number
3825
- : R extends "boolean"
3826
- ? boolean
3827
- : R extends ReadonlyArray<infer E>
3828
- ? E
3829
- : R extends "map"
3830
- ? "properties" extends keyof A
3831
- ? {
3832
- [P in keyof A["properties"]]: A["properties"][P] extends infer M
3833
- ? M extends Attribute
3834
- ? ItemAttribute<M>
3835
- : never
3836
- : never;
3837
- }
3838
- : never
3839
- : R extends "list"
3840
- ? "items" extends keyof A
3841
- ? A["items"] extends infer I
3842
- ? I extends Attribute
3843
- ? Array<ItemAttribute<I>>
3917
+ A["type"] extends infer T
3918
+ ? T extends OpaquePrimitiveTypeName<infer OP>
3919
+ ? OP
3920
+ : T extends CustomAttributeTypeName<infer CA>
3921
+ ? CA
3922
+ : T extends infer R
3923
+ ? R extends "string"
3924
+ ? string
3925
+ : R extends "number"
3926
+ ? number
3927
+ : R extends "boolean"
3928
+ ? boolean
3929
+ : R extends ReadonlyArray<infer E>
3930
+ ? E
3931
+ : R extends "map"
3932
+ ? "properties" extends keyof A
3933
+ ? {
3934
+ [P in keyof A["properties"]]: A["properties"][P] extends infer M
3935
+ ? M extends Attribute
3936
+ ? ItemAttribute<M>
3937
+ : never
3938
+ : never;
3939
+ }
3940
+ : never
3941
+ : R extends "list"
3942
+ ? "items" extends keyof A
3943
+ ? A["items"] extends infer I
3944
+ ? I extends Attribute
3945
+ ? Array<ItemAttribute<I>>
3946
+ : never
3844
3947
  : never
3845
3948
  : never
3846
- : never
3847
- : R extends "set"
3848
- ? "items" extends keyof A
3849
- ? A["items"] extends infer I
3850
- ? I extends "string"
3851
- ? string[]
3852
- : I extends "number"
3853
- ? number[]
3854
- : I extends ReadonlyArray<infer ENUM>
3855
- ? ENUM[]
3949
+ : R extends "set"
3950
+ ? "items" extends keyof A
3951
+ ? A["items"] extends infer I
3952
+ ? I extends "string"
3953
+ ? string[]
3954
+ : I extends "number"
3955
+ ? number[]
3956
+ : I extends ReadonlyArray<infer ENUM>
3957
+ ? ENUM[]
3958
+ : never
3856
3959
  : never
3857
3960
  : never
3961
+ : R extends "any"
3962
+ ? any
3858
3963
  : never
3859
- : R extends "any"
3860
- ? any
3861
3964
  : never
3862
3965
  : never;
3863
3966
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrodb",
3
- "version": "2.11.0",
3
+ "version": "2.12.1",
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 =
@@ -3846,6 +3869,14 @@ class Entity {
3846
3869
  let indexType =
3847
3870
  typeof index.type === "string" ? index.type : IndexTypes.isolated;
3848
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
+
3849
3880
  if (indexType === "clustered") {
3850
3881
  clusteredIndexes.add(accessPattern);
3851
3882
  }
@@ -3953,6 +3984,7 @@ class Entity {
3953
3984
  type: indexType,
3954
3985
  index: indexName,
3955
3986
  scope: indexScope,
3987
+ condition: indexCondition,
3956
3988
  };
3957
3989
 
3958
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
  }
@@ -174,6 +174,11 @@ const Index = {
174
174
  enum: ["clustered", "isolated"],
175
175
  required: false,
176
176
  },
177
+ condition: {
178
+ type: "any",
179
+ required: false,
180
+ format: "isFunction",
181
+ }
177
182
  },
178
183
  };
179
184