electrodb 2.13.0 → 2.14.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
@@ -3967,65 +3967,67 @@ export type ItemAttribute<A extends Attribute> =
3967
3967
  : never;
3968
3968
 
3969
3969
  export type ReturnedAttribute<A extends Attribute> =
3970
- A["type"] extends OpaquePrimitiveTypeName<infer T>
3971
- ? T
3972
- : A["type"] extends CustomAttributeTypeName<infer T>
3973
- ? T
3974
- : A["type"] extends infer R
3975
- ? R extends "static"
3976
- ? never
3977
- : R extends "string"
3978
- ? string
3979
- : R extends "number"
3980
- ? number
3981
- : R extends "boolean"
3982
- ? boolean
3983
- : R extends ReadonlyArray<infer E>
3984
- ? E
3985
- : R extends "map"
3986
- ? "properties" extends keyof A
3987
- ? {
3988
- [P in keyof A["properties"] as A["properties"][P] extends RequiredAttribute
3989
- ? P
3990
- : never]: A["properties"][P] extends infer M
3991
- ? M extends Attribute
3992
- ? ReturnedAttribute<M>
3993
- : never
3994
- : never;
3995
- } & {
3996
- [P in keyof A["properties"] as A["properties"][P] extends
3997
- | HiddenAttribute
3998
- | RequiredAttribute
3999
- ? never
4000
- : P]?: A["properties"][P] extends infer M
4001
- ? M extends Attribute
4002
- ? ReturnedAttribute<M> | undefined
4003
- : never
4004
- : never;
4005
- }
4006
- : never
4007
- : R extends "list"
4008
- ? "items" extends keyof A
4009
- ? A["items"] extends infer I
4010
- ? I extends Attribute
4011
- ? ReturnedAttribute<I>[]
3970
+ A["type"] extends infer T
3971
+ ? T extends OpaquePrimitiveTypeName<infer OP>
3972
+ ? OP
3973
+ : T extends CustomAttributeTypeName<infer CA>
3974
+ ? CA
3975
+ : T extends infer R
3976
+ ? R extends "static"
3977
+ ? never
3978
+ : R extends "string"
3979
+ ? string
3980
+ : R extends "number"
3981
+ ? number
3982
+ : R extends "boolean"
3983
+ ? boolean
3984
+ : R extends ReadonlyArray<infer E>
3985
+ ? E
3986
+ : R extends "map"
3987
+ ? "properties" extends keyof A
3988
+ ? {
3989
+ [P in keyof A["properties"] as A["properties"][P] extends RequiredAttribute
3990
+ ? P
3991
+ : never]: A["properties"][P] extends infer M
3992
+ ? M extends Attribute
3993
+ ? ReturnedAttribute<M>
3994
+ : never
3995
+ : never;
3996
+ } & {
3997
+ [P in keyof A["properties"] as A["properties"][P] extends
3998
+ | HiddenAttribute
3999
+ | RequiredAttribute
4000
+ ? never
4001
+ : P]?: A["properties"][P] extends infer M
4002
+ ? M extends Attribute
4003
+ ? ReturnedAttribute<M> | undefined
4004
+ : never
4005
+ : never;
4006
+ }
4007
+ : never
4008
+ : R extends "list"
4009
+ ? "items" extends keyof A
4010
+ ? A["items"] extends infer I
4011
+ ? I extends Attribute
4012
+ ? ReturnedAttribute<I>[]
4013
+ : never
4012
4014
  : never
4013
4015
  : never
4014
- : never
4015
- : R extends "set"
4016
- ? "items" extends keyof A
4017
- ? A["items"] extends infer I
4018
- ? I extends "string"
4019
- ? string[]
4020
- : I extends "number"
4021
- ? number[]
4022
- : I extends ReadonlyArray<infer ENUM>
4023
- ? ENUM[]
4016
+ : R extends "set"
4017
+ ? "items" extends keyof A
4018
+ ? A["items"] extends infer I
4019
+ ? I extends "string"
4020
+ ? string[]
4021
+ : I extends "number"
4022
+ ? number[]
4023
+ : I extends ReadonlyArray<infer ENUM>
4024
+ ? ENUM[]
4025
+ : never
4024
4026
  : never
4025
4027
  : never
