electrodb 2.13.1 → 2.14.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/package.json +1 -1
- package/src/clauses.js +5 -1
- package/src/entity.js +168 -35
- package/src/errors.js +6 -0
package/package.json
CHANGED
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.
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
|
3129
|
-
|
|
3130
|
-
const
|
|
3131
|
-
|
|
3132
|
-
|
|
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
|
|
3137
|
-
.map(([index,
|
|
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
|
|
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
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
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
|
-
|
|
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] === undefined && included[name] === undefined || 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",
|