electrodb 2.7.1 → 2.8.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
@@ -1780,6 +1780,8 @@ export interface IndexWithSortKey {
1780
1780
 
1781
1781
  export type AccessPatternCollection<C extends string> = C | ReadonlyArray<C>;
1782
1782
 
1783
+ export type KeyCastOption = 'string' | 'number'
1784
+
1783
1785
  export interface Schema<A extends string, F extends string, C extends string> {
1784
1786
  readonly model: {
1785
1787
  readonly entity: string;
@@ -1800,12 +1802,14 @@ export interface Schema<A extends string, F extends string, C extends string> {
1800
1802
  readonly field: string;
1801
1803
  readonly composite: ReadonlyArray<F>;
1802
1804
  readonly template?: string;
1805
+ readonly cast?: KeyCastOption;
1803
1806
  }
1804
1807
  readonly sk?: {
1805
1808
  readonly casing?: "upper" | "lower" | "none" | "default";
1806
1809
  readonly field: string;
1807
1810
  readonly composite: ReadonlyArray<F>;
1808
1811
  readonly template?: string;
1812
+ readonly cast?: KeyCastOption;
1809
1813
  }
1810
1814
  }
1811
1815
  }
package/package.json CHANGED
@@ -1,19 +1,24 @@
1
1
  {
2
2
  "name": "electrodb",
3
- "version": "2.7.1",
3
+ "version": "2.8.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": {
7
- "test": "npm run test:types && npm run test:init && npm run test:unit",
8
- "test:init": "LOCAL_DYNAMO_ENDPOINT=http://localhost:8000 node ./test/init.js",
9
- "test:unit": "LOCAL_DYNAMO_ENDPOINT=http://localhost:8000 mocha -r ts-node/register ./test/**.spec.*",
7
+ "test:run": "npm run test:types && npm run test:init && npm run test:unit",
8
+ "test:init": "node ./test/init.js",
9
+ "test:unit": "mocha -r ts-node/register ./test/**.spec.*",
10
10
  "test:types": "tsd",
11
11
  "coverage": "npm run test:init && nyc npm run test:unit && nyc report --reporter=text-lcov | coveralls",
12
12
  "coverage:local:coveralls": "npm run test:init && nyc npm run test:unit && nyc report --reporter=text-lcov | coveralls",
13
13
  "coverage:local:html": "npm run test:init && nyc npm run test:unit && nyc report --reporter=html",
14
14
  "build:browser": "browserify playground/browser.js -o playground/bundle.js",
15
15
  "build": "sh buildbrowser.sh",
16
- "start:containers": "docker compose up -d"
16
+ "test": "./test.sh",
17
+ "test:ci": "npm install && npm test",
18
+ "ddb:start": "docker compose up -d",
19
+ "ddb:load": "docker compose exec electro npm run test:init",
20
+ "ddb:stop": "docker compose stop",
21
+ "example:taskapp": "npm run ddb:start && npm run ddb:load && ts-node ./examples/taskmanager/src/index.ts; npm run ddb:stop"
17
22
  },
18
23
  "repository": {
19
24
  "type": "git",
package/src/entity.js CHANGED
@@ -26,6 +26,7 @@ const {
26
26
  PartialComparisons,
27
27
  MethodTypeTranslation,
28
28
  TransactionCommitSymbol,
29
+ CastKeyOptions,
29
30
  } = require("./types");
30
31
  const { FilterFactory } = require("./filters");
31
32
  const { FilterOperations } = require("./operations");
@@ -1279,7 +1280,7 @@ class Entity {
1279
1280
  }
1280
1281
  indexParts = { ...indexParts, ...tableIndexParts };
1281
1282
  }
1282
- let noPartsFound = Object.keys(indexParts).length === 0;
1283
+ let noPartsFound = Object.keys(indexParts).length === 0 && this.model.facets.byIndex[tableIndex].all.length > 0;
1283
1284
  let partsAreIncomplete = this.model.facets.byIndex[tableIndex].all.find(facet => indexParts[facet.name] === undefined);
1284
1285
  if (noPartsFound || partsAreIncomplete) {
1285
1286
  // In this case no suitable record could be found be the deconstructed pager.
@@ -2592,7 +2593,8 @@ class Entity {
2592
2593
  version = "1",
2593
2594
  tableIndex,
2594
2595
  modelVersion,
2595
- isClustered
2596
+ isClustered,
2597
+ schema
2596
2598
  }) {
2597
2599
  /*
2598
2600
  Collections will prefix the sort key so they can be queried with
@@ -2608,12 +2610,14 @@ class Entity {
2608
2610
  field: tableIndex.pk.field,
2609
2611
  casing: tableIndex.pk.casing,
2610
2612
  isCustom: tableIndex.customFacets.pk,
2613
+ cast: tableIndex.pk.cast,
2611
2614
  },
2612
2615
  sk: {
2613
2616
  prefix: "",
2614
2617
  casing: tableIndex.sk.casing,
2615
2618
  isCustom: tableIndex.customFacets.sk,
2616
2619
  field: tableIndex.sk ? tableIndex.sk.field : undefined,
2620
+ cast: tableIndex.sk ? tableIndex.sk.cast : undefined,
2617
2621
  }
2618
2622
  };
2619
2623
 
@@ -2652,7 +2656,7 @@ class Entity {
2652
2656
  }
2653
2657
  }
2654
2658
 
2655
- // If keys arent custom, set the prefixes
2659
+ // If keys are not custom, set the prefixes
2656
2660
  if (!keys.pk.isCustom) {
2657
2661
  keys.pk.prefix = u.formatKeyCasing(pk, tableIndex.pk.casing);
2658
2662
  }
@@ -2662,6 +2666,40 @@ class Entity {
2662
2666
  keys.sk.postfix = u.formatKeyCasing(postfix, tableIndex.sk.casing);
2663
2667
  }
2664
2668
 
2669
+ const castKeys = tableIndex.hasSk
2670
+ ? [tableIndex.pk, tableIndex.sk]
2671
+ : [tableIndex.pk];
2672
+
2673
+ for (const castKey of castKeys) {
2674
+ if (castKey.cast === CastKeyOptions.string) {
2675
+ keys[castKey.type].cast = CastKeyOptions.string;
2676
+ } else if (
2677
+ // custom keys with only one facet and no labels are numeric by default
2678
+ castKey.cast === undefined &&
2679
+ castKey.isCustom &&
2680
+ castKey.facets.length === 1 &&
2681
+ castKey.facetLabels.every(({label}) => !label) &&
2682
+ schema.attributes[castKey.facets[0]] &&
2683
+ schema.attributes[castKey.facets[0]].type === 'number'
2684
+ ) {
2685
+ keys[castKey.type].cast = CastKeyOptions.number;
2686
+ } else if (
2687
+ castKey.cast === CastKeyOptions.number &&
2688
+ castKey.facets.length === 1 &&
2689
+ schema.attributes[castKey.facets[0]] &&
2690
+ ['number', 'string', 'boolean'].includes(schema.attributes[castKey.facets[0]].type)
2691
+ ) {
2692
+ keys[castKey.type].cast = CastKeyOptions.number;
2693
+ } else if (
2694
+ castKey.cast === CastKeyOptions.number &&
2695
+ castKey.facets.length > 1
2696
+ ) {
2697
+ throw new e.ElectroError(e.ErrorCodes.InvalidModel, `Invalid "cast" option provided for ${castKey.type} definition on index "${u.formatIndexNameForDisplay(tableIndex.index)}". Keys can only be cast to 'number' if they are a composite of one numeric attribute.`);
2698
+ } else {
2699
+ keys[castKey.type].cast = CastKeyOptions.string;
2700
+ }
2701
+ }
2702
+
2665
2703
  return keys;
2666
2704
  }
2667
2705
 
@@ -2743,7 +2781,12 @@ class Entity {
2743
2781
  throw new Error(`Invalid index: ${index}`);
2744
2782
  }
2745
2783
  // let partitionKey = this._makeKey(prefixes.pk, facets.pk, pkFacets, this.model.facets.labels[index].pk, { excludeLabelTail: true });
2746
- let partitionKey = this._makeKey(prefixes.pk, facets.pk, pkFacets, this.model.facets.labels[index].pk);
2784
+ let partitionKey = this._makeKey(
2785
+ prefixes.pk,
2786
+ facets.pk,
2787
+ pkFacets,
2788
+ this.model.facets.labels[index].pk,
2789
+ );
2747
2790
  let pk = partitionKey.key;
2748
2791
  let sk = [];
2749
2792
  let fulfilled = false;
@@ -2822,21 +2865,45 @@ class Entity {
2822
2865
  };
2823
2866
  }
2824
2867
 
2825
- _isNumericKey(isCustom, facets = [], labels = []) {
2826
- let attribute = this.model.schema.attributes[facets[0]];
2827
- let isSingleComposite = facets.length === 1;
2828
- let hasNoLabels = isCustom && labels.every(({label}) => !label);
2829
- let facetIsNonStringPrimitive = attribute && attribute.type === "number";
2830
- return isCustom && isSingleComposite && hasNoLabels && facetIsNonStringPrimitive
2868
+ _formatNumericCastKey(attributeName, key) {
2869
+ const fulfilled = key !== undefined;
2870
+ if (!fulfilled) {
2871
+ return {
2872
+ fulfilled,
2873
+ key,
2874
+ }
2875
+ }
2876
+ if (typeof key === 'number') {
2877
+ return {
2878
+ fulfilled,
2879
+ key,
2880
+ }
2881
+ }
2882
+ if (typeof key === 'string') {
2883
+ const parsed = parseInt(key);
2884
+ if (!isNaN(parsed)) {
2885
+ return {
2886
+ fulfilled,
2887
+ key: parsed,
2888
+ }
2889
+ }
2890
+ }
2891
+
2892
+ if (typeof key === 'boolean') {
2893
+ return {
2894
+ fulfilled,
2895
+ key: key === true ? 1 : 0,
2896
+ }
2897
+ }
2898
+
2899
+ throw new e.ElectroAttributeValidationError(attributeName, `Invalid key value provided, could not cast composite attribute ${attributeName} to number for index`);
2831
2900
  }
2832
2901
 
2902
+
2833
2903
  /* istanbul ignore next */
2834
- _makeKey({prefix, isCustom, casing, postfix} = {}, facets = [], supplied = {}, labels = [], {excludeLabelTail, excludePostfix, transform = (val) => val} = {}) {
2835
- if (this._isNumericKey(isCustom, facets, labels)) {
2836
- return {
2837
- fulfilled: supplied[facets[0]] !== undefined,
2838
- key: supplied[facets[0]],
2839
- };
2904
+ _makeKey({prefix, isCustom, casing, postfix, cast} = {}, facets = [], supplied = {}, labels = [], {excludeLabelTail, excludePostfix, transform = (val) => val} = {}) {
2905
+ if (cast === CastKeyOptions.number) {
2906
+ return this._formatNumericCastKey(facets[0], supplied[facets[0]]);
2840
2907
  }
2841
2908
 
2842
2909
  let key = prefix;
@@ -3153,7 +3220,6 @@ class Entity {
3153
3220
  };
3154
3221
  let seenIndexes = {};
3155
3222
  let seenIndexFields = {};
3156
-
3157
3223
  let accessPatterns = Object.keys(indexes);
3158
3224
 
3159
3225
  for (let i in accessPatterns) {
@@ -3202,6 +3268,7 @@ class Entity {
3202
3268
  index: indexName,
3203
3269
  casing: pkCasing,
3204
3270
  type: KeyTypes.pk,
3271
+ cast: index.pk.cast,
3205
3272
  field: index.pk.field || "",
3206
3273
  facets: parsedPKAttributes.attributes,
3207
3274
  isCustom: parsedPKAttributes.isCustom,
@@ -3219,6 +3286,7 @@ class Entity {
3219
3286
  index: indexName,
3220
3287
  casing: skCasing,
3221
3288
  type: KeyTypes.sk,
3289
+ cast: index.sk.cast,
3222
3290
  field: index.sk.field || "",
3223
3291
  facets: parsedSKAttributes.attributes,
3224
3292
  isCustom: parsedSKAttributes.isCustom,
@@ -3410,7 +3478,7 @@ class Entity {
3410
3478
  return normalized;
3411
3479
  }
3412
3480
 
3413
- _normalizeKeyFixings({service, entity, version, indexes, modelVersion, clusteredIndexes}) {
3481
+ _normalizeKeyFixings({service, entity, version, indexes, modelVersion, clusteredIndexes, schema}) {
3414
3482
  let prefixes = {};
3415
3483
  for (let accessPattern of Object.keys(indexes)) {
3416
3484
  let tableIndex = indexes[accessPattern];
@@ -3421,6 +3489,7 @@ class Entity {
3421
3489
  tableIndex,
3422
3490
  modelVersion,
3423
3491
  isClustered: clusteredIndexes.has(accessPattern),
3492
+ schema,
3424
3493
  });
3425
3494
  }
3426
3495
  return prefixes;
@@ -3569,17 +3638,17 @@ class Entity {
3569
3638
  let schema = new Schema(model.attributes, facets, {client, isRoot: true});
3570
3639
  let filters = this._normalizeFilters(model.filters);
3571
3640
  // todo: consider a rename
3572
- let prefixes = this._normalizeKeyFixings({service, entity, version, indexes, modelVersion, clusteredIndexes});
3641
+ let prefixes = this._normalizeKeyFixings({service, entity, version, indexes, modelVersion, clusteredIndexes, schema});
3573
3642
 
3574
3643
  // apply model defined labels
3575
3644
  let schemaDefinedLabels = schema.getLabels();
3645
+ const deconstructors = {};
3576
3646
  facets.labels = this._mergeKeyDefinitions(facets.labels, schemaDefinedLabels);
3577
3647
  for (let indexName of Object.keys(facets.labels)) {
3578
- indexes[indexAccessPattern.fromIndexToAccessPattern[indexName]].pk.labels = facets.labels[indexName].pk;
3579
- indexes[indexAccessPattern.fromIndexToAccessPattern[indexName]].sk.labels = facets.labels[indexName].sk;
3580
- }
3581
- const deconstructors = {};
3582
- for (const indexName of Object.keys(facets.labels)) {
3648
+ const accessPattern = indexAccessPattern.fromIndexToAccessPattern[indexName];
3649
+ indexes[accessPattern].pk.labels = facets.labels[indexName].pk;
3650
+ indexes[accessPattern].sk.labels = facets.labels[indexName].sk;
3651
+
3583
3652
  const keyTypes = prefixes[indexName] || {};
3584
3653
  deconstructors[indexName] = {};
3585
3654
  for (const keyType in keyTypes) {
package/src/errors.js CHANGED
@@ -148,7 +148,7 @@ const ErrorCodes = {
148
148
  InconsistentIndexDefinition: {
149
149
  code: 1022,
150
150
  section: "inconsistent-index-definition",
151
- name: "InvalidClientProvided",
151
+ name: "Inconsistent Index Definition",
152
152
  sym: ErrorCode,
153
153
  },
154
154
  MissingAttribute: {
package/src/schema.js CHANGED
@@ -542,8 +542,9 @@ class MapAttribute extends Attribute {
542
542
  const getter = get || ((val) => {
543
543
  const isEmpty = !val || Object.keys(val).length === 0;
544
544
  const isNotRequired = !this.required;
545
+ const doesNotHaveDefault = this.default === undefined;
545
546
  const isRoot = this.isRoot;
546
- if (isEmpty && isRoot && !isNotRequired) {
547
+ if (isEmpty && isRoot && isNotRequired && doesNotHaveDefault) {
547
548
  return undefined;
548
549
  }
549
550
  return val;
@@ -582,11 +583,16 @@ class MapAttribute extends Attribute {
582
583
  const setter = set || ((val) => {
583
584
  const isEmpty = !val || Object.keys(val).length === 0;
584
585
  const isNotRequired = !this.required;
586
+ const doesNotHaveDefault = this.default === undefined;
587
+ const defaultIsValue = this.default === val;
585
588
  const isRoot = this.isRoot;
586
- if (isEmpty && isRoot && !isNotRequired) {
589
+ if (defaultIsValue) {
590
+ return val;
591
+ } else if (isEmpty && isRoot && isNotRequired && doesNotHaveDefault) {
587
592
  return undefined;
593
+ } else {
594
+ return val;
588
595
  }
589
- return val;
590
596
  });
591
597
 
592
598
  return (values, siblings) => {
@@ -1113,7 +1119,7 @@ class Schema {
1113
1119
  prefix,
1114
1120
  postfix,
1115
1121
  traverser,
1116
- isKeyField,
1122
+ isKeyField: isKeyField || isKey,
1117
1123
  isRoot: !!isRoot,
1118
1124
  label: attribute.label,
1119
1125
  required: !!attribute.required,
package/src/types.js CHANGED
@@ -312,6 +312,11 @@ const DynamoDBAttributeTypes = Object.entries({
312
312
  return obj;
313
313
  }, {});
314
314
 
315
+ const CastKeyOptions = {
316
+ string: 'string',
317
+ number: 'number',
318
+ }
319
+
315
320
  module.exports = {
316
321
  Pager,
317
322
  KeyTypes,
@@ -331,6 +336,7 @@ module.exports = {
331
336
  ItemOperations,
332
337
  AttributeTypes,
333
338
  EntityVersions,
339
+ CastKeyOptions,
334
340
  ServiceVersions,
335
341
  ExpressionTypes,
336
342
  ElectroInstance,
@@ -113,6 +113,11 @@ const Index = {
113
113
  type: "string",
114
114
  enum: ["upper", "lower", "none", "default"],
115
115
  required: false,
116
+ },
117
+ cast: {
118
+ type: "string",
119
+ enum: ["string", "number"],
120
+ required: false,
116
121
  }
117
122
  },
118
123
  },
@@ -146,6 +151,11 @@ const Index = {
146
151
  type: "string",
147
152
  enum: ["upper", "lower", "none", "default"],
148
153
  required: false,
154
+ },
155
+ cast: {
156
+ type: "string",
157
+ enum: ["string", "number"],
158
+ required: false,
149
159
  }
150
160
  },
151
161
  },
package/output DELETED
@@ -1,168 +0,0 @@
1
- "target": "top",
2
- "target": "byOrganization",
3
- "target": "byCategory",
4
- "target": "top",
5
- "target": "byOrganization",
6
- "target": "byCategory",
7
- "target": "top",
8
- "target": "byOrganization",
9
- "target": "byCategory",
10
- "target": "top",
11
- "target": "byOrganization",
12
- "target": "byCategory",
13
- "target": "top",
14
- "target": "byOrganization",
15
- "target": "byCategory",
16
- "target": "top",
17
- "target": "byOrganization",
18
- "target": "byCategory",
19
- "target": "top",
20
- "target": "byOrganization",
21
- "target": "byCategory",
22
- "target": "top",
23
- "target": "byOrganization",
24
- "target": "byCategory",
25
- "target": "top",
26
- "target": "byOrganization",
27
- "target": "byCategory",
28
- "target": "top",
29
- "target": "byOrganization",
30
- "target": "byCategory",
31
- "target": "top",
32
- "target": "byOrganization",
33
- "target": "byCategory",
34
- "target": "top",
35
- "target": "byOrganization",
36
- "target": "byCategory",
37
- "target": "top",
38
- "target": "byOrganization",
39
- "target": "byCategory",
40
- "target": "top",
41
- "target": "byOrganization",
42
- "target": "byCategory",
43
- "target": "top",
44
- "target": "byOrganization",
45
- "target": "byCategory",
46
- "target": "top",
47
- "target": "byOrganization",
48
- "target": "byCategory",
49
- "target": "top",
50
- "target": "byOrganization",
51
- "target": "byCategory",
52
- "target": "top",
53
- "target": "byOrganization",
54
- "target": "byCategory",
55
- "target": "top",
56
- "target": "byOrganization",
57
- "target": "byCategory",
58
- "target": "top",
59
- "target": "byOrganization",
60
- "target": "byCategory",
61
- "target": "top",
62
- "target": "byOrganization",
63
- "target": "byCategory",
64
- "target": "top",
65
- "target": "byOrganization",
66
- "target": "byCategory",
67
- "target": "top",
68
- "target": "byOrganization",
69
- "target": "byCategory",
70
- "target": "top",
71
- "target": "byOrganization",
72
- "target": "byCategory",
73
- "target": "top",
74
- "target": "byOrganization",
75
- "target": "byCategory",
76
- "target": "top",
77
- "target": "byOrganization",
78
- "target": "byCategory",
79
- "target": "top",
80
- "target": "byOrganization",
81
- "target": "byCategory",
82
- "target": "top",
83
- "target": "byOrganization",
84
- "target": "byCategory",
85
- "target": "top",
86
- "target": "byOrganization",
87
- "target": "byCategory",
88
- "target": "top",
89
- "target": "byOrganization",
90
- "target": "byCategory",
91
- "target": "top",
92
- "target": "byOrganization",
93
- "target": "byCategory",
94
- "target": "top",
95
- "target": "byOrganization",
96
- "target": "byCategory",
97
- "target": "top",
98
- "target": "byOrganization",
99
- "target": "byCategory",
100
- "target": "top",
101
- "target": "byOrganization",
102
- "target": "byCategory",
103
- "target": "top",
104
- "target": "byOrganization",
105
- "target": "byCategory",
106
- "target": "top",
107
- "target": "byOrganization",
108
- "target": "byCategory",
109
- "target": "top",
110
- "target": "byOrganization",
111
- "target": "byCategory",
112
- "target": "top",
113
- "target": "byOrganization",
114
- "target": "byCategory",
115
- "target": "top",
116
- "target": "byOrganization",
117
- "target": "byCategory",
118
- "target": "top",
119
- "target": "byOrganization",
120
- "target": "byCategory",
121
- "target": "top",
122
- "target": "byOrganization",
123
- "target": "byCategory",
124
- "target": "top",
125
- "target": "byOrganization",
126
- "target": "byCategory",
127
- "target": "top",
128
- "target": "byOrganization",
129
- "target": "byCategory",
130
- "target": "top",
131
- "target": "byOrganization",
132
- "target": "byCategory",
133
- "target": "top",
134
- "target": "byOrganization",
135
- "target": "byCategory",
136
- "target": "top",
137
- "target": "byOrganization",
138
- "target": "byCategory",
139
- "target": "top",
140
- "target": "byOrganization",
141
- "target": "byCategory",
142
- "target": "top",
143
- "target": "byOrganization",
144
- "target": "byCategory",
145
- "target": "top",
146
- "target": "byOrganization",
147
- "target": "byCategory",
148
- "target": "top",
149
- "target": "byOrganization",
150
- "target": "byCategory",
151
- "target": "top",
152
- "target": "byOrganization",
153
- "target": "byCategory",
154
- "target": "top",
155
- "target": "byOrganization",
156
- "target": "byCategory",
157
- "target": "top",
158
- "target": "byOrganization",
159
- "target": "byCategory",
160
- "target": "top",
161
- "target": "byOrganization",
162
- "target": "byCategory",
163
- "target": "top",
164
- "target": "byOrganization",
165
- "target": "byCategory",
166
- "target": "top",
167
- "target": "byOrganization",
168
- "target": "byCategory",