4028
+ : R extends "any"
4029
+ ? any
4026
4030
  : never
4027
- : R extends "any"
4028
- ? any
4029
4031
  : never
4030
4032
  : never;
4031
4033
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrodb",
3
- "version": "2.13.0",
3
+ "version": "2.14.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/clauses.js CHANGED
@@ -432,12 +432,16 @@ let clauses = {
432
432
  const { pk } = state.query.keys;
433
433
  const sk = state.query.keys.sk[0];
434
434
 
435
- const { updatedKeys, setAttributes, indexKey } = entity._getPutKeys(
435
+ const { updatedKeys, setAttributes, indexKey, deletedKeys = [] } = entity._getPutKeys(
436
436
  pk,
437
437
  sk && sk.facets,
438
438
  onlySetAppliedData,
439
439
  );
440
440
 
441
+ for (const deletedKey of deletedKeys) {
442
+ state.query.update.remove(deletedKey)
443
+ }
444
+
441
445
  // calculated here but needs to be used when building the params
442
446
  upsert.indexKey = indexKey;
443
447
 
package/src/entity.js CHANGED
@@ -39,6 +39,11 @@ const u = require("./util");
39
39
  const e = require("./errors");
40
40
  const v = require("./validations");
41
41
 
42
+ const ImpactedIndexTypeSource = {
43
+ composite: 'composite',
44
+ provided: 'provided',
45
+ }
46
+
42
47
  class Entity {
43
48
  constructor(model, config = {}) {
44
49
  config = c.normalizeConfig(config);
@@ -2354,14 +2359,13 @@ class Entity {
2354
2359
  // change, and we also don't want to trigger the setters of any attributes watching these facets because that
2355
2360
  // should only happen when an attribute is changed.
2356
2361
  const attributesAndComposites = {
2357
- ...update.composites,
2358
2362
  ...preparedUpdateValues,
2359
2363
  };
2360
2364
  const {
2361
2365
  indexKey,
2362
2366
  updatedKeys,
2363
2367
  deletedKeys = [],
2364
- } = this._getUpdatedKeys(pk, sk, attributesAndComposites, removed);
2368
+ } = this._getUpdatedKeys(pk, sk, attributesAndComposites, removed, update.composites);
2365
2369
  const accessPattern =
2366
2370
  this.model.translations.indexes.fromIndexToAccessPattern[TableIndex];
2367
2371
  for (const path of Object.keys(preparedUpdateValues)) {
@@ -2926,11 +2930,13 @@ class Entity {
2926
2930
  return params;
2927
2931
  }
2928
2932
 
2929
- _expectIndexFacets(attributes, facets) {
2933
+ _expectIndexFacets(attributes, facets, { utilizeIncludedOnlyIndexes, skipConditionCheck } = {}) {
2930
2934
  let [isIncomplete, { incomplete, complete }] = this._getIndexImpact(
2931
2935
  attributes,
2932
2936
  facets,
2937
+ { utilizeIncludedOnlyIndexes, skipConditionCheck },
2933
2938
  );
2939
+
2934
2940
  if (isIncomplete) {
2935
2941
  let incompleteAccessPatterns = incomplete.map(
2936
2942
  ({ index }) =>
@@ -2940,6 +2946,7 @@ class Entity {
2940
2946
  (result, { missing }) => [...result, ...missing],
2941
2947
  [],
2942
2948
  );
2949
+
2943
2950
  throw new e.ElectroError(
2944
2951
  e.ErrorCodes.IncompleteCompositeAttributes,
2945
2952
  `Incomplete composite attributes: Without the composite attributes ${u.commaSeparatedString(
@@ -2953,11 +2960,11 @@ class Entity {
2953
2960
  return complete;
2954
2961
  }
2955
2962
 
2956
- _makeKeysFromAttributes(indexes, attributes) {
2963
+ _makeKeysFromAttributes(indexes, attributes, conditions) {
2957
2964
  let indexKeys = {};
2958
2965
  for (let [index, keyTypes] of Object.entries(indexes)) {
2959
- const shouldMakeKeys = this.model.indexes[this.model.translations.indexes.fromIndexToAccessPattern[index]].condition(attributes);
2960
- if (!shouldMakeKeys) {
2966
+ const shouldMakeKeys = !this._indexConditionIsDefined(index) || conditions[index];
2967
+ if (!shouldMakeKeys && index !== TableIndex) {
2961
2968
  continue;
2962
2969
  }
2963
2970
 
@@ -3008,8 +3015,19 @@ class Entity {
3008
3015
  let completeFacets = this._expectIndexFacets(
3009
3016
  { ...setAttributes, ...validationAssistance },
3010
3017
  { ...keyAttributes },
3018
+ { set },
3011
3019
  );
3012
3020
 
3021
+ let deletedKeys = [];
3022
+ for (const [indexName, condition] of Object.entries(completeFacets.conditions)) {
3023
+ if (!condition) {
3024
+ deletedKeys.push(this.model.translations.keys[indexName][KeyTypes.pk]);
3025
+ if (this.model.translations.keys[indexName][KeyTypes.sk]) {
3026
+ deletedKeys.push(this.model.translations.keys[indexName][KeyTypes.sk]);
3027
+ }
3028
+ }
3029
+ }
3030
+
3013
3031
  // complete facets, only includes impacted facets which likely does not include the updateIndex which then needs to be added here.
3014
3032
  if (!completeFacets.indexes.includes(updateIndex)) {
3015
3033
  completeFacets.indexes.push(updateIndex);
@@ -3036,20 +3054,24 @@ class Entity {
3036
3054
  }
3037
3055
  }
3038
3056
 
3039
- return { indexKey, updatedKeys, setAttributes };
3057
+ return { indexKey, updatedKeys, setAttributes, deletedKeys };
3040
3058
  }
3041
3059
 
3042
- _getUpdatedKeys(pk, sk, set, removed) {
3060
+ _getUpdatedKeys(pk, sk, set, removed, composite = {}) {
3043
3061
  let updateIndex = TableIndex;
3044
3062
  let keyTranslations = this.model.translations.keys;
3045
3063
  let keyAttributes = { ...sk, ...pk };
3064
+
3046
3065
  let completeFacets = this._expectIndexFacets(
3047
3066
  { ...set },
3048
- { ...keyAttributes },
3067
+ { ...composite, ...keyAttributes },
3068
+ { utilizeIncludedOnlyIndexes: true },
3049
3069
  );
3070
+
3050
3071
  const removedKeyImpact = this._expectIndexFacets(
3051
3072
  { ...removed },
3052
3073
  { ...keyAttributes },
3074
+ { skipConditionCheck: true }
3053
3075
  );
3054
3076
 
3055
3077
  // complete facets, only includes impacted facets which likely does not include the updateIndex which then needs to be added here.
@@ -3059,17 +3081,29 @@ class Entity {
3059
3081
  sk: "sk",
3060
3082
  };
3061
3083
  }
3084
+
3062
3085
  let composedKeys = this._makeKeysFromAttributes(
3063
3086
  completeFacets.impactedIndexTypes,
3064
- { ...set, ...keyAttributes },
3087
+ { ...composite, ...set, ...keyAttributes },
3088
+ completeFacets.conditions,
3065
3089
  );
3066
3090
 
3067
3091
  let updatedKeys = {};
3068
3092
  let deletedKeys = [];
3069
3093
  let indexKey = {};
3094
+ for (const [indexName, condition] of Object.entries(completeFacets.conditions)) {
3095
+ if (!condition) {
3096
+ deletedKeys.push(this.model.translations.keys[indexName][KeyTypes.pk]);
3097
+ if (this.model.translations.keys[indexName][KeyTypes.sk]) {
3098
+ deletedKeys.push(this.model.translations.keys[indexName][KeyTypes.sk]);
3099
+ }
3100
+ }
3101
+ }
3102
+
3070
3103
  for (const keys of Object.values(removedKeyImpact.impactedIndexTypes)) {
3071
3104
  deletedKeys = deletedKeys.concat(Object.values(keys));
3072
3105
  }
3106
+
3073
3107
  for (let [index, keys] of Object.entries(composedKeys)) {
3074
3108
  let { pk, sk } = keyTranslations[index];
3075
3109
  if (index === updateIndex) {
@@ -3103,58 +3137,111 @@ class Entity {
3103
3137
  return { indexKey, updatedKeys, deletedKeys };
3104
3138
  }
3105
3139
 
3140
+ _indexConditionIsDefined(index) {
3141
+ const definition = this.model.indexes[this.model.translations.indexes.fromIndexToAccessPattern[index]];
3142
+ return definition && definition.conditionDefined;
3143
+ }
3144
+
3106
3145
  /* istanbul ignore next */
3107
- _getIndexImpact(attributes = {}, included = {}) {
3146
+ _getIndexImpact(attributes = {}, included = {}, { utilizeIncludedOnlyIndexes, skipConditionCheck } = {}) {
3147
+ // beware: this entire algorithm stinks and needs to be completely refactored. It does redundant loops and fights
3148
+ // itself the whole way through. I am sorry.
3108
3149
  let includedFacets = Object.keys(included);
3109
3150
  let impactedIndexes = {};
3110
- let skippedIndexes = new Set();
3151
+ let conditions = {};
3111
3152
  let impactedIndexTypes = {};
3153
+ let impactedIndexTypeSources = {};
3112
3154
  let completedIndexes = [];
3113
3155
  let facets = {};
3114
3156
  for (let [attribute, indexes] of Object.entries(this.model.facets.byAttr)) {
3115
3157
  if (attributes[attribute] !== undefined) {
3116
3158
  facets[attribute] = attributes[attribute];
3117
- indexes.forEach(({ index, type }) => {
3159
+ indexes.forEach((definition) => {
3160
+ const { index, type } = definition;
3118
3161
  impactedIndexes[index] = impactedIndexes[index] || {};
3119
3162
  impactedIndexes[index][type] = impactedIndexes[index][type] || [];
3120
3163
  impactedIndexes[index][type].push(attribute);
3121
3164
  impactedIndexTypes[index] = impactedIndexTypes[index] || {};
3122
- impactedIndexTypes[index][type] =
3123
- this.model.translations.keys[index][type];
3165
+ impactedIndexTypes[index][type] = this.model.translations.keys[index][type];
3166
+
3167
+ impactedIndexTypeSources[index] = impactedIndexTypeSources[index] || {};
3168
+ impactedIndexTypeSources[index][type] = ImpactedIndexTypeSource.provided;
3124
3169
  });
3125
3170
  }
3126
3171
  }
3127
3172
 
3128
- for (const indexName in impactedIndexes) {
3129
- const accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[indexName];
3130
- const shouldMakeKeys = this.model.indexes[accessPattern].condition({ ...attributes, ...included });
3131
- if (!shouldMakeKeys) {
3132
- skippedIndexes.add(indexName);
3173
+ // this function is used to determine key impact for update `set`, update `delete`, and `put`. This block is currently only used by update `set`
3174
+ if (utilizeIncludedOnlyIndexes) {
3175
+ for (const [index, { pk, sk }] of Object.entries(this.model.facets.byIndex)) {
3176
+ // The main table index is handled somewhere else (messy I know), and we only want to do this processing if an
3177
+ // index condition is defined for backwards compatibility. Backwards compatibility is not required for this
3178
+ // change, but I have paranoid concerns of breaking changes around sparse indexes.
3179
+ if (index === TableIndex || !this._indexConditionIsDefined(index)) {
3180
+ continue;
3181
+ }
3182
+
3183
+ if (pk && pk.length && pk.every(attr => included[attr] !== undefined)) {
3184
+ pk.forEach((attr) => {
3185
+ facets[attr] = included[attr];
3186
+ });
3187
+ impactedIndexes[index] = impactedIndexes[index] || {};
3188
+ impactedIndexes[index][KeyTypes.pk] = [...pk];
3189
+ impactedIndexTypes[index] = impactedIndexTypes[index] || {};
3190
+ impactedIndexTypes[index][KeyTypes.pk] = this.model.translations.keys[index][KeyTypes.pk];
3191
+
3192
+ // flagging the impactedIndexTypeSource as `composite` means the entire key is only being impacted because
3193
+ // all composites are in `included`. This will help us determine if we need to evaluate the `condition`
3194
+ // callback for the index. If both the `sk` and `pk` were impacted because of `included` then we can skip
3195
+ // the condition check because the index doesn't need to be recalculated;
3196
+ impactedIndexTypeSources[index] = impactedIndexTypeSources[index] || {};
3197
+ impactedIndexTypeSources[index][KeyTypes.pk] = impactedIndexTypeSources[index][KeyTypes.pk] || ImpactedIndexTypeSource.composite;
3198
+ }
3199
+
3200
+ if (sk && sk.length && sk.every(attr => included[attr] !== undefined)) {
3201
+ if (this.model.translations.keys[index][KeyTypes.sk]) {
3202
+ sk.forEach((attr) => {
3203
+ facets[attr] = included[attr];
3204
+ });
3205
+ impactedIndexes[index] = impactedIndexes[index] || {};
3206
+ impactedIndexes[index][KeyTypes.sk] = [...sk];
3207
+ impactedIndexTypes[index] = impactedIndexTypes[index] || {};
3208
+ impactedIndexTypes[index][KeyTypes.sk] = this.model.translations.keys[index][KeyTypes.sk];
3209
+
3210
+ // flagging the impactedIndexTypeSource as `composite` means the entire key is only being impacted because
3211
+ // all composites are in `included`. This will help us determine if we need to evaluate the `condition`
3212
+ // callback for the index. If both the `sk` and `pk` were impacted because of `included` then we can skip
3213
+ // the condition check because the index doesn't need to be recalculated;
3214
+ impactedIndexTypeSources[index] = impactedIndexTypeSources[index] || {};
3215
+ impactedIndexTypeSources[index][KeyTypes.sk] = impactedIndexTypeSources[index][KeyTypes.sk] || ImpactedIndexTypeSource.composite;
3216
+ }
3217
+ }
3133
3218
  }
3134
3219
  }
3135
3220
 
3136
- let incomplete = Object.entries(this.model.facets.byIndex)
3137
- .map(([index, { pk, sk }]) => {
3221
+ let indexesWithMissingComposites = Object.entries(this.model.facets.byIndex)
3222
+ .map(([index, definition]) => {
3223
+ const { pk, sk } = definition;
3138
3224
  let impacted = impactedIndexes[index];
3139
3225
  let impact = {
3140
3226
  index,
3227
+ definition,
3141
3228
  missing: []
3142
3229
  };
3143
- if (impacted && !skippedIndexes.has(index)) {
3230
+ if (impacted) {
3144
3231
  let missingPk =
3145
3232
  impacted[KeyTypes.pk] && impacted[KeyTypes.pk].length !== pk.length;
3146
3233
  let missingSk =
3147
3234
  impacted[KeyTypes.sk] && impacted[KeyTypes.sk].length !== sk.length;
3148
3235
  if (missingPk) {
3149
- impact.missing = [
3150
- ...impact.missing,
3151
- ...pk.filter((attr) => {
3152
- return (
3153
- !impacted[KeyTypes.pk].includes(attr) &&
3154
- !includedFacets.includes(attr)
3155
- );
3156
- }),
3157
- ];
3236
+ impact.missing = [
3237
+ ...impact.missing,
3238
+ ...pk.filter((attr) => {
3239
+ return (
3240
+ !impacted[KeyTypes.pk].includes(attr) &&
3241
+ !includedFacets.includes(attr)
3242
+ );
3243
+ }),
3244
+ ];
3158
3245
  }
3159
3246
  if (missingSk) {
3160
3247
  impact.missing = [
@@ -3170,12 +3257,55 @@ class Entity {
3170
3257
  completedIndexes.push(index);
3171
3258
  }
3172
3259
  }
3260
+
3173
3261
  return impact;
3174
- })
3175
- .filter(({ missing }) => missing.length);
3262
+ });
3263
+
3264
+ let incomplete = [];
3265
+ for (const { index, missing, definition } of indexesWithMissingComposites) {
3266
+ const indexConditionIsDefined = this._indexConditionIsDefined(index);
3267
+
3268
+ // `skipConditionCheck` is being used by update `remove`. If Attributes are being removed then the condition check
3269
+ // is meaningless and ElectroDB should uphold its obligation to keep keys and attributes in sync.
3270
+ // `index === TableIndex` is a special case where we don't need to check the condition because the main table is immutable
3271
+ // `!this._indexConditionIsDefined(index)` means the index doesn't have a condition defined, so we can skip the check
3272
+ if (skipConditionCheck || index === TableIndex || !indexConditionIsDefined) {
3273
+ incomplete.push({ index, missing });
3274
+ conditions[index] = true;
3275
+ continue;
3276
+ }
3277
+
3278
+ const memberAttributeIsImpacted = impactedIndexTypeSources[index] && (impactedIndexTypeSources[index][KeyTypes.pk] === ImpactedIndexTypeSource.provided || impactedIndexTypeSources[index][KeyTypes.sk] === ImpactedIndexTypeSource.provided);
3279
+ const allMemberAttributesAreIncluded = definition.all.every(({name}) => included[name] !== undefined);
3280
+
3281
+ if (memberAttributeIsImpacted || allMemberAttributesAreIncluded) {
3282
+ // the `missing` array will contain indexes that are partially provided, but that leaves cases where the pk or
3283
+ // sk of an index is complete but not both. Both cases are invalid if `indexConditionIsDefined=true`
3284
+ const missingAttributes = definition.all
3285
+ .filter(({name}) => !attributes[name] && !included[name] || missing.includes(name))
3286
+ .map(({name}) => name)
3287
+
3288
+ if (missingAttributes.length) {
3289
+ throw new e.ElectroError(e.ErrorCodes.IncompleteIndexCompositesAttributesProvided, `Incomplete composite attributes provided for index ${index}. Write operations that include composite attributes, for indexes with a condition callback defined, must always provide values for every index composite. This is to ensure consistency between index values and attribute values. Missing composite attributes identified: ${u.commaSeparatedString(missingAttributes)}`);
3290
+ }
3291
+
3292
+ const accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[index];
3293
+ let shouldMakeKeys = !!this.model.indexes[accessPattern].condition({...attributes, ...included});
3294
+
3295
+ // this helps identify which conditions were checked (key is present) and what the result was (true/false)
3296
+ conditions[index] = shouldMakeKeys;
3297
+ if (!shouldMakeKeys) {
3298
+ continue;
3299
+ }
3300
+ } else {
3301
+ incomplete.push({ index, missing });
3302
+ }
3303
+ }
3304
+
3305
+ incomplete = incomplete.filter(({ missing }) => missing.length);
3176
3306
 
3177
3307
  let isIncomplete = !!incomplete.length;
3178
- let complete = { facets, indexes: completedIndexes, impactedIndexTypes };
3308
+ let complete = { facets, indexes: completedIndexes, impactedIndexTypes, conditions };
3179
3309
  return [isIncomplete, { incomplete, complete }];
3180
3310
  }
3181
3311
 
@@ -3944,6 +4074,8 @@ class Entity {
3944
4074
  `The index option 'condition' is only allowed on secondary indexes`,
3945
4075
  );
3946
4076
  }
4077
+
4078
+ let conditionDefined = v.isFunction(index.condition);
3947
4079
  let indexCondition = index.condition || (() => true);
3948
4080
 
3949
4081
  if (indexType === "clustered") {
@@ -4054,6 +4186,7 @@ class Entity {
4054
4186
  index: indexName,
4055
4187
  scope: indexScope,
4056
4188
  condition: indexCondition,
4189
+ conditionDefined: conditionDefined,
4057
4190
  };
4058
4191
 
4059
4192
  indexHasSubCollections[indexName] =
package/src/errors.js CHANGED
@@ -217,6 +217,12 @@ const ErrorCodes = {
217
217
  name: "InvalidIndexOption",
218
218
  sym: ErrorCode,
219
219
  },
220
+ IncompleteIndexCompositesAttributesProvided: {
221
+ code: 2012,
222
+ section: 'invalid-index-composite-attributes-provided',
223
+ name: 'IncompleteIndexCompositesAttributesProvided',
224
+ sym: ErrorCode,
225
+ },
220
226
  InvalidAttribute: {
221
227
  code: 3001,
222
228
  section: "invalid-attribute",
package/output.json DELETED
@@ -1,12 +0,0 @@
1
-
2
- > electrodb@2.12.3 test:unit /Users/tylerw.walch/Desktop/media/code/projects/electrodb
3
- > mocha -r ts-node/register ./test/**.spec.* "--grep" "Paginate without overlapping values with count"
4
-
5
-
6
-
7
- Query Pagination
8
- ✓ Paginate without overlapping values with count (44ms)
9
-
10
-
11
- 1 passing (6s)
12
-