• 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

87.27
/src/schema.js
1
const { CastTypes, ValueTypes, KeyCasing, AttributeTypes, AttributeMutationMethods, AttributeWildCard, PathTypes, TableIndex, ItemOperations } = require("./types");
1✔
2
const AttributeTypeNames = Object.keys(AttributeTypes);
1✔
3
const ValidFacetTypes = [AttributeTypes.string, AttributeTypes.number, AttributeTypes.boolean, AttributeTypes.enum];
1✔
4
const e = require("./errors");
1✔
5
const u = require("./util");
1✔
6
const v = require("./validations");
1✔
7
const {DynamoDBSet} = require("./set");
1✔
8

9
function getValueType(value) {
10
        if (value === undefined) {
2,963✔
11
                return ValueTypes.undefined;
224✔
12
        } else if (value === null) {
2,739!
13
                return ValueTypes.null;
×
14
        } else if (typeof value === "string") {
2,739✔
15
                return ValueTypes.string;
49✔
16
        } else if (typeof value === "number") {
2,690!
17
                return ValueTypes.number;
×
18
        } else if (typeof value === "boolean") {
2,690!
19
                return ValueTypes.boolean;
×
20
        } else if (Array.isArray(value)) {
2,690✔
21
                return ValueTypes.array;
869✔
22
        } else if (value.wrapperName === "Set") {
1,821✔
23
                return ValueTypes.aws_set;
957✔
24
        } else if (value.constructor.name === "Set") {
864✔
25
                return ValueTypes.set;
39✔
26
        } else if (value.constructor.name === "Map") {
825✔
27
                return ValueTypes.map;
1✔
28
        } else if (value.constructor.name === "Object") {
824!
29
                return ValueTypes.object;
824✔
30
        } else {
31
                return ValueTypes.unknown;
×
32
        }
33
}
34

35
class AttributeTraverser {
36
        constructor(parentTraverser) {
37
                if (parentTraverser instanceof AttributeTraverser) {
4,915✔
38
                        this.parent = parentTraverser;
4,185✔
39
                        this.paths = this.parent.paths;
4,185✔
40
                } else {
41
                        this.parent = null;
730✔
42
                        this.paths = new Map();
730✔
43
                }
44
                this.children = new Map();
4,915✔
45
        }
46

47
        setChild(name, attribute) {
48
                this.children.set(name, attribute);
4,185✔
49
        }
50

51
        asChild(name, attribute) {
52
                if (this.parent) {
4,193✔
53
                        this.parent.setChild(name, attribute);
4,185✔
54
                }
55
        }
56

57
        setPath(path, attribute) {
58
                if (this.parent) {
17,582✔
59
                        this.parent.setPath(path, attribute);
9,196✔
60
                }
61
                this.paths.set(path, attribute);
17,582✔
62
        }
63

64
        getPath(path) {
65
                path = u.genericizeJSONPath(path);
127,467✔
66
                if (this.parent) {
127,467!
67
                        return this.parent.getPath(path);
×
68
                }
69
                return this.paths.get(path);
127,467✔
70
        }
71

72
        getChild(name) {
73
                return this.children.get(name);
100✔
74
        }
75

76
        getAllChildren() {
77
                return this.children.entries();
×
78
        }
79

80
        getAll() {
81
                if (this.parent) {
340!
82
                        return this.parent.getAll();
×
83
                }
84
                return this.paths.entries();
340✔
85
        }
86
}
87

88

89
class Attribute {
90
        constructor(definition = {}) {
×
91
                this.name = definition.name;
4,195✔
92
                this.field = definition.field || definition.name;
4,195✔
93
                this.label = definition.label;
4,195✔
94
                this.readOnly = !!definition.readOnly;
4,195✔
95
                this.hidden = !!definition.hidden;
4,195✔
96
                this.required = !!definition.required;
4,195✔
97
                this.cast = this._makeCast(definition.name, definition.cast);
4,195✔
98
                this.default = this._makeDefault(definition.default);
4,194✔
99
                this.validate = this._makeValidate(definition.validate);
4,194✔
100
                this.isKeyField = !!definition.isKeyField;
4,194✔
101
                this.unformat = this._makeDestructureKey(definition);
4,194✔
102
                this.format = this._makeStructureKey(definition);
4,194✔
103
                this.padding = definition.padding;
4,194✔
104
                this.applyFixings = this._makeApplyFixings(definition);
4,194✔
105
                this.applyPadding = this._makePadding(definition);
4,194✔
106
                this.indexes = [...(definition.indexes || [])];
4,194✔
107
                let {isWatched, isWatcher, watchedBy, watching, watchAll} = Attribute._destructureWatcher(definition);
4,194✔
108
                this._isWatched = isWatched
4,194✔
109
                this._isWatcher = isWatcher;
4,194✔
110
                this.watchedBy = watchedBy;
4,194✔
111
                this.watching = watching;
4,194✔
112
                this.watchAll = watchAll;
4,194✔
113
                let { type, enumArray } = this._makeType(this.name, definition);
4,194✔
114
                this.type = type;
4,193✔
115
                this.enumArray = enumArray;
4,193✔
116
                this.parentType = definition.parentType;
4,193✔
117
                this.parentPath = definition.parentPath;
4,193✔
118
                const pathType = this.getPathType(this.type, this.parentType);
4,193✔
119
                const path = Attribute.buildPath(this.name, pathType, this.parentPath);
4,193✔
120
                const fieldPath = Attribute.buildPath(this.field, pathType, this.parentType);
4,193✔
121
                this.path = path;
4,193✔
122
                this.fieldPath = fieldPath;
4,193✔
123
                this.traverser = new AttributeTraverser(definition.traverser);
4,193✔
124
                this.traverser.setPath(this.path, this);
4,193✔
125
                this.traverser.setPath(this.fieldPath, this);
4,193✔
126
                this.traverser.asChild(this.name, this);
4,193✔
127
                this.parent = { parentType: this.type, parentPath: this.path };
4,193✔
128
                this.get = this._makeGet(definition.get);
4,193✔
129
                this.set = this._makeSet(definition.set);
4,192✔
130
                this.client = definition.client;
4,191✔
131
        }
132

133
        static buildChildAttributes(type, definition, parent) {
134
                let items;
135
                let properties;
136
                if (type === AttributeTypes.list) {
×
137
                        items = Attribute.buildChildListItems(definition, parent);
×
138
                } else if (type === AttributeTypes.set) {
×
139
                        items = Attribute.buildChildSetItems(definition, parent);
×
140
                } else if (type === AttributeTypes.map) {
×
141
                        properties = Attribute.buildChildMapProperties(definition, parent);
×
142
                }
143

144
                return {items, properties};
×
145
        }
146

147
        static buildChildListItems(definition, parent) {
148
                const {items, client} = definition;
52✔
149
                const prop = {...items, ...parent};
52✔
150
                // The use of "*" is to ensure the child's name is "*" when added to the traverser and searching for the children of a list
151
                return Schema.normalizeAttributes({ '*': prop }, {}, {client, traverser: parent.traverser, parent}).attributes["*"];
52✔
152
        }
153

154
        static buildChildSetItems(definition, parent) {
155
                const {items, client} = definition;
65✔
156

157
                const allowedTypes = [AttributeTypes.string, AttributeTypes.boolean, AttributeTypes.number, AttributeTypes.enum];
65✔
158
                if (!Array.isArray(items) && !allowedTypes.includes(items)) {
65!
159
                        throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid "items" definition for Set attribute: "${definition.path}". Acceptable item types include ${u.commaSeparatedString(allowedTypes)}`);
×
160
                }
161
                const prop = {type: items, ...parent};
65✔
162
                return Schema.normalizeAttributes({ prop }, {}, {client, traverser: parent.traverser, parent}).attributes.prop;
65✔
163
        }
164

165
        static buildChildMapProperties(definition, parent) {
166
                const {properties, client} = definition;
90✔
167
                if (!properties || typeof properties !== "object") {
90!
168
                        throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid "properties" definition for Map attribute: "${definition.path}". The "properties" definition must describe the attributes that the Map will accept`);
×
169
                }
170
                const attributes = {};
90✔
171
                for (let name of Object.keys(properties)) {
90✔
172
                        attributes[name] = {...properties[name], ...parent};
182✔
173
                }
174
                return Schema.normalizeAttributes(attributes, {}, {client, traverser: parent.traverser, parent});
90✔
175
        }
176

177
        static buildPath(name, type, parentPath) {
178
                if (!parentPath) return name;
8,386✔
179
                switch(type) {
588!
180
                        case AttributeTypes.string:
181
                        case AttributeTypes.number:
182
                        case AttributeTypes.boolean:
183
                        case AttributeTypes.map:
184
                        case AttributeTypes.set:
185
                        case AttributeTypes.list:
186
                        case AttributeTypes.enum:
187
                                return `${parentPath}.${name}`;
360✔
188
                        case PathTypes.item:
189
                                return `${parentPath}[*]`;
228✔
190
                        case AttributeTypes.any:
191
                        default:
192
                                return `${parentPath}.*`;
×
193
                }
194
        }
195

196
        static _destructureWatcher(definition) {
197
                let watchAll = !!definition.watchAll;
4,194✔
198
                let watchingArr = watchAll ? []: [...(definition.watching || [])];
4,194✔
199
                let watchedByArr = [...(definition.watchedBy || [])];
4,194✔
200
                let isWatched = watchedByArr.length > 0;
4,194✔
201
                let isWatcher = watchingArr.length > 0;
4,194✔
202
                let watchedBy = {};
4,194✔
203
                let watching = {};
4,194✔
204

205
                for (let watched of watchedByArr) {
4,194✔
206
                        watchedBy[watched] = watched;
54✔
207
                }
208

209
                for (let attribute of watchingArr) {
4,194✔
210
                        watching[attribute] = attribute;
56✔
211
                }
212

213
                return {
4,194✔
214
                        watchAll,
215
                        watching,
216
                        watchedBy,
217
                        isWatched,
218
                        isWatcher
219
                }
220
        }
221

222
        _makeGet(get) {
223
                this._checkGetSet(get, "get");
3,986✔
224
                const getter = get || ((attr) => attr);
37,243✔
225
                return (value, siblings) => {
3,985✔
226
                        if (this.hidden) {
37,496✔
227
                                return;
34✔
228
                        }
229
                        value = this.unformat(value);
37,462✔
230
                        return getter(value, siblings);
37,462✔
231
                }
232
        }
233

234
        _makeSet(set) {
235
                this._checkGetSet(set, "set");
3,985✔
236
                return set || ((attr) => attr);
38,794✔
237
        }
238

239
        _makeApplyFixings({ prefix = "", postfix = "", casing= KeyCasing.none } = {}) {
24!
240
                return (value) => {
4,194✔
241
                        if (value === undefined) {
563✔
242
                                return;
4✔
243
                        }
244

245
                        if ([AttributeTypes.string, AttributeTypes.enum].includes(this.type)) {
559✔
246
                                value = `${prefix}${value}${postfix}`;
318✔
247
                        }
248

249
                        return u.formatAttributeCasing(value, casing);
559✔
250
                }
251
        }
252

253
        _makeStructureKey() {
254
                return (key) => {
4,194✔
255
                        return this.applyPadding(key);
48,243✔
256
                }
257
        }
258

259
        _isPaddingEligible(padding = {} ) {
×
260
                return !!padding && padding.length && v.isStringHasLength(padding.char);
48,243✔
261
        }
262

263
        _makePadding({ padding = {} }) {
3,735✔
264
                return (value) => {
4,194✔
265
                        if (typeof value !== 'string') {
48,243!
266
                                return value;
×
267
                        } else if (this._isPaddingEligible(padding)) {
48,243✔
268
                                return u.addPadding({padding, value});
4,584✔
269
                        } else {
270
                                return value;
43,659✔
271
                        }
272
                }
273
        }
274

275
        _makeRemoveFixings({prefix = "", postfix = "", casing= KeyCasing.none} = {}) {
×
276
                return (key) => {
×
277
                        let value = "";
×
278
                        if (![AttributeTypes.string, AttributeTypes.enum].includes(this.type) || typeof key !== "string") {
×
279
                                value = key;
×
280
                        } else if (prefix.length > 0 && key.length > prefix.length) {
×
281
                                for (let i = prefix.length; i < key.length - postfix.length; i++) {
×
282
                                        value += key[i];
×
283
                                }
284
                        } else {
285
                                value = key;
×
286
                        }
287

288
                        return value;
×
289
                }
290
        }
291

292
        _makeDestructureKey({prefix = "", postfix = "", casing= KeyCasing.none, padding = {}} = {}) {
3,759!
293
                return (key) => {
4,194✔
294
                        let value = "";
37,462✔
295
                        if (![AttributeTypes.string, AttributeTypes.enum].includes(this.type) || typeof key !== "string") {
37,462✔
296
                                return key;
6,790✔
297
                        } else if (key.length > prefix.length) {
30,672✔
298
                                value = u.removeFixings({prefix, postfix, value: key});
30,660✔
299
                        } else {
300
                                value = key;
12✔
301
                        }
302

303
                        // todo: if an attribute is also used as a pk or sk directly in one index, but a composite in another, then padding is going to be broken
304
                        // if (padding && padding.length) {
305
                        //         value = u.removePadding({padding, value});
306
                        // }
307

308
                        return value;
30,672✔
309
                };
310
        }
311

312
        acceptable(val) {
313
                return val !== undefined;
481✔
314
        }
315

316
        getPathType(type, parentType) {
317
                if (parentType === AttributeTypes.list || parentType === AttributeTypes.set) {
4,193✔
318
                        return PathTypes.item;
115✔
319
                }
320
                return type;
4,078✔
321
        }
322

323
        getAttribute(path) {
324
                return this.traverser.getPath(path);
×
325
        }
326

327
        getChild(path) {
328
                if (this.type === AttributeTypes.any) {
111✔
329
                        return this;
11✔
330
                } else if (!isNaN(path) && (this.type === AttributeTypes.list || this.type === AttributeTypes.set)) {
100!
331
                        // if they're asking for a number, and this is a list, children will be under "*"
332
                        return this.traverser.getChild("*");
34✔
333
                } else {
334
                        return this.traverser.getChild(path);
66✔
335
                }
336
        }
337

338
        _checkGetSet(val, type) {
339
                if (typeof val !== "function" && val !== undefined) {
8,791✔
340
                        throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid "${type}" property for attribute ${this.path}. Please ensure value is a function or undefined.`);
2✔
341
                }
342
        }
343

344
        _makeCast(name, cast) {
345
                if (cast !== undefined && !CastTypes.includes(cast)) {
4,195✔
346
                        throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid "cast" property for attribute: "${name}". Acceptable types include ${CastTypes.join(", ")}`,
1✔
347
                );
348
                } else if (cast === AttributeTypes.string) {
4,194✔
349
                        return (val) => {
1✔
350
                                if (val === undefined) {
3✔
351
                                        // todo: #electroerror
352
                                        throw new Error(`Attribute ${name} is undefined and cannot be cast to type ${cast}`);
1✔
353
                                } else if (typeof val === "string") {
2✔
354
                                        return val;
1✔
355
                                } else {
356
                                        return JSON.stringify(val);
1✔
357
                                }
358
                        };
359
                } else if (cast === AttributeTypes.number) {
4,193✔
360
                        return (val) => {
1✔
361
                                if (val === undefined) {
4✔
362
                                        // todo: #electroerror
363
                                        throw new Error(`Attribute ${name} is undefined and cannot be cast to type ${cast}`);
1✔
364
                                } else if (typeof val === "number") {
3✔
365
                                        return val;
1✔
366
                                } else {
367
                                        let results = Number(val);
2✔
368
                                        if (isNaN(results)) {
2✔
369
                                                // todo: #electroerror
370
                                                throw new Error(`Attribute ${name} cannot be cast to type ${cast}. Doing so results in NaN`);
1✔
371
                                        } else {
372
                                                return results;
1✔
373
                                        }
374
                                }
375
                        };
376
                } else {
377
                        return (val) => val;
40,008✔
378
                }
379
        }
380

381
        _makeValidate(definition) {
382
                if (typeof definition === "function") {
4,259✔
383
                        return (val) => {
180✔
384
                                try {
660✔
385
                                        let reason = definition(val);
660✔
386
                                        const isValid = !reason;
646✔
387
                                        if (isValid) {
646✔
388
                                                return [isValid, []];
594✔
389
                                        } else if (typeof reason === "boolean") {
52✔
390
                                                return [isValid, [new e.ElectroUserValidationError(this.path, "Invalid value provided")]];
30✔
391
                                        } else {
392
                                                return [isValid, [new e.ElectroUserValidationError(this.path, reason)]];
22✔
393
                                        }
394
                                } catch(err) {
395
                                        return [false, [new e.ElectroUserValidationError(this.path, err)]];
14✔
396
                                }
397
                        };
398
                } else if (definition instanceof RegExp) {
4,079✔
399
                        return (val) => {
10✔
400
                                if (val === undefined) {
22✔
401
                                        return [true, []];
4✔
402
                                }
403
                                let isValid = definition.test(val);
18✔
404
                                let reason = [];
18✔
405
                                if (!isValid) {
18✔
406
                                        reason.push(new e.ElectroUserValidationError(this.path, `Invalid value for attribute "${this.path}": Failed model defined regex`));
7✔
407
                                }
408
                                return [isValid, reason];
18✔
409
                        };
410
                } else {
411
                        return () => [true, []];
40,425✔
412
                }
413
        }
414

415
        _makeDefault(definition) {
416
                if (typeof definition === "function") {
4,194✔
417
                        return () => definition();
154✔
418
                } else {
419
                        return () => definition;
4,085✔
420
                }
421
        }
422

423
        _makeType(name, definition) {
424
                let type = "";
4,194✔
425
                let enumArray = [];
4,194✔
426
                if (Array.isArray(definition.type)) {
4,194✔
427
                        type = AttributeTypes.enum;
96✔
428
                        enumArray = [...definition.type];
96✔
429
                // } else if (definition.type === AttributeTypes.set && Array.isArray(definition.items)) {
430
                        // type = AttributeTypes.enumSet;
431
                        // enumArray = [...definition.items];
432
                } else {
433
                        type = definition.type || "string";
4,098✔
434
                }
435
                if (!AttributeTypeNames.includes(type)) {
4,194✔
436
                        throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid "type" property for attribute: "${name}". Acceptable types include ${AttributeTypeNames.join(", ")}`);
1✔
437
                }
438
                return { type, enumArray };
4,193✔
439
        }
440

441
        isWatcher() {
442
                return this._isWatcher;
3,895✔
443
        }
444

445
        isWatched() {
446
                return this._isWatched;
×
447
        }
448

449
        isWatching(attribute) {
450
                return this.watching[attribute] !== undefined;
×
451
        }
452

453
        isWatchedBy(attribute) {
454
                return this.watchedBy[attribute] !== undefined;
×
455
        }
456

457
        _isType(value) {
458
                if (value === undefined) {
40,263✔
459
                        let reason = [];
933✔
460
                        if (this.required) {
933✔
461
                                reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`));
2✔
462
                        }
463
                        return [!this.required, reason];
933✔
464
                }
465
                let isTyped = false;
39,330✔
466
                let reason = [];
39,330✔
467
                switch (this.type) {
39,330✔
468
                        case AttributeTypes.enum:
469
                        // case AttributeTypes.enumSet:
470
                                // isTyped = this.enumArray.every(enumValue => {
471
                                //         const val = Array.isArray(value) ? value : [value];
472
                                //         return val.includes(enumValue);
473
                                // })
474
                                isTyped = this.enumArray.includes(value);
2,674✔
475
                                if (!isTyped) {
2,674✔
476
                                        reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value not found in set of acceptable values: ${u.commaSeparatedString(this.enumArray)}`));
3✔
477
                                }
478
                                break;
2,674✔
479
                        case AttributeTypes.any:
480
                        case AttributeTypes.static:
481
                        case AttributeTypes.custom:
482
                                isTyped = true;
2,145✔
483
                                break;
2,145✔
484
                        case AttributeTypes.string:
485
                        case AttributeTypes.number:
486
                        case AttributeTypes.boolean:
487
                        default:
488
                                isTyped = typeof value === this.type;
34,511✔
489
                                if (!isTyped) {
34,511✔
490
                                        reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Received value of type "${typeof value}", expected value of type "${this.type}"`));
25✔
491
                                }
492
                                break;
34,511✔
493
                }
494
                return [isTyped, reason];
39,330✔
495
        }
496

497
        isValid(value) {
498
                try {
41,188✔
499
                        let [isTyped, typeErrorReason] = this._isType(value);
41,188✔
500
                        let [isValid, validationError] = isTyped ? this.validate(value) : [false, []];
41,188✔
501
                        let errors = [...typeErrorReason, ...validationError].filter(value => value !== undefined);
41,188✔
502
                        return [isTyped && isValid, errors];
41,188✔
503
                } catch (err) {
504
                        return [false, [err]];
×
505
                }
506
        }
507

508
        val(value) {
509
                value = this.cast(value);
39,920✔
510
                if (value === undefined) {
39,920✔
511
                        value = this.default();
1,128✔
512
                }
513
                return value;
39,920✔
514
        }
515

516
        getValidate(value) {
517
                value = this.val(value);
40,012✔
518
                let [isValid, validationErrors] = this.isValid(value);
39,994✔
519
                if (!isValid) {
39,994✔
520
                        throw new e.ElectroValidationError(validationErrors);
72✔
521
                }
522
                return value;
39,922✔
523
        }
524
}
525

526
class MapAttribute extends Attribute {
527
        constructor(definition) {
528
                super(definition);
90✔
529
                const properties = Attribute.buildChildMapProperties(definition, {
90✔
530
                        parentType: this.type,
531
                        parentPath: this.path,
532
                        traverser: this.traverser
533
                });
534
                this.properties = properties;
88✔
535
                this.isRoot = !!definition.isRoot;
88✔
536
                this.get = this._makeGet(definition.get, properties);
88✔
537
                this.set = this._makeSet(definition.set, properties);
88✔
538
        }
539

540
        _makeGet(get, properties) {
541
                this._checkGetSet(get, "get");
178✔
542
                const getter = get || ((val) => {
178✔
543
                        const isEmpty = !val || Object.keys(val).length === 0;
314✔
544
                        const isNotRequired = !this.required;
314✔
545
                        const isRoot = this.isRoot;
314✔
546
                        if (isEmpty && isRoot && !isNotRequired) {
314!
547
                                return undefined;
×
548
                        }
549
                        return val;
314✔
550
                });
551
                return (values, siblings) => {
178✔
552
                        const data = {};
323✔
553

554
                        if (this.hidden) {
323!
555
                                return;
×
556
                        }
557

558
                        if (values === undefined) {
323!
559
                                if (!get) {
×
560
                                        return undefined;
×
561
                                }
562
                                return getter(data, siblings);
×
563
                        }
564

565
                        for (const name of Object.keys(properties.attributes)) {
323✔
566
                                const attribute = properties.attributes[name];
661✔
567
                                if (values[attribute.field] !== undefined) {
661✔
568
                                        let results = attribute.get(values[attribute.field], {...values});
544✔
569
                                        if (results !== undefined) {
544!
570
                                                data[name] = results;
544✔
571
                                        }
572
                                }
573
                        }
574

575

576
                        return getter(data, siblings);
323✔
577
                }
578
        }
579

580
        _makeSet(set, properties) {
581
                this._checkGetSet(set, "set");
178✔
582
                const setter = set || ((val) => {
178✔
583
                        const isEmpty = !val || Object.keys(val).length === 0;
229✔
584
                        const isNotRequired = !this.required;
229✔
585
                        const isRoot = this.isRoot;
229✔
586
                        if (isEmpty && isRoot && !isNotRequired) {
229!
587
                                return undefined;
×
588
                        }
589
                        return val;
229✔
590
                });
591

592
                return (values, siblings) => {
178✔
593
                        const data = {};
242✔
594
                        if (values === undefined) {
242✔
595
                                if (!set) {
7✔
596
                                        return undefined;
4✔
597
                                }
598
                                return setter(values, siblings);
3✔
599
                        }
600
                        for (const name of Object.keys(properties.attributes)) {
235✔
601
                                const attribute = properties.attributes[name];
507✔
602
                                if (values[name] !== undefined) {
507✔
603
                                        const results = attribute.set(values[name], {...values});
416✔
604
                                        if (results !== undefined) {
416!
605
                                                data[attribute.field] = results;
416✔
606
                                        }
607
                                }
608
                        }
609
                        return setter(data, siblings);
235✔
610
                }
611
        }
612

613
        _isType(value) {
614
                if (value === undefined) {
291✔
615
                        let reason = [];
22✔
616
                        if (this.required) {
22✔
617
                                reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`));
1✔
618
                        }
619
                        return [!this.required, reason];
22✔
620
                }
621
                const valueType = getValueType(value);
269✔
622
                if (valueType !== ValueTypes.object) {
269!
623
                        return [false, [new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path "${this.path}. Received value of type "${valueType}", expected value of type "object"`)]];
×
624
                }
625
                let reason = [];
269✔
626
                const [childrenAreValid, childErrors] = this._validateChildren(value);
269✔
627
                if (!childrenAreValid) {
269✔
628
                        reason = childErrors;
26✔
629
                }
630
                return [childrenAreValid, reason]
269✔
631
        }
632

633
        _validateChildren(value) {
634
                const valueType = getValueType(value);
269✔
635
                const attributes = this.properties.attributes;
269✔
636
                let errors = [];
269✔
637
                if (valueType === ValueTypes.object) {
269!
638
                        for (const child of Object.keys(attributes)) {
269✔
639
                                const [isValid, errorValues] = attributes[child].isValid(value === undefined ? value : value[child])
614!
640
                                if (!isValid) {
614✔
641
                                        errors = [...errors, ...errorValues]
42✔
642
                                }
643
                        }
644
                } else if (valueType !== ValueTypes.object) {
×
645
                        errors.push(
×
646
                                new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Expected value to be an object to fulfill attribute type "${this.type}"`)
647
                        );
648
                } else if (this.properties.hasRequiredAttributes) {
×
649
                        errors.push(
×
650
                                new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Map attribute requires at least the properties ${u.commaSeparatedString(Object.keys(attributes))}`)
651
                        );
652
                }
653
                return [errors.length === 0, errors];
269✔
654
        }
655

656
        val(value) {
657
                const incomingIsEmpty = value === undefined;
305✔
658
                let fromDefault = false;
305✔
659
                let data;
660
                if (value === undefined) {
305✔
661
                        data = this.default();
40✔
662
                        if (data !== undefined) {
40✔
663
                                fromDefault = true;
18✔
664
                        }
665
                } else {
666
                        data = value;
265✔
667
                }
668

669
                const valueType = getValueType(data);
305✔
670

671
                if (data === undefined) {
305✔
672
                        return data;
22✔
673
                } else if (valueType !== "object") {
283✔
674
                        throw new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Expected value to be an object to fulfill attribute type "${this.type}"`);
4✔
675
                }
676

677
                const response = {};
279✔
678

679
                for (const name of Object.keys(this.properties.attributes)) {
279✔
680
                        const attribute = this.properties.attributes[name];
630✔
681
                        const results = attribute.val(data[attribute.name]);
630✔
682
                        if (results !== undefined) {
620✔
683
                                response[name] = results;
471✔
684
                        }
685
                }
686

687
                if (Object.keys(response).length === 0 && !fromDefault && this.isRoot && !this.required && incomingIsEmpty) {
269!
688
                        return undefined;
×
689
                }
690

691
                return response;
269✔
692
        }
693
}
694

695
class ListAttribute extends Attribute {
696
        constructor(definition) {
697
                super(definition);
52✔
698
                const items = Attribute.buildChildListItems(definition, {
52✔
699
                        parentType: this.type,
700
                        parentPath: this.path,
701
                        traverser: this.traverser
702
                });
703
                this.items = items;
50✔
704
                this.get = this._makeGet(definition.get, items);
50✔
705
                this.set = this._makeSet(definition.set, items);
50✔
706
        }
707

708
        _makeGet(get, items) {
709
                this._checkGetSet(get, "get");
102✔
710

711
                const getter = get || ((attr) => attr);
149✔
712

713
                return (values, siblings) => {
102✔
714
                        const data = [];
166✔
715

716
                        if (this.hidden) {
166!
717
                                return;
×
718
                        }
719

720
                        if (values === undefined) {
166!
721
                                return getter(data, siblings);
×
722
                        }
723

724
                        for (let value of values) {
166✔
725
                                const results = items.get(value, [...values]);
263✔
726
                                if (results !== undefined) {
263!
727
                                        data.push(results);
263✔
728
                                }
729
                        }
730

731
                        return getter(data, siblings);
166✔
732
                }
733
        }
734

735
        _makeSet(set, items) {
736
                this._checkGetSet(set, "set");
102✔
737
                const setter = set || ((attr) => attr);
190✔
738
                return (values, siblings) => {
102✔
739
                        const data = [];
205✔
740

741
                        if (values === undefined) {
205✔
742
                                return setter(values, siblings);
65✔
743
                        }
744

745
                        for (const value of values) {
140✔
746
                                const results = items.set(value, [...values]);
197✔
747
                                if (results !== undefined) {
197!
748
                                        data.push(results);
197✔
749
                                }
750
                        }
751

752
                        return setter(data, siblings);
140✔
753
                }
754
        }
755

756
        _validateArrayValue(value) {
757
                const reason = [];
164✔
758
                const valueType = getValueType(value);
164✔
759
                if (value !== undefined && valueType !== ValueTypes.array) {
164!
760
                        return [false, [new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path "${this.path}. Received value of type "${valueType}", expected value of type "array"`)]];
×
761
                } else {
762
                        return [true, []];
164✔
763
                }
764
        }
765

766
        _isType(value) {
767
                if (value === undefined) {
244✔
768
                        let reason = [];
80✔
769
                        if (this.required) {
80✔
770
                                reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`));
1✔
771
                        }
772
                        return [!this.required, reason];
80✔
773
                }
774

775
                const [isValidArray, errors] = this._validateArrayValue(value);
164✔
776
                if (!isValidArray) {
164!
777
                        return [isValidArray, errors];
×
778
                }
779
                let reason = [];
164✔
780
                const [childrenAreValid, childErrors] = this._validateChildren(value);
164✔
781
                if (!childrenAreValid) {
164✔
782
                        reason = childErrors;
14✔
783
                }
784
                return [childrenAreValid, reason]
164✔
785
        }
786

787
        _validateChildren(value) {
788
                const valueType = getValueType(value);
164✔
789
                const errors = [];
164✔
790
                if (valueType === ValueTypes.array) {
164!
791
                        for (const i in value) {
164✔
792
                                const [isValid, errorValues] = this.items.isValid(value[i]);
231✔
793
                                if (!isValid) {
231✔
794
                                        for (const err of errorValues) {
24✔
795
                                                if (err instanceof e.ElectroAttributeValidationError || err instanceof e.ElectroUserValidationError) {
26!
796
                                                        err.index = parseInt(i);
26✔
797
                                                }
798
                                                errors.push(err);
26✔
799
                                        }
800
                                }
801
                        }
802
                } else {
803
                        errors.push(
×
804
                                new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Expected value to be an Array to fulfill attribute type "${this.type}"`)
805
                        );
806
                }
807
                return [errors.length === 0, errors];
164✔
808
        }
809

810
        val(value) {
811
                const getValue = (v) => {
261✔
812
                        v = this.cast(v);
88✔
813
                        if (v === undefined) {
88!
814
                                v = this.default();
88✔
815
                        }
816
                        return v;
88✔
817
                }
818

819
                const data = value === undefined
261✔
820
                        ? getValue(value)
821
                        : value;
822

823
                if (data === undefined) {
261✔
824
                        return data;
82✔
825
                } else if (!Array.isArray(data)) {
179✔
826
                        throw new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path "${this.path}. Received value of type "${getValueType(value)}", expected value of type "array"`);
9✔
827
                }
828

829
                const response = [];
170✔
830
                for (const d of data) {
170✔
831
                        const results = this.items.val(d);
237✔
832
                        if (results !== undefined) {
231!
833
                                response.push(results);
231✔
834
                        }
835
                }
836

837
                return response;
164✔
838
        }
839
}
840

841
class SetAttribute extends Attribute {
842
        constructor(definition) {
843
                super(definition);
65✔
844
                const items = Attribute.buildChildSetItems(definition, {
65✔
845
                        parentType: this.type,
846
                        parentPath: this.path,
847
                        traverser: this.traverser
848
                });
849
                this.items = items;
65✔
850
                this.get = this._makeGet(definition.get, items);
65✔
851
                this.set = this._makeSet(definition.set, items);
65✔
852
                this.validate = this._makeSetValidate(definition);
65✔
853
        }
854

855
        _makeSetValidate(definition) {
856
                const validate = this._makeValidate(definition.validate);
65✔
857
                return (value) => {
65✔
858
                        switch (getValueType(value)) {
381✔
859
                                case ValueTypes.array:
860
                                        return validate([...value]);
1✔
861
                                case ValueTypes.aws_set:
862
                                        return validate([...value.values]);
248✔
863
                                case ValueTypes.set:
864
                                        return validate(Array.from(value));
9✔
865
                                default:
866
                                        return validate(value);
123✔
867
                        }
868
                }
869
        }
870

871
        fromDDBSet(value) {
872
                switch (getValueType(value)) {
624✔
873
                        case ValueTypes.aws_set:
874
                                return [...value.values];
453✔
875
                        case ValueTypes.set:
876
                                return Array.from(value);
16✔
877
                        default:
878
                                return value;
155✔
879
                }
880
        }
881

882
        _createDDBSet(value) {
883
                if (this.client && typeof this.client.createSet === "function") {
506✔
884
                        value = Array.isArray(value)
488✔
885
                                ? Array.from(new Set(value))
886
                                : value;
887
                        return this.client.createSet(value, { validate: true });
488✔
888
                } else {
889
                        return new DynamoDBSet(value, this.items.type);
18✔
890
                }
891
        }
892

893
        acceptable(val) {
894
                return Array.isArray(val)
90✔
895
                        ? val.length > 0
896
                        : this.items.acceptable(val);
897
        }
898

899
        toDDBSet(value) {
900
                const valueType = getValueType(value);
511✔
901
                let array;
902
                switch(valueType) {
511!
903
                        case ValueTypes.set:
904
                                array = Array.from(value);
1✔
905
                                return this._createDDBSet(array);
1✔
906
                        case ValueTypes.aws_set:
907
                                return value;
×
908
                        case ValueTypes.array:
909
                                return this._createDDBSet(value);
475✔
910
                        case ValueTypes.string:
911
                        case ValueTypes.number: {
912
                                this.items.getValidate(value);
30✔
913
                                return this._createDDBSet(value);
30✔
914
                        }
915
                        default:
916
                                throw new e.ElectroAttributeValidationError(this.path, `Invalid attribute value supplied to "set" attribute "${this.path}". Received value of type "${valueType}". Set values must be supplied as either Arrays, native JavaScript Set objects, DocumentClient Set objects, strings, or numbers.`)
5✔
917
                }
918

919
        }
920

921
        _makeGet(get, items) {
922
                this._checkGetSet(get, "get");
130✔
923
                const getter = get || ((attr) => attr);
298✔
924
                return (values, siblings) => {
130✔
925
                        if (values !== undefined) {
304!
926
                                const data = this.fromDDBSet(values);
304✔
927
                                return getter(data, siblings);
304✔
928
                        }
929
                        const data = this.fromDDBSet(values);
×
930
                        const results = getter(data, siblings);
×
931
                        if (results !== undefined) {
×
932
                                // if not undefined, try to convert, else no need to return
933
                                return this.fromDDBSet(results);
×
934
                        }
935
                }
936
        }
937

938
        _makeSet(set, items) {
939
                this._checkGetSet(set, "set");
130✔
940
                const setter = set || ((attr) => attr);
316✔
941
                return (values, siblings) => {
130✔
942
                        const results = setter(this.fromDDBSet(values), siblings);
320✔
943
                        if (results !== undefined) {
320✔
944
                                return this.toDDBSet(results);
241✔
945
                        }
946
                }
947
        }
948

949
        _isType(value) {
950
                if (value === undefined) {
390✔
951
                        const reason = [];
123✔
952
                        if (this.required) {
123!
953
                                reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`));
×
954
                        }
955
                        return [!this.required, reason];
123✔
956
                }
957

958
                let reason = [];
267✔
959
                const [childrenAreValid, childErrors] = this._validateChildren(value);
267✔
960
                if (!childrenAreValid) {
267✔
961
                        reason = childErrors;
9✔
962
                }
963
                return [childrenAreValid, reason]
267✔
964
        }
965

966
        _validateChildren(value) {
967
                const valueType = getValueType(value);
267✔
968
                let errors = [];
267✔
969
                let arr = [];
267✔
970
                if (valueType === ValueTypes.array) {
267✔
971
                        arr = value;
2✔
972
                } else if (valueType === ValueTypes.set) {
265✔
973
                        arr = Array.from(value);
9✔
974
                } else if (valueType === ValueTypes.aws_set) {
256!
975
                        arr = value.values;
256✔
976
                } else {
977
                        errors.push(
×
978
                                new e.ElectroAttributeValidationError(this.path, `Invalid value type at attribute path: "${this.path}". Expected value to be an Expected value to be an Array, native JavaScript Set objects, or DocumentClient Set objects to fulfill attribute type "${this.type}"`)
979
                        );
980
                }
981
                for (const item of arr) {
267✔
982
                        const [isValid, errorValues] = this.items.isValid(item);
345✔
983
                        if (!isValid) {
345✔
984
                                errors = [...errors, ...errorValues];
13✔
985
                        }
986
                }
987
                return [errors.length === 0, errors];
267✔
988
        }
989

990
        val(value) {
991
                if (value === undefined) {
393✔
992
                        value = this.default();
123✔
993
                }
994

995
                if (value !== undefined) {
393✔
996
                        return this.toDDBSet(value);
270✔
997
                }
998
        }
999
}
1000

1001
class Schema {
1002
        constructor(properties = {}, facets = {}, {traverser = new AttributeTraverser(), client, parent, isRoot} = {}) {
722!
1003
                this._validateProperties(properties, parent);
722✔
1004
                let schema = Schema.normalizeAttributes(properties, facets, {traverser, client, parent, isRoot});
722✔
1005
                this.client = client;
702✔
1006
                this.attributes = schema.attributes;
702✔
1007
                this.enums = schema.enums;
702✔
1008
                this.translationForTable = schema.translationForTable;
702✔
1009
                this.translationForRetrieval = schema.translationForRetrieval;
702✔
1010
                this.hiddenAttributes = schema.hiddenAttributes;
702✔
1011
                this.readOnlyAttributes = schema.readOnlyAttributes;
702✔
1012
                this.requiredAttributes = schema.requiredAttributes;
702✔
1013
                this.translationForWatching = this._formatWatchTranslations(this.attributes);
702✔
1014
                this.traverser = traverser;
702✔
1015
                this.isRoot = !!isRoot;
702✔
1016
        }
1017

1018
        static normalizeAttributes(attributes = {}, facets = {}, {traverser, client, parent, isRoot} = {}) {
×
1019
                const attributeHasParent = !!parent;
929✔
1020
                let invalidProperties = [];
929✔
1021
                let normalized = {};
929✔
1022
                let usedAttrs = {};
929✔
1023
                let enums = {};
929✔
1024
                let translationForTable = {};
929✔
1025
                let translationForRetrieval = {};
929✔
1026
                let watchedAttributes = {};
929✔
1027
                let requiredAttributes = new Set();
929✔
1028
                let hiddenAttributes = new Set();
929✔
1029
                let readOnlyAttributes = new Set();
929✔
1030
                let definitions = {};
929✔
1031
                for (let name in attributes) {
929✔
1032
                        let attribute = attributes[name];
4,205✔
1033
                        if (typeof attribute === AttributeTypes.string || Array.isArray(attribute)) {
4,205✔
1034
                                attribute = {
46✔
1035
                                        type: attribute
1036
                                };
1037
                        }
1038
                        const field = attribute.field || name;
4,205✔
1039
                        let isKeyField = false;
4,205✔
1040
                        let prefix = "";
4,205✔
1041
                        let postfix = "";
4,205✔
1042
                        let casing = KeyCasing.none;
4,205✔
1043
                        if (facets.byField && facets.byField[field] !== undefined) {
4,205✔
1044
                                for (const indexName of Object.keys(facets.byField[field])) {
10✔
1045
                                        let definition = facets.byField[field][indexName];
10✔
1046
                                        if (definition.facets.length > 1) {
10!
1047
                                                throw new e.ElectroError(
×
1048
                                                        e.ErrorCodes.InvalidIndexWithAttributeName,
1049
                                                        `Invalid definition for "${definition.type}" field on index "${u.formatIndexNameForDisplay(indexName)}". The ${definition.type} field "${definition.field}" shares a field name with an attribute defined on the Entity, and therefore is not allowed to contain composite references to other attributes. Please either change the field name of the attribute, or redefine the index to use only the single attribute "${definition.field}".`
1050
                                                )
1051
                                        }
1052
                                        if (definition.isCustom) {
10!
1053
                                                const keyFieldLabels = facets.labels[indexName][definition.type].labels;
10✔
1054
                                                // I am not sure how more than two would happen but it would mean either
1055
                                                // 1. Code prior has an unknown edge-case.
1056
                                                // 2. Method is being incorrectly used.
1057
                                                if (keyFieldLabels.length > 2) {
10!
1058
                                                        throw new e.ElectroError(
×
1059
                                                                e.ErrorCodes.InvalidIndexWithAttributeName,
1060
                                                                `Unexpected definition for "${definition.type}" field on index "${u.formatIndexNameForDisplay(indexName)}". The ${definition.type} field "${definition.field}" shares a field name with an attribute defined on the Entity, and therefore is not possible to have more than two labels as part of it's template. Please either change the field name of the attribute, or reformat the key template to reduce all pre-fixing or post-fixing text around the attribute reference to two.`
1061
                                                        )
1062
                                                }
1063
                                                isKeyField = true;
10✔
1064
                                                casing = definition.casing;
10✔
1065
                                                // Walk through the labels, given the above exception handling, I'd expect the first element to
1066
                                                // be the prefix and the second element to be the postfix.
1067
                                                for (const value of keyFieldLabels) {
10✔
1068
                                                        if (value.name === field) {
15✔
1069
                                                                prefix = value.label || "";
10✔
1070
                                                        } else {
1071
                                                                postfix = value.label || "";
5!
1072
                                                        }
1073
                                                }
1074
                                                if (attribute.type !== AttributeTypes.string && !Array.isArray(attribute.type)) {
10✔
1075
                                                        if (prefix.length > 0 || postfix.length > 0) {
3✔
1076
                                                                throw new e.ElectroError(e.ErrorCodes.InvalidIndexWithAttributeName, `definition for "${definition.type}" field on index "${u.formatIndexNameForDisplay(indexName)}". Index templates may only have prefix or postfix values on "string" or "enum" type attributes. The ${definition.type} field "${field}" is type "${attribute.type}", and therefore cannot be used with prefixes or postfixes. Please either remove the prefixed or postfixed values from the template or change the field name of the attribute.`);
1✔
1077
                                                        }
1078
                                                }
1079
                                        } else {
1080
                                                // Upstream middleware should have taken care of this. An error here would mean:
1081
                                                // 1. Code prior has an unknown edge-case.
1082
                                                // 2. Method is being incorrectly used.
1083
                                                throw new e.ElectroError(
×
1084
                                                        e.ErrorCodes.InvalidIndexCompositeWithAttributeName,
1085
                                                        `Unexpected definition for "${definition.type}" field on index "${u.formatIndexNameForDisplay(indexName)}". The ${definition.type} field "${definition.field}" shares a field name with an attribute defined on the Entity, and therefore must be defined with a template. Please either change the field name of the attribute, or add a key template to the "${definition.type}" field on index "${u.formatIndexNameForDisplay(indexName)}" with the value: "\${${definition.field}}"`
1086
                                                )
1087
                                        }
1088

1089
                                        if (definition.inCollection) {
9✔
1090
                                                throw new e.ElectroError(
1✔
1091
                                                        e.ErrorCodes.InvalidCollectionOnIndexWithAttributeFieldNames,
1092
                                                        `Invalid use of a collection on index "${u.formatIndexNameForDisplay(indexName)}". The ${definition.type} field "${definition.field}" shares a field name with an attribute defined on the Entity, and therefore the index is not allowed to participate in a Collection. Please either change the field name of the attribute, or remove all collection(s) from the index.`
1093
                                                )
1094
                                        }
1095

1096
                                        if (definition.field === field) {
8!
1097
                                                if (attribute.padding !== undefined) {
8✔
1098
                                                        throw new e.ElectroError(
1✔
1099
                                                                e.ErrorCodes.InvalidAttributeDefinition,
1100
                                                                `Invalid padding definition for the attribute "${name}". Padding is not currently supported for attributes that are also defined as table indexes.`
1101
                                                        );
1102
                                                }
1103
                                        }
1104
                                }
1105
                        }
1106

1107
                        let isKey = !!facets.byIndex && facets.byIndex[TableIndex].all.find((facet) => facet.name === name);
7,958✔
1108
                        let definition = {
4,202✔
1109
                                name,
1110
                                field,
1111
                                client,
1112
                                casing,
1113
                                prefix,
1114
                                postfix,
1115
                                traverser,
1116
                                isKeyField,
1117
                                isRoot: !!isRoot,
1118
                                label: attribute.label,
1119
                                required: !!attribute.required,
1120
                                default: attribute.default,
1121
                                validate: attribute.validate,
1122
                                readOnly: !!attribute.readOnly || isKey,
8,388✔
1123
                                hidden: !!attribute.hidden,
1124
                                indexes: (facets.byAttr && facets.byAttr[name]) || [],
9,567✔
1125
                                type: attribute.type,
1126
                                get: attribute.get,
1127
                                set: attribute.set,
1128
                                watching: Array.isArray(attribute.watch) ? attribute.watch : [],
4,202✔
1129
                                items: attribute.items,
1130
                                properties: attribute.properties,
1131
                                parentPath: attribute.parentPath,
1132
                                parentType: attribute.parentType,
1133
                                padding: attribute.padding,
1134
                        };
1135

1136
                        if (definition.type === AttributeTypes.custom) {
4,202✔
1137
                                definition.type = AttributeTypes.any;
2✔
1138
                        }
1139

1140
                        if (attribute.watch !== undefined) {
4,202✔
1141
                                if (attribute.watch === AttributeWildCard) {
54✔
1142
                                        definition.watchAll = true;
6✔
1143
                                        definition.watching = [];
6✔
1144
                                } else if (Array.isArray(attribute.watch)) {
48!
1145
                                        definition.watching = attribute.watch;
48✔
1146
                                } else {
1147
                                        throw new e.ElectroError(e.ErrorCodes.InvalidAttributeWatchDefinition, `Attribute Validation Error. The attribute '${name}' is defined to "watch" an invalid value of: '${attribute.watch}'. The watch property must either be a an array of attribute names, or the single string value of "${WatchAll}".`);
×
1148
                                }
1149
                        } else {
1150
                                definition.watching = [];
4,148✔
1151
                        }
1152

1153
                        if (definition.readOnly) {
4,202✔
1154
                                readOnlyAttributes.add(name);
1,585✔
1155
                        }
1156

1157
                        if (definition.hidden) {
4,202✔
1158
                                hiddenAttributes.add(name);
13✔
1159
                        }
1160

1161
                        if (definition.required) {
4,202✔
1162
                                requiredAttributes.add(name);
434✔
1163
                        }
1164

1165
                        if (facets.byAttr && facets.byAttr[definition.name] !== undefined && (!ValidFacetTypes.includes(definition.type) && !Array.isArray(definition.type))) {
4,202✔
1166
                                let assignedIndexes = facets.byAttr[name].map(assigned => assigned.index === "" ? "Table Index" : assigned.index);
6!
1167
                                throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid composite attribute definition: Composite attributes must be one of the following: ${ValidFacetTypes.join(", ")}. The attribute "${name}" is defined as being type "${attribute.type}" but is a composite attribute of the the following indexes: ${assignedIndexes.join(", ")}`);
6✔
1168
                        }
1169

1170
                        if (usedAttrs[definition.field] || usedAttrs[name]) {
4,196✔
1171
                                invalidProperties.push({
1✔
1172
                                        name,
1173
                                        property: "field",
1174
                                        value: definition.field,
1175
                                        expected: `Unique field property, already used by attribute ${
1176
                                                usedAttrs[definition.field]
1177
                                        }`,
1178
                                });
1179
                        } else {
1180
                                usedAttrs[definition.field] = definition.name;
4,195✔
1181
                        }
1182

1183
                        translationForTable[definition.name] = definition.field;
4,196✔
1184
                        translationForRetrieval[definition.field] = definition.name;
4,196✔
1185

1186
                        for (let watched of definition.watching) {
4,196✔
1187
                                watchedAttributes[watched] = watchedAttributes[watched] || [];
56✔
1188
                                watchedAttributes[watched].push(name);
56✔
1189
                        }
1190

1191
                        definitions[name] = definition;
4,196✔
1192
                }
1193

1194
                for (let name of Object.keys(definitions)) {
920✔
1195
                        const definition = definitions[name];
4,190✔
1196

1197
                        definition.watchedBy = Array.isArray(watchedAttributes[name])
4,190✔
1198
                                ? watchedAttributes[name]
1199
                                : [];
1200

1201
                        switch(definition.type) {
4,190✔
1202
                                case AttributeTypes.map:
1203
                                        normalized[name] = new MapAttribute(definition);
90✔
1204
                                        break;
88✔
1205
                                case AttributeTypes.list:
1206
                                        normalized[name] = new ListAttribute(definition);
52✔
1207
                                        break;
50✔
1208
                                case AttributeTypes.set:
1209
                                        normalized[name] = new SetAttribute(definition);
63✔
1210
                                        break;
63✔
1211
                                case AttributeTypes.any:
1212
                                        if (attributeHasParent) {
40✔
1213
                                                throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid attribute "${definition.name}" defined within "${parent.parentPath}". Attributes with type ${u.commaSeparatedString([AttributeTypes.any, AttributeTypes.custom])} are only supported as root level attributes.`);
4✔
1214
                                        }
1215
                                default:
1216
                                        normalized[name] = new Attribute(definition);
3,981✔
1217
                        }
1218
                }
1219

1220
                let watchedWatchers = [];
911✔
1221
                let watchingUnknownAttributes = [];
911✔
1222
                for (let watched of Object.keys(watchedAttributes)) {
911✔
1223
                        if (normalized[watched] === undefined) {
42✔
1224
                                for (let attribute of watchedAttributes[watched]) {
2✔
1225
                                        watchingUnknownAttributes.push({attribute, watched});
2✔
1226
                                }
1227
                        } else if (normalized[watched].isWatcher()) {
40✔
1228
                                for (let attribute of watchedAttributes[watched]) {
6✔
1229
                                        watchedWatchers.push({attribute, watched});
10✔
1230
                                }
1231
                        }
1232
                }
1233

1234
                if (watchingUnknownAttributes.length > 0) {
911✔
1235
                        throw new e.ElectroError(e.ErrorCodes.InvalidAttributeWatchDefinition, `Attribute Validation Error. The following attributes are defined to "watch" invalid/unknown attributes: ${watchingUnknownAttributes.map(({watched, attribute}) => `"${attribute}"->"${watched}"`).join(", ")}.`);
2✔
1236
                }
1237

1238
                if (watchedWatchers.length > 0) {
909✔
1239
                        throw new e.ElectroError(e.ErrorCodes.InvalidAttributeWatchDefinition, `Attribute Validation Error. Attributes may only "watch" other attributes also watch attributes. The following attributes are defined with ineligible attributes to watch: ${watchedWatchers.map(({attribute, watched}) => `"${attribute}"->"${watched}"`).join(", ")}.`)
10✔
1240
                }
1241

1242
                let missingFacetAttributes = Array.isArray(facets.attributes)
907✔
1243
                        ? facets.attributes
1244
                                .filter(({ name }) => !normalized[name])
3,429✔
1245
                                .map((facet) => `"${facet.type}: ${facet.name}"`)
3✔
1246
                        : []
1247
                if (missingFacetAttributes.length > 0) {
907✔
1248
                        throw new e.ElectroError(e.ErrorCodes.InvalidKeyCompositeAttributeTemplate, `Invalid key composite attribute template. The following composite attribute attributes were described in the key composite attribute template but were not included model's attributes: ${missingFacetAttributes.join(", ")}`);
1✔
1249
                }
1250
                if (invalidProperties.length > 0) {
906✔
1251
                        let message = invalidProperties.map((prop) => `Schema Validation Error. Attribute "${prop.name}" property "${prop.property}". Received: "${prop.value}", Expected: "${prop.expected}"`);
1✔
1252
                        throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, message);
1✔
1253
                } else {
1254
                        return {
905✔
1255
                                enums,
1256
                                hiddenAttributes,
1257
                                readOnlyAttributes,
1258
                                requiredAttributes,
1259
                                translationForTable,
1260
                                translationForRetrieval,
1261
                                attributes: normalized,
1262
                        };
1263
                }
1264
        }
1265

1266
        _validateProperties() {}
1267

1268
        _formatWatchTranslations(attributes) {
1269
                let watchersToAttributes = {};
702✔
1270
                let attributesToWatchers = {};
702✔
1271
                let watchAllAttributes = {};
702✔
1272
                let hasWatchers = false;
702✔
1273
                for (let name of Object.keys(attributes)) {
702✔
1274
                        if (attributes[name].isWatcher()) {
3,855✔
1275
                                hasWatchers = true;
34✔
1276
                                watchersToAttributes[name] = attributes[name].watching;
34✔
1277
                        } else if (attributes[name].watchAll) {
3,821✔
1278
                                hasWatchers = true;
6✔
1279
                                watchAllAttributes[name] = name;
6✔
1280
                        } else {
1281
                                attributesToWatchers[name] = attributesToWatchers[name] || {};
3,815✔
1282
                                attributesToWatchers[name] = attributes[name].watchedBy;
3,815✔
1283
                        }
1284
                }
1285
                return {
702✔
1286
                        hasWatchers,
1287
                        watchAllAttributes,
1288
                        watchersToAttributes,
1289
                        attributesToWatchers
1290
                };
1291
        }
1292

1293
        getAttribute(path) {
1294
                return this.traverser.getPath(path);
127,423✔
1295
        }
1296

1297
        getLabels() {
1298
                let labels = {};
×
1299
                for (let name of Object.keys(this.attributes)) {
×
1300
                        let label = this.attributes[name].label;
×
1301
                        if (label !== undefined) {
×
1302
                                labels[name] = label;
×
1303
                        }
1304
                }
1305
                return labels;
×
1306
        };
1307

1308
        getLabels() {
1309
                let labels = {};
699✔
1310
                for (let name of Object.keys(this.attributes)) {
699✔
1311
                        let label = this.attributes[name].label;
3,852✔
1312
                        if (label !== undefined) {
3,852✔
1313
                                labels[name] = label;
7✔
1314
                        }
1315
                }
1316
                return labels;
699✔
1317
        };
1318

1319
        _applyAttributeMutation(method, include, avoid, payload) {
1320
                let data = { ...payload };
12,076✔
1321
                for (let path of Object.keys(include)) {
12,076✔
1322
                        // this.attributes[attribute] !== undefined | Attribute exists as actual attribute. If `includeKeys` is turned on for example this will include values that do not have a presence in the model and therefore will not have a `.get()` method
1323
                        // avoid[attribute] === undefined           | Attribute shouldn't be in the avoided
1324
                        const attribute = this.getAttribute(path);
78,575✔
1325
                        if (attribute !== undefined && avoid[path] === undefined) {
78,575✔
1326
                                data[path] = attribute[method](payload[path], {...payload});
76,806✔
1327
                        }
1328
                }
1329
                return data;
12,076✔
1330
        }
1331

1332
        _fulfillAttributeMutationMethod(method, payload) {
1333
                let watchersToTrigger = {};
11,818✔
1334
                // include: payload               | We want to hit the getters/setters for any attributes coming in to be changed
1335
                // avoid: watchersToAttributes    | We want to avoid anything that is a watcher, even if it was included
1336
                let avoid = {...this.translationForWatching.watchersToAttributes, ...this.translationForWatching.watchAllAttributes};
11,818✔
1337
                let data = this._applyAttributeMutation(method, payload, avoid, payload);
11,818✔
1338
                // `data` here will include all the original payload values, but with the mutations applied to on non-watchers
1339
                if (!this.translationForWatching.hasWatchers) {
11,818✔
1340
                        // exit early, why not
1341
                        return data;
11,560✔
1342
                }
1343
                for (let attribute of Object.keys(data)) {
258✔
1344
                        let watchers = this.translationForWatching.attributesToWatchers[attribute];
1,743✔
1345
                        // Any of the attributes on data have a watcher?
1346
                        if (watchers !== undefined) {
1,743✔
1347
                                watchersToTrigger = {...watchersToTrigger, ...watchers}
1,497✔
1348
                        }
1349
                }
1350

1351
                // include: ...data, ...watchersToTrigger | We want to hit attributes that were watching an attribute included in data, and include an properties that were skipped because they were a watcher
1352
                // avoid: attributesToWatchers            | We want to avoid hit anything that was not a watcher because they were already hit once above
1353
                let include = {...data, ...watchersToTrigger, ...this.translationForWatching.watchAllAttributes};
258✔
1354
                return this._applyAttributeMutation(method, include, this.translationForWatching.attributesToWatchers, data);
258✔
1355
        }
1356

1357
        applyAttributeGetters(payload = {}) {
×
1358
                return this._fulfillAttributeMutationMethod(AttributeMutationMethods.get, payload);
×
1359
        }
1360

1361
        applyAttributeSetters(payload = {}) {
×
1362
                return this._fulfillAttributeMutationMethod(AttributeMutationMethods.set, payload);
6,047✔
1363
        }
1364

1365
        translateFromFields(item = {}, options = {}) {
×
1366
                let { includeKeys } = options;
5,771✔
1367
                let data = {};
5,771✔
1368
                let names = this.translationForRetrieval;
5,771✔
1369
                for (let [attr, value] of Object.entries(item)) {
5,771✔
1370
                        let name = names[attr];
80,643✔
1371
                        if (name) {
80,643✔
1372
                                data[name] = value;
37,462✔
1373
                        } else if (includeKeys) {
43,181✔
1374
                                data[attr] = value;
24✔
1375
                        }
1376
                }
1377
                return data;
5,771✔
1378
        }
1379

1380
        translateToFields(payload = {}) {
×
1381
                let record = {};
5,739✔
1382
                for (let [name, value] of Object.entries(payload)) {
5,739✔
1383
                        let field = this.getFieldName(name);
38,893✔
1384
                        if (value !== undefined) {
38,893✔
1385
                                record[field] = value;
38,015✔
1386
                        }
1387
                }
1388
                return record;
5,739✔
1389
        }
1390

1391
        getFieldName(name) {
1392
                if (typeof name === 'string') {
38,948✔
1393
                        return this.translationForTable[name];
38,946✔
1394
                }
1395
        }
1396

1397
        checkCreate(payload = {}) {
×
1398
                let record = {};
5,898✔
1399
                for (let attribute of Object.values(this.attributes)) {
5,898✔
1400
                        let value = payload[attribute.name];
39,499✔
1401
                        record[attribute.name] = attribute.getValidate(value);
39,499✔
1402
                }
1403
                return record;
5,848✔
1404
        }
1405

1406
        checkRemove(paths = []) {
×
1407
                for (const path of paths) {
29✔
1408
                        const attribute = this.traverser.getPath(path);
44✔
1409
                        if (!attribute) {
44!
1410
                                throw new e.ElectroAttributeValidationError(path, `Attribute "${path}" does not exist on model.`);
×
1411
                        } else if (attribute.readOnly) {
44✔
1412
                                throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Read-Only and cannot be removed`);
2✔
1413
                        } else if (attribute.required) {
42✔
1414
                                throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Required and cannot be removed`);
2✔
1415
                        }
1416
                }
1417
                return paths;
25✔
1418
        }
1419

1420
        checkOperation(attribute, operation, value) {
1421
                if (attribute.required && operation === ItemOperations.remove) {
155✔
1422
                        throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Required and cannot be removed`);
2✔
1423
                } else if (attribute.readOnly) {
153✔
1424
                        throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Read-Only and cannot be updated`);
3✔
1425
                }
1426

1427
                return value === undefined
150✔
1428
                        ? undefined
1429
                        : attribute.getValidate(value);
1430
        }
1431

1432
        checkUpdate(payload = {}) {
×
1433
                let record = {};
340✔
1434
                for (let [path, attribute] of this.traverser.getAll()) {
340✔
1435
                        let value = payload[path];
4,534✔
1436
                        if (value === undefined) {
4,534✔
1437
                                continue;
4,141✔
1438
                        }
1439
                        if (attribute.readOnly) {
393✔
1440
                                // todo: #electroerror
1441
                                throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Read-Only and cannot be updated`);
10✔
1442
                        } else {
1443
                                record[path] = attribute.getValidate(value);
383✔
1444
                        }
1445
                }
1446
                return record;
290✔
1447
        }
1448

1449
        getReadOnly() {
1450
                return Array.from(this.readOnlyAttributes);
307✔
1451
        }
1452

1453
        getRequired() {
1454
                return Array.from(this.requiredAttributes);
×
1455
        }
1456

1457
        formatItemForRetrieval(item, config) {
1458
                let returnAttributes = new Set(config.attributes || []);
5,771✔
1459
                let hasUserSpecifiedReturnAttributes = returnAttributes.size > 0;
5,771✔
1460
                let remapped = this.translateFromFields(item, config);
5,771✔
1461
                let data = this._fulfillAttributeMutationMethod("get", remapped);
5,771✔
1462
                if (this.hiddenAttributes.size > 0 || hasUserSpecifiedReturnAttributes) {
5,771✔
1463
                        for (let attribute of Object.keys(data)) {
30✔
1464
                                if (this.hiddenAttributes.has(attribute)) {
118✔
1465
                                        delete data[attribute];
34✔
1466
                                }
1467
                                if (hasUserSpecifiedReturnAttributes && !returnAttributes.has(attribute)) {
118✔
1468
                                        delete data[attribute];
7✔
1469
                                }
1470
                        }
1471
                }
1472
                return data;
5,771✔
1473
        }
1474
}
1475

1476
function createCustomAttribute(definition = {}) {
×
1477
        return {
2✔
1478
                ...definition,
1479
                type: 'custom'
1480
        };
1481
}
1482

1483
function CustomAttributeType(base) {
1484
        const supported = ['string', 'number', 'boolean', 'any'];
4✔
1485
        if (!supported.includes(base)) {
4!
1486
                throw new Error(`OpaquePrimitiveType only supports base types: ${u.commaSeparatedString(supported)}`);
×
1487
        }
1488
        return base;
4✔
1489
}
1490

1491
function createSchema(schema) {
1492
        return v.model(schema);
×
1493
}
1494

1495
module.exports = {
1✔
1496
        Schema,
1497
        Attribute,
1498
        CastTypes,
1499
        SetAttribute,
1500
        createSchema,
1501
        CustomAttributeType,
1502
        createCustomAttribute,
1503
};
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