• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tywalch / electrodb / 394

pending completion
394

Pull #209

travis-ci-com

web-flow
Merge c619d81c8 into 5404b3d05
Pull Request #209: Feature/do not apply empty sets

1712 of 2200 branches covered (77.82%)

Branch coverage included in aggregate %.

56 of 56 new or added lines in 4 files covered. (100.0%)

3085 of 3401 relevant lines covered (90.71%)

2856.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.86
/src/operations.js
1
const {AttributeTypes, ItemOperations, AttributeProxySymbol, BuilderTypes, DynamoDBAttributeTypes} = require("./types");
1✔
2
const e = require("./errors");
1✔
3
const u = require("./util");
1✔
4

5
const deleteOperations = {
1✔
6
    canNest: false,
7
    template: function del(options, attr, path, value) {
8
        let operation = "";
24✔
9
        let expression = "";
24✔
10
        switch(attr.type) {
24!
11
            case AttributeTypes.any:
12
            case AttributeTypes.set:
13
                operation = ItemOperations.delete;
22✔
14
                expression = `${path} ${value}`;
22✔
15
                break;
22✔
16
            default:
17
                throw new Error(`Invalid Update Attribute Operation: "DELETE" Operation can only be performed on attributes with type "set" or "any".`);
2✔
18
        }
19
        return {operation, expression};
22✔
20
    },
21
};
22

23
const UpdateOperations = {
1✔
24
    ifNotExists: {
25
        template: function if_not_exists(options, attr, path, value) {
26
            const operation = ItemOperations.set;
4✔
27
            const expression = `${path} = if_not_exists(${path}, ${value})`;
4✔
28
            return {operation, expression};
4✔
29
        }
30
    },
31
    name: {
32
        canNest: true,
33
        template: function name(options, attr, path) {
34
            return path;
9✔
35
        }
36
    },
37
    value: {
38
        canNest: true,
39
        template: function value(options, attr, path, value) {
40
            return value;
6✔
41
        }
42
    },
43
    append: {
44
        canNest: false,
45
        template: function append(options, attr, path, value) {
46
            let operation = "";
42✔
47
            let expression = "";
42✔
48
            switch(attr.type) {
42✔
49
                case AttributeTypes.any:
50
                case AttributeTypes.list:
51
                    operation = ItemOperations.set;
40✔
52
                    expression = `${path} = list_append(${path}, ${value})`;
40✔
53
                    break;
40✔
54
                default:
55
                    throw new Error(`Invalid Update Attribute Operation: "APPEND" Operation can only be performed on attributes with type "list" or "any".`);
2✔
56
            }
57
            return {operation, expression};
40✔
58
        }
59
    },
60
    add: {
61
        canNest: false,
62
        template: function add(options, attr, path, value, ifNotExists) {
63
            let operation = "";
82✔
64
            let expression = "";
82✔
65
            let type = attr.type;
82✔
66
            if (type === AttributeTypes.any) {
82✔
67
                type = typeof value === 'number'
1!
68
                    ? AttributeTypes.number
69
                    : AttributeTypes.any;
70
            }
71
            switch(type) {
82✔
72
                case AttributeTypes.any:
73
                case AttributeTypes.set:
74
                    operation = ItemOperations.add;
49✔
75
                    expression = `${path} ${value}`;
49✔
76
                    break;
49✔
77
                case AttributeTypes.number:
78
                    if (options.nestedValue) {
31✔
79
                        operation = ItemOperations.set;
9✔
80
                        expression = `${path} = ${path} + ${value}`;
9✔
81
                    } else {
82
                        operation = ItemOperations.add;
22✔
83
                        expression = `${path} ${value}`;
22✔
84
                    }
85
                    break;
31✔
86
                default:
87
                    throw new Error(`Invalid Update Attribute Operation: "ADD" Operation can only be performed on attributes with type "number", "set", or "any".`);
2✔
88
            }
89
            return {operation, expression};
80✔
90
        }
91
    },
92
    subtract: {
93
        canNest: false,
94
        template: function subtract(options, attr, path, value, ifNotExists) {
95
            let operation = "";
18✔
96
            let expression = "";
18✔
97
            switch(attr.type) {
18!
98
                case AttributeTypes.any:
99
                case AttributeTypes.number:
100
                    operation = ItemOperations.set;
18✔
101
                    expression = `${path} = ${path} - ${value}`;
18✔
102
                    break;
18✔
103
                default:
104
                    throw new Error(`Invalid Update Attribute Operation: "SUBTRACT" Operation can only be performed on attributes with type "number" or "any".`);
×
105
            }
106

107
            return {operation, expression};
18✔
108
        }
109
    },
110
    set: {
111
        canNest: false,
112
        template: function set(options, attr, path, value) {
113
            let operation = "";
273✔
114
            let expression = "";
273✔
115
            switch(attr.type) {
273!
116
                case AttributeTypes.set:
117
                case AttributeTypes.list:
118
                case AttributeTypes.map:
119
                case AttributeTypes.enum:
120
                case AttributeTypes.string:
121
                case AttributeTypes.number:
122
                case AttributeTypes.boolean:
123
                case AttributeTypes.any:
124
                    operation = ItemOperations.set;
273✔
125
                    expression = `${path} = ${value}`;
273✔
126
                    break;
273✔
127
                default:
128
                    throw new Error(`Invalid Update Attribute Operation: "SET" Operation can only be performed on attributes with type "list", "map", "string", "number", "boolean", or "any".`);
×
129
            }
130
            return {operation, expression};
273✔
131
        }
132
    },
133
    remove: {
134
        canNest: false,
135
        template: function remove(options, attr, ...paths) {
136
            let operation = "";
66✔
137
            let expression = "";
66✔
138
            switch(attr.type) {
66!
139
                case AttributeTypes.set:
140
                case AttributeTypes.any:
141
                case AttributeTypes.list:
142
                case AttributeTypes.map:
143
                case AttributeTypes.string:
144
                case AttributeTypes.number:
145
                case AttributeTypes.boolean:
146
                case AttributeTypes.enum:
147
                    operation = ItemOperations.remove;
66✔
148
                    expression = paths.join(", ");
66✔
149
                    break;
66✔
150
                default: {
151
                    throw new Error(`Invalid Update Attribute Operation: "REMOVE" Operation can only be performed on attributes with type "map", "list", "string", "number", "boolean", or "any".`);
×
152
                }
153
            }
154
            return {operation, expression};
66✔
155
        }
156
    },
157
    del: deleteOperations,
158
    delete: deleteOperations
159
}
160

161
const FilterOperations = {
1✔
162
    escape: {
163
        template: function escape(options, attr) {
164
            return `${attr}`;
3✔
165
        },
166
        noAttribute: true,
167
    },
168
    size: {
169
      template: function size(options, attr, name) {
170
        return `size(${name})`
2✔
171
      },
172
      strict: false,
173
    },
174
    type: {
175
        template: function attributeType(options, attr, name, value) {
176
            return `attribute_type(${name}, ${value})`;
2✔
177
        },
178
        strict: false
179
    },
180
    ne: {
181
        template: function ne(options, attr, name, value) {
182
            return `${name} <> ${value}`;
×
183
        },
184
        strict: false,
185
    },
186
    eq: {
187
        template: function eq(options, attr, name, value) {
188
            return `${name} = ${value}`;
452✔
189
        },
190
        strict: false,
191
    },
192
    gt: {
193
        template: function gt(options, attr, name, value) {
194
            return `${name} > ${value}`;
43✔
195
        },
196
        strict: false
197
    },
198
    lt: {
199
        template: function lt(options, attr, name, value) {
200
            return `${name} < ${value}`;
3✔
201
        },
202
        strict: false
203
    },
204
    gte: {
205
        template: function gte(options, attr, name, value) {
206
            return `${name} >= ${value}`;
10✔
207
        },
208
        strict: false
209
    },
210
    lte: {
211
        template: function lte(options, attr, name, value) {
212
            return `${name} <= ${value}`;
74✔
213
        },
214
        strict: false
215
    },
216
    between: {
217
        template: function between(options, attr, name, value1, value2) {
218
            return `(${name} between ${value1} and ${value2})`;
8✔
219
        },
220
        strict: false
221
    },
222
    begins: {
223
        template: function begins(options, attr, name, value) {
224
            return `begins_with(${name}, ${value})`;
3✔
225
        },
226
        strict: false
227
    },
228
    exists: {
229
        template: function exists(options, attr, name) {
230
            return `attribute_exists(${name})`;
179✔
231
        },
232
        strict: false
233
    },
234
    notExists: {
235
        template: function notExists(options, attr, name) {
236
            return `attribute_not_exists(${name})`;
198✔
237
        },
238
        strict: false
239
    },
240
    contains: {
241
        template: function contains(options, attr, name, value) {
242
            return `contains(${name}, ${value})`;
9✔
243
        },
244
        strict: false
245
    },
246
    notContains: {
247
        template: function notContains(options, attr, name, value) {
248
            return `not contains(${name}, ${value})`;
9✔
249
        },
250
        strict: false
251
    },
252
    value: {
253
        template: function(options, attr, name, value) {
254
            return value;
10✔
255
        },
256
        strict: false,
257
        canNest: true,
258
    },
259
    name: {
260
        template: function(options, attr, name) {
261
            return name;
9✔
262
        },
263
        strict: false,
264
        canNest: true,
265
    }
266
};
267

268
class ExpressionState {
269
    constructor({prefix} = {}) {
20,128✔
270
        this.names = {};
30,192✔
271
        this.values = {};
30,192✔
272
        this.paths = {};
30,192✔
273
        this.counts = {};
30,192✔
274
        this.impacted = {};
30,192✔
275
        this.expression = "";
30,192✔
276
        this.prefix = prefix || "";
30,192✔
277
        this.refs = {};
30,192✔
278
    }
279

280
    incrementName(name) {
281
        if (this.counts[name] === undefined) {
2,372✔
282
            this.counts[name] = 0;
2,348✔
283
        }
284
        return `${this.prefix}${this.counts[name]++}`;
2,372✔
285
    }
286

287
    // todo: make the structure: name, value, paths
288
    setName(paths, name, value) {
289
        let json = "";
2,950✔
290
        let expression = "";
2,950✔
291
        const prop = `#${name}`;
2,950✔
292
        if (Object.keys(paths).length === 0) {
2,950✔
293
            json = `${name}`;
2,855✔
294
            expression = `${prop}`;
2,855✔
295
            this.names[prop] = value;
2,855✔
296
        } else if (isNaN(name)) {
95✔
297
            json = `${paths.json}.${name}`;
65✔
298
            expression = `${paths.expression}.${prop}`;
65✔
299
            this.names[prop] = value;
65✔
300
        } else {
301
            json = `${paths.json}[*]`;
30✔
302
            expression = `${paths.expression}[${name}]`;
30✔
303
        }
304
        return {json, expression, prop};
2,950✔
305
    }
306

307
    getNames() {
308
        return this.names;
1,661✔
309
    }
310

311
    setValue(name, value) {
312
        let valueCount = this.incrementName(name);
2,372✔
313
        let expression = `:${name}${valueCount}`
2,372✔
314
        this.values[expression] = value;
2,372✔
315
        return expression;
2,372✔
316
    }
317

318
    updateValue(name, value) {
319
        this.values[name] = value;
388✔
320
    }
321

322
    getValues() {
323
        return this.values;
1,477✔
324
    }
325

326
    setPath(path, value) {
327
        this.paths[path] = value;
533✔
328
    }
329

330
    setExpression(expression) {
331
        this.expression = expression;
×
332
    }
333

334
    getExpression() {
335
        return this.expression;
2✔
336
    }
337

338
    setImpacted(operation, path, ref) {
339
        this.impacted[path] = operation;
645✔
340
        this.refs[path] = ref;
645✔
341
    }
342
}
343

344
class AttributeOperationProxy {
345
    constructor({builder, attributes = {}, operations = {}}) {
×
346
        this.ref = {
10,179✔
347
            attributes,
348
            operations
349
        };
350
        this.attributes = AttributeOperationProxy.buildAttributes(builder, attributes);
10,179✔
351
        this.operations = AttributeOperationProxy.buildOperations(builder, operations);
10,179✔
352
    }
353

354
    invokeCallback(op, ...params) {
355
        return op(this.attributes, this.operations, ...params);
174✔
356
    }
357

358
    fromObject(operation, record) {
359
        for (let path of Object.keys(record)) {
290✔
360
            if (record[path] === undefined) {
346!
361
                continue;
×
362
            }
363
            const value = record[path];
346✔
364
            const parts = u.parseJSONPath(path);
346✔
365
            let attribute = this.attributes;
346✔
366
            for (let part of parts) {
346✔
367
                attribute = attribute[part];
354✔
368
            }
369
            if (attribute) {
346!
370
                this.operations[operation](attribute, value);
346✔
371
                const {target} = attribute();
340✔
372
                if (target.readOnly) {
340✔
373
                    throw new Error(`Attribute "${target.path}" is Read-Only and cannot be updated`);
1✔
374
                }
375
            }
376
        }
377
    }
378

379
    fromArray(operation, paths) {
380
        for (let path of paths) {
25✔
381
            const parts = u.parseJSONPath(path);
40✔
382
            let attribute = this.attributes;
40✔
383
            for (let part of parts) {
40✔
384
                attribute = attribute[part];
42✔
385
            }
386
            if (attribute) {
40!
387
                this.operations[operation](attribute);
40✔
388
                const {target} = attribute();
40✔
389
                if (target.readOnly) {
40!
390
                    throw new Error(`Attribute "${target.path}" is Read-Only and cannot be updated`);
×
391
                } else if (operation === ItemOperations.remove && target.required) {
40!
392
                    throw new Error(`Attribute "${target.path}" is Required and cannot be removed`);
×
393
                }
394
            }
395
        }
396
    }
397

398
    static buildOperations(builder, operations) {
399
        let ops = {};
10,179✔
400
        let seen = new Map();
10,179✔
401
        for (let operation of Object.keys(operations)) {
10,179✔
402
            let {template, canNest, noAttribute} = operations[operation];
102,595✔
403
            Object.defineProperty(ops, operation, {
102,595✔
404
                get: () => {
405
                    return (property, ...values) => {
649✔
406
                        if (property === undefined) {
667✔
407
                            throw new e.ElectroError(e.ErrorCodes.InvalidWhere, `Invalid/Unknown property passed in where clause passed to operation: '${operation}'`);
2✔
408
                        }
409
                        if (property[AttributeProxySymbol]) {
665✔
410
                            const {commit, target} = property();
661✔
411
                            const fixedValues = values.map((value) => target.applyFixings(value))
661✔
412
                                .filter(value => value !== undefined);
563✔
413
                            const isFilterBuilder = builder.type === BuilderTypes.filter;
661✔
414
                            const takesValueArgument = template.length > 3;
661✔
415
                            const isAcceptableValue = fixedValues.every(value => {
661✔
416
                                const seenAttributes = seen.get(value);
559✔
417
                                if (seenAttributes) {
559✔
418
                                    return seenAttributes.every(v => target.acceptable(v))
23✔
419
                                }
420
                                return target.acceptable(value);
536✔
421
                            });
422

423
                            const shouldCommit =
424
                                // if it is a filterBuilder than we don't care what they pass because the user needs more freedom here
425
                                isFilterBuilder ||
661✔
426
                                // if the operation does not take a value argument then not committing here could cause problems.
427
                                // this should be revisited to make more robust, we could hypothetically store the commit in the
428
                                // "seen" map for when the value is used, but that's a lot of new complexity
429
                                !takesValueArgument ||
430
                                // if the operation takes a value, we should determine if that value is acceptable. For
431
                                // example, in the cases of a "set" we check to see if it is empty, or if the value is
432
                                // undefined, we should not commit. The "fixedValues" length check is because the
433
                                // "fixedValues" array has been filtered for undefined, so no length there indicates an
434
                                // undefined value was passed.
435
                                (takesValueArgument && isAcceptableValue && fixedValues.length > 0);
436

437
                            if (!shouldCommit) {
661✔
438
                                return '';
10✔
439
                            }
440

441
                            const paths = commit();
651✔
442
                            const attributeValues = [];
651✔
443
                            let hasNestedValue = false;
651✔
444
                            for (let fixedValue of fixedValues) {
651✔
445
                                if (seen.has(fixedValue)) {
553✔
446
                                    attributeValues.push(fixedValue);
23✔
447
                                    hasNestedValue = true;
23✔
448
                                } else {
449
                                    let attributeValueName = builder.setValue(target.name, fixedValue);
530✔
450
                                    builder.setPath(paths.json, {
530✔
451
                                        value: fixedValue,
452
                                        name: attributeValueName
453
                                    });
454
                                    attributeValues.push(attributeValueName);
530✔
455
                                }
456
                            }
457

458
                            const options = {
651✔
459
                                nestedValue: hasNestedValue
460
                            }
461

462
                            const formatted = template(options, target, paths.expression, ...attributeValues);
651✔
463
                            builder.setImpacted(operation, paths.json, target);
645✔
464
                            if (canNest) {
645✔
465
                                seen.set(paths.expression, attributeValues);
32✔
466
                                seen.set(formatted, attributeValues);
32✔
467
                            }
468

469
                            if (builder.type === BuilderTypes.update && formatted && typeof formatted.operation === "string" && typeof formatted.expression === "string") {
645✔
470
                                builder.add(formatted.operation, formatted.expression);
503✔
471
                                return formatted.expression;
503✔
472
                            }
473

474
                            return formatted;
142✔
475
                        } else if (noAttribute) {
4✔
476
                            // const {json, expression} = builder.setName({}, property, property);
477
                            let attributeValueName = builder.setValue(property, property);
3✔
478
                            builder.setPath(property, {
3✔
479
                                value: property,
480
                                name: attributeValueName,
481
                            });
482
                            const formatted = template({}, attributeValueName);
3✔
483
                            seen.set(attributeValueName, [property]);
3✔
484
                            seen.set(formatted, [property]);
3✔
485
                            return formatted;
3✔
486
                        } else {
487
                            throw new e.ElectroError(e.ErrorCodes.InvalidWhere, `Invalid Attribute in where clause passed to operation '${operation}'. Use injected attributes only.`);
1✔
488
                        }
489
                    }
490
                }
491
            });
492
        }
493
        return ops;
10,179✔
494
    }
495

496
    static pathProxy(build) {
497
        return new Proxy(() => build(), {
1,041✔
498
            get: (_, prop, o) => {
499
                if (prop === AttributeProxySymbol) {
761✔
500
                    return true;
661✔
501
                } else {
502
                    return AttributeOperationProxy.pathProxy(() => {
100✔
503
                        const { commit, root, target, builder } = build();
111✔
504
                        const attribute = target.getChild(prop);
111✔
505
                        let field;
506
                        if (attribute === undefined) {
111!
507
                            throw new Error(`Invalid attribute "${prop}" at path "${target.path}.${prop}"`);
×
508
                        } else if (attribute === root && attribute.type === AttributeTypes.any) {
111✔
509
                            // 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
510
                            field = prop;
11✔
511
                        } else {
512
                            field = attribute.field;
100✔
513
                        }
514

515
                        return {
111✔
516
                            root,
517
                            builder,
518
                            target: attribute,
519
                            commit: () => {
520
                                const paths = commit();
95✔
521
                                return builder.setName(paths, prop, field);
95✔
522
                            },
523
                        }
524
                    });
525
                }
526
            }
527
        });
528
    }
529

530
    static buildAttributes(builder, attributes) {
531
        let attr = {};
10,179✔
532
        for (let [name, attribute] of Object.entries(attributes)) {
10,179✔
533
            Object.defineProperty(attr, name, {
69,272✔
534
                get: () => {
535
                    return AttributeOperationProxy.pathProxy(() => {
645✔
536
                        return {
1,041✔
537
                            root: attribute,
538
                            target: attribute,
539
                            builder,
540
                            commit: () => builder.setName({}, attribute.name, attribute.field)
651✔
541
                        }
542
                    });
543
                }
544
            });
545
        }
546
        return attr;
10,179✔
547
    }
548
}
549

550
const FilterOperationNames = Object.keys(FilterOperations).reduce((ops, name) => {
1✔
551
    ops[name] = name;
17✔
552
    return ops;
17✔
553
}, {});
554

555
module.exports = {UpdateOperations, FilterOperations, FilterOperationNames, ExpressionState, AttributeOperationProxy};
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc