electrodb 2.10.0 → 2.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/schema.js CHANGED
@@ -1,1510 +1,1862 @@
1
- const { CastTypes, ValueTypes, KeyCasing, AttributeTypes, AttributeMutationMethods, AttributeWildCard, PathTypes, TableIndex, ItemOperations } = require("./types");
1
+ const {
2
+ CastTypes,
3
+ ValueTypes,
4
+ KeyCasing,
5
+ AttributeTypes,
6
+ AttributeMutationMethods,
7
+ AttributeWildCard,
8
+ PathTypes,
9
+ TableIndex,
10
+ ItemOperations,
11
+ } = require("./types");
2
12
  const AttributeTypeNames = Object.keys(AttributeTypes);
3
- const ValidFacetTypes = [AttributeTypes.string, AttributeTypes.number, AttributeTypes.boolean, AttributeTypes.enum];
13
+ const ValidFacetTypes = [
14
+ AttributeTypes.string,
15
+ AttributeTypes.number,
16
+ AttributeTypes.boolean,
17
+ AttributeTypes.enum,
18
+ ];
4
19
  const e = require("./errors");
5
20
  const u = require("./util");
6
21
  const v = require("./validations");
7
- const {DynamoDBSet} = require("./set");
22
+ const { DynamoDBSet } = require("./set");
8
23
 
9
24
  function getValueType(value) {
10
- if (value === undefined) {
11
- return ValueTypes.undefined;
12
- } else if (value === null) {
13
- return ValueTypes.null;
14
- } else if (typeof value === "string") {
15
- return ValueTypes.string;
16
- } else if (typeof value === "number") {
17
- return ValueTypes.number;
18
- } else if (typeof value === "boolean") {
19
- return ValueTypes.boolean;
20
- } else if (Array.isArray(value)) {
21
- return ValueTypes.array;
22
- } else if (value.wrapperName === "Set") {
23
- return ValueTypes.aws_set;
24
- } else if (value.constructor.name === "Set") {
25
- return ValueTypes.set;
26
- } else if (value.constructor.name === "Map") {
27
- return ValueTypes.map;
28
- } else if (value.constructor.name === "Object") {
29
- return ValueTypes.object;
30
- } else {
31
- return ValueTypes.unknown;
32
- }
25
+ if (value === undefined) {
26
+ return ValueTypes.undefined;
27
+ } else if (value === null) {
28
+ return ValueTypes.null;
29
+ } else if (typeof value === "string") {
30
+ return ValueTypes.string;
31
+ } else if (typeof value === "number") {
32
+ return ValueTypes.number;
33
+ } else if (typeof value === "boolean") {
34
+ return ValueTypes.boolean;
35
+ } else if (Array.isArray(value)) {
36
+ return ValueTypes.array;
37
+ } else if (value.wrapperName === "Set") {
38
+ return ValueTypes.aws_set;
39
+ } else if (value.constructor.name === "Set") {
40
+ return ValueTypes.set;
41
+ } else if (value.constructor.name === "Map") {
42
+ return ValueTypes.map;
43
+ } else if (value.constructor.name === "Object") {
44
+ return ValueTypes.object;
45
+ } else {
46
+ return ValueTypes.unknown;
47
+ }
33
48
  }
34
49
 
35
50
  class AttributeTraverser {
36
- constructor(parentTraverser) {
37
- if (parentTraverser instanceof AttributeTraverser) {
38
- this.parent = parentTraverser;
39
- this.paths = this.parent.paths;
40
- } else {
41
- this.parent = null;
42
- this.paths = new Map();
43
- }
44
- this.children = new Map();
45
- }
46
-
47
- setChild(name, attribute) {
48
- this.children.set(name, attribute);
49
- }
50
-
51
- asChild(name, attribute) {
52
- if (this.parent) {
53
- this.parent.setChild(name, attribute);
54
- }
55
- }
56
-
57
- setPath(path, attribute) {
58
- if (this.parent) {
59
- this.parent.setPath(path, attribute);
60
- }
61
- this.paths.set(path, attribute);
62
- }
63
-
64
- getPath(path) {
65
- path = u.genericizeJSONPath(path);
66
- if (this.parent) {
67
- return this.parent.getPath(path);
68
- }
69
- return this.paths.get(path);
70
- }
71
-
72
- getChild(name) {
73
- return this.children.get(name);
74
- }
75
-
76
- getAllChildren() {
77
- return this.children.entries();
78
- }
79
-
80
- getAll() {
81
- if (this.parent) {
82
- return this.parent.getAll();
83
- }
84
- return this.paths.entries();
85
- }
51
+ constructor(parentTraverser) {
52
+ if (parentTraverser instanceof AttributeTraverser) {
53
+ this.parent = parentTraverser;
54
+ this.paths = this.parent.paths;
55
+ } else {
56
+ this.parent = null;
57
+ this.paths = new Map();
58
+ }
59
+ this.children = new Map();
60
+ }
61
+
62
+ setChild(name, attribute) {
63
+ this.children.set(name, attribute);
64
+ }
65
+
66
+ asChild(name, attribute) {
67
+ if (this.parent) {
68
+ this.parent.setChild(name, attribute);
69
+ }
70
+ }
71
+
72
+ setPath(path, attribute) {
73
+ if (this.parent) {
74
+ this.parent.setPath(path, attribute);
75
+ }
76
+ this.paths.set(path, attribute);
77
+ }
78
+
79
+ getPath(path) {
80
+ path = u.genericizeJSONPath(path);
81
+ if (this.parent) {
82
+ return this.parent.getPath(path);
83
+ }
84
+ return this.paths.get(path);
85
+ }
86
+
87
+ getChild(name) {
88
+ return this.children.get(name);
89
+ }
90
+
91
+ getAllChildren() {
92
+ return this.children.entries();
93
+ }
94
+
95
+ getAll() {
96
+ if (this.parent) {
97
+ return this.parent.getAll();
98
+ }
99
+ return this.paths.entries();
100
+ }
86
101
  }
87
102
 
88
-
89
103
  class Attribute {
90
- constructor(definition = {}) {
91
- this.name = definition.name;
92
- this.field = definition.field || definition.name;
93
- this.label = definition.label;
94
- this.readOnly = !!definition.readOnly;
95
- this.hidden = !!definition.hidden;
96
- this.required = !!definition.required;
97
- this.cast = this._makeCast(definition.name, definition.cast);
98
- this.default = this._makeDefault(definition.default);
99
- this.validate = this._makeValidate(definition.validate);
100
- this.isKeyField = !!definition.isKeyField;
101
- this.unformat = this._makeDestructureKey(definition);
102
- this.format = this._makeStructureKey(definition);
103
- this.padding = definition.padding;
104
- this.applyFixings = this._makeApplyFixings(definition);
105
- this.applyPadding = this._makePadding(definition);
106
- this.indexes = [...(definition.indexes || [])];
107
- let {isWatched, isWatcher, watchedBy, watching, watchAll} = Attribute._destructureWatcher(definition);
108
- this._isWatched = isWatched
109
- this._isWatcher = isWatcher;
110
- this.watchedBy = watchedBy;
111
- this.watching = watching;
112
- this.watchAll = watchAll;
113
- let { type, enumArray } = this._makeType(this.name, definition);
114
- this.type = type;
115
- this.enumArray = enumArray;
116
- this.parentType = definition.parentType;
117
- this.parentPath = definition.parentPath;
118
- const pathType = this.getPathType(this.type, this.parentType);
119
- const path = Attribute.buildPath(this.name, pathType, this.parentPath);
120
- const fieldPath = Attribute.buildPath(this.field, pathType, this.parentType);
121
- this.path = path;
122
- this.fieldPath = fieldPath;
123
- this.traverser = new AttributeTraverser(definition.traverser);
124
- this.traverser.setPath(this.path, this);
125
- this.traverser.setPath(this.fieldPath, this);
126
- this.traverser.asChild(this.name, this);
127
- this.parent = { parentType: this.type, parentPath: this.path };
128
- this.get = this._makeGet(definition.get);
129
- this.set = this._makeSet(definition.set);
130
- this.getClient = definition.getClient;
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, getClient} = definition;
149
- const prop = {...items, ...parent};
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 }, {}, {getClient, traverser: parent.traverser, parent}).attributes["*"];
152
- }
153
-
154
- static buildChildSetItems(definition, parent) {
155
- const {items, getClient} = definition;
156
-
157
- const allowedTypes = [AttributeTypes.string, AttributeTypes.boolean, AttributeTypes.number, AttributeTypes.enum];
158
- if (!Array.isArray(items) && !allowedTypes.includes(items)) {
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};
162
- return Schema.normalizeAttributes({ prop }, {}, {getClient, traverser: parent.traverser, parent}).attributes.prop;
163
- }
164
-
165
- static buildChildMapProperties(definition, parent) {
166
- const {properties, getClient} = definition;
167
- if (!properties || typeof properties !== "object") {
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 = {};
171
- for (let name of Object.keys(properties)) {
172
- attributes[name] = {...properties[name], ...parent};
173
- }
174
- return Schema.normalizeAttributes(attributes, {}, {getClient, traverser: parent.traverser, parent});
175
- }
176
-
177
- static buildPath(name, type, parentPath) {
178
- if (!parentPath) return name;
179
- switch(type) {
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}`;
188
- case PathTypes.item:
189
- return `${parentPath}[*]`;
190
- case AttributeTypes.any:
191
- default:
192
- return `${parentPath}.*`;
193
- }
194
- }
195
-
196
- static _destructureWatcher(definition) {
197
- let watchAll = !!definition.watchAll;
198
- let watchingArr = watchAll ? []: [...(definition.watching || [])];
199
- let watchedByArr = [...(definition.watchedBy || [])];
200
- let isWatched = watchedByArr.length > 0;
201
- let isWatcher = watchingArr.length > 0;
202
- let watchedBy = {};
203
- let watching = {};
204
-
205
- for (let watched of watchedByArr) {
206
- watchedBy[watched] = watched;
207
- }
208
-
209
- for (let attribute of watchingArr) {
210
- watching[attribute] = attribute;
211
- }
212
-
213
- return {
214
- watchAll,
215
- watching,
216
- watchedBy,
217
- isWatched,
218
- isWatcher
219
- }
220
- }
221
-
222
- _makeGet(get) {
223
- this._checkGetSet(get, "get");
224
- const getter = get || ((attr) => attr);
225
- return (value, siblings) => {
226
- if (this.hidden) {
227
- return;
228
- }
229
- value = this.unformat(value);
230
- return getter(value, siblings);
231
- }
232
- }
233
-
234
- _makeSet(set) {
235
- this._checkGetSet(set, "set");
236
- return set || ((attr) => attr);
237
- }
238
-
239
- _makeApplyFixings({ prefix = "", postfix = "", casing= KeyCasing.none } = {}) {
240
- return (value) => {
241
- if (value === undefined) {
242
- return;
243
- }
244
-
245
- if ([AttributeTypes.string, AttributeTypes.enum].includes(this.type)) {
246
- value = `${prefix}${value}${postfix}`;
247
- }
248
-
249
- return u.formatAttributeCasing(value, casing);
250
- }
251
- }
252
-
253
- _makeStructureKey() {
254
- return (key) => {
255
- return this.applyPadding(key);
256
- }
257
- }
258
-
259
- _isPaddingEligible(padding = {} ) {
260
- return !!padding && padding.length && v.isStringHasLength(padding.char);
261
- }
262
-
263
- _makePadding({ padding = {} }) {
264
- return (value) => {
265
- if (typeof value !== 'string') {
266
- return value;
267
- } else if (this._isPaddingEligible(padding)) {
268
- return u.addPadding({padding, value});
269
- } else {
270
- return value;
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 = {}} = {}) {
293
- return (key) => {
294
- let value = "";
295
- if (![AttributeTypes.string, AttributeTypes.enum].includes(this.type) || typeof key !== "string") {
296
- return key;
297
- } else if (key.length > prefix.length) {
298
- value = u.removeFixings({prefix, postfix, value: key});
299
- } else {
300
- value = key;
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;
309
- };
310
- }
311
-
312
- acceptable(val) {
313
- return val !== undefined;
314
- }
315
-
316
- getPathType(type, parentType) {
317
- if (parentType === AttributeTypes.list || parentType === AttributeTypes.set) {
318
- return PathTypes.item;
319
- }
320
- return type;
321
- }
322
-
323
- getAttribute(path) {
324
- return this.traverser.getPath(path);
325
- }
326
-
327
- getChild(path) {
328
- if (this.type === AttributeTypes.any) {
329
- return this;
330
- } else if (!isNaN(path) && (this.type === AttributeTypes.list || this.type === AttributeTypes.set)) {
331
- // if they're asking for a number, and this is a list, children will be under "*"
332
- return this.traverser.getChild("*");
333
- } else {
334
- return this.traverser.getChild(path);
335
- }
336
- }
337
-
338
- _checkGetSet(val, type) {
339
- if (typeof val !== "function" && val !== undefined) {
340
- throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid "${type}" property for attribute ${this.path}. Please ensure value is a function or undefined.`);
341
- }
342
- }
343
-
344
- _makeCast(name, cast) {
345
- if (cast !== undefined && !CastTypes.includes(cast)) {
346
- throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid "cast" property for attribute: "${name}". Acceptable types include ${CastTypes.join(", ")}`,
347
- );
348
- } else if (cast === AttributeTypes.string) {
349
- return (val) => {
350
- if (val === undefined) {
351
- // todo: #electroerror
352
- throw new Error(`Attribute ${name} is undefined and cannot be cast to type ${cast}`);
353
- } else if (typeof val === "string") {
354
- return val;
355
- } else {
356
- return JSON.stringify(val);
357
- }
358
- };
359
- } else if (cast === AttributeTypes.number) {
360
- return (val) => {
361
- if (val === undefined) {
362
- // todo: #electroerror
363
- throw new Error(`Attribute ${name} is undefined and cannot be cast to type ${cast}`);
364
- } else if (typeof val === "number") {
365
- return val;
366
- } else {
367
- let results = Number(val);
368
- if (isNaN(results)) {
369
- // todo: #electroerror
370
- throw new Error(`Attribute ${name} cannot be cast to type ${cast}. Doing so results in NaN`);
371
- } else {
372
- return results;
373
- }
374
- }
375
- };
376
- } else {
377
- return (val) => val;
378
- }
379
- }
380
-
381
- _makeValidate(definition) {
382
- if (typeof definition === "function") {
383
- return (val) => {
384
- try {
385
- let reason = definition(val);
386
- const isValid = !reason;
387
- if (isValid) {
388
- return [isValid, []];
389
- } else if (typeof reason === "boolean") {
390
- return [isValid, [new e.ElectroUserValidationError(this.path, "Invalid value provided")]];
391
- } else {
392
- return [isValid, [new e.ElectroUserValidationError(this.path, reason)]];
393
- }
394
- } catch(err) {
395
- return [false, [new e.ElectroUserValidationError(this.path, err)]];
396
- }
397
- };
398
- } else if (definition instanceof RegExp) {
399
- return (val) => {
400
- if (val === undefined) {
401
- return [true, []];
402
- }
403
- let isValid = definition.test(val);
404
- let reason = [];
405
- if (!isValid) {
406
- reason.push(new e.ElectroUserValidationError(this.path, `Invalid value for attribute "${this.path}": Failed model defined regex`));
407
- }
408
- return [isValid, reason];
409
- };
410
- } else {
411
- return () => [true, []];
412
- }
413
- }
414
-
415
- _makeDefault(definition) {
416
- if (typeof definition === "function") {
417
- return () => definition();
418
- } else {
419
- return () => definition;
420
- }
421
- }
422
-
423
- _makeType(name, definition) {
424
- let type = "";
425
- let enumArray = [];
426
- if (Array.isArray(definition.type)) {
427
- type = AttributeTypes.enum;
428
- enumArray = [...definition.type];
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";
434
- }
435
- if (!AttributeTypeNames.includes(type)) {
436
- throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid "type" property for attribute: "${name}". Acceptable types include ${AttributeTypeNames.join(", ")}`);
437
- }
438
- return { type, enumArray };
439
- }
440
-
441
- isWatcher() {
442
- return this._isWatcher;
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) {
459
- let reason = [];
460
- if (this.required) {
461
- reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`));
462
- }
463
- return [!this.required, reason];
464
- }
465
- let isTyped = false;
466
- let reason = [];
467
- switch (this.type) {
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);
475
- if (!isTyped) {
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)}`));
477
- }
478
- break;
479
- case AttributeTypes.any:
480
- case AttributeTypes.static:
481
- case AttributeTypes.custom:
482
- isTyped = true;
483
- break;
484
- case AttributeTypes.string:
485
- case AttributeTypes.number:
486
- case AttributeTypes.boolean:
487
- default:
488
- isTyped = typeof value === this.type;
489
- if (!isTyped) {
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}"`));
491
- }
492
- break;
493
- }
494
- return [isTyped, reason];
495
- }
496
-
497
- isValid(value) {
498
- try {
499
- let [isTyped, typeErrorReason] = this._isType(value);
500
- let [isValid, validationError] = isTyped ? this.validate(value) : [false, []];
501
- let errors = [...typeErrorReason, ...validationError].filter(value => value !== undefined);
502
- return [isTyped && isValid, errors];
503
- } catch (err) {
504
- return [false, [err]];
505
- }
506
- }
507
-
508
- val(value) {
509
- value = this.cast(value);
510
- if (value === undefined) {
511
- value = this.default();
512
- }
513
- return value;
514
- }
515
-
516
- getValidate(value) {
517
- value = this.val(value);
518
- let [isValid, validationErrors] = this.isValid(value);
519
- if (!isValid) {
520
- throw new e.ElectroValidationError(validationErrors);
521
- }
522
- return value;
523
- }
104
+ constructor(definition = {}) {
105
+ this.name = definition.name;
106
+ this.field = definition.field || definition.name;
107
+ this.label = definition.label;
108
+ this.readOnly = !!definition.readOnly;
109
+ this.hidden = !!definition.hidden;
110
+ this.required = !!definition.required;
111
+ this.cast = this._makeCast(definition.name, definition.cast);
112
+ this.default = this._makeDefault(definition.default);
113
+ this.validate = this._makeValidate(definition.validate);
114
+ this.isKeyField = !!definition.isKeyField;
115
+ this.unformat = this._makeDestructureKey(definition);
116
+ this.format = this._makeStructureKey(definition);
117
+ this.padding = definition.padding;
118
+ this.applyFixings = this._makeApplyFixings(definition);
119
+ this.applyPadding = this._makePadding(definition);
120
+ this.indexes = [...(definition.indexes || [])];
121
+ let { isWatched, isWatcher, watchedBy, watching, watchAll } =
122
+ Attribute._destructureWatcher(definition);
123
+ this._isWatched = isWatched;
124
+ this._isWatcher = isWatcher;
125
+ this.watchedBy = watchedBy;
126
+ this.watching = watching;
127
+ this.watchAll = watchAll;
128
+ let { type, enumArray } = this._makeType(this.name, definition);
129
+ this.type = type;
130
+ this.enumArray = enumArray;
131
+ this.parentType = definition.parentType;
132
+ this.parentPath = definition.parentPath;
133
+ const pathType = this.getPathType(this.type, this.parentType);
134
+ const path = Attribute.buildPath(this.name, pathType, this.parentPath);
135
+ const fieldPath = Attribute.buildPath(
136
+ this.field,
137
+ pathType,
138
+ this.parentType,
139
+ );
140
+ this.path = path;
141
+ this.fieldPath = fieldPath;
142
+ this.traverser = new AttributeTraverser(definition.traverser);
143
+ this.traverser.setPath(this.path, this);
144
+ this.traverser.setPath(this.fieldPath, this);
145
+ this.traverser.asChild(this.name, this);
146
+ this.parent = { parentType: this.type, parentPath: this.path };
147
+ this.get = this._makeGet(definition.get);
148
+ this.set = this._makeSet(definition.set);
149
+ this.getClient = definition.getClient;
150
+ }
151
+
152
+ static buildChildAttributes(type, definition, parent) {
153
+ let items;
154
+ let properties;
155
+ if (type === AttributeTypes.list) {
156
+ items = Attribute.buildChildListItems(definition, parent);
157
+ } else if (type === AttributeTypes.set) {
158
+ items = Attribute.buildChildSetItems(definition, parent);
159
+ } else if (type === AttributeTypes.map) {
160
+ properties = Attribute.buildChildMapProperties(definition, parent);
161
+ }
162
+
163
+ return { items, properties };
164
+ }
165
+
166
+ static buildChildListItems(definition, parent) {
167
+ const { items, getClient } = definition;
168
+ const prop = { ...items, ...parent };
169
+ // The use of "*" is to ensure the child's name is "*" when added to the traverser and searching for the children of a list
170
+ return Schema.normalizeAttributes(
171
+ { "*": prop },
172
+ {},
173
+ { getClient, traverser: parent.traverser, parent },
174
+ ).attributes["*"];
175
+ }
176
+
177
+ static buildChildSetItems(definition, parent) {
178
+ const { items, getClient } = definition;
179
+
180
+ const allowedTypes = [
181
+ AttributeTypes.string,
182
+ AttributeTypes.boolean,
183
+ AttributeTypes.number,
184
+ AttributeTypes.enum,
185
+ ];
186
+ if (!Array.isArray(items) && !allowedTypes.includes(items)) {
187
+ throw new e.ElectroError(
188
+ e.ErrorCodes.InvalidAttributeDefinition,
189
+ `Invalid "items" definition for Set attribute: "${
190
+ definition.path
191
+ }". Acceptable item types include ${u.commaSeparatedString(
192
+ allowedTypes,
193
+ )}`,
194
+ );
195
+ }
196
+ const prop = { type: items, ...parent };
197
+ return Schema.normalizeAttributes(
198
+ { prop },
199
+ {},
200
+ { getClient, traverser: parent.traverser, parent },
201
+ ).attributes.prop;
202
+ }
203
+
204
+ static buildChildMapProperties(definition, parent) {
205
+ const { properties, getClient } = definition;
206
+ if (!properties || typeof properties !== "object") {
207
+ throw new e.ElectroError(
208
+ e.ErrorCodes.InvalidAttributeDefinition,
209
+ `Invalid "properties" definition for Map attribute: "${definition.path}". The "properties" definition must describe the attributes that the Map will accept`,
210
+ );
211
+ }
212
+ const attributes = {};
213
+ for (let name of Object.keys(properties)) {
214
+ attributes[name] = { ...properties[name], ...parent };
215
+ }
216
+ return Schema.normalizeAttributes(
217
+ attributes,
218
+ {},
219
+ { getClient, traverser: parent.traverser, parent },
220
+ );
221
+ }
222
+
223
+ static buildPath(name, type, parentPath) {
224
+ if (!parentPath) return name;
225
+ switch (type) {
226
+ case AttributeTypes.string:
227
+ case AttributeTypes.number:
228
+ case AttributeTypes.boolean:
229
+ case AttributeTypes.map:
230
+ case AttributeTypes.set:
231
+ case AttributeTypes.list:
232
+ case AttributeTypes.enum:
233
+ return `${parentPath}.${name}`;
234
+ case PathTypes.item:
235
+ return `${parentPath}[*]`;
236
+ case AttributeTypes.any:
237
+ default:
238
+ return `${parentPath}.*`;
239
+ }
240
+ }
241
+
242
+ static _destructureWatcher(definition) {
243
+ let watchAll = !!definition.watchAll;
244
+ let watchingArr = watchAll ? [] : [...(definition.watching || [])];
245
+ let watchedByArr = [...(definition.watchedBy || [])];
246
+ let isWatched = watchedByArr.length > 0;
247
+ let isWatcher = watchingArr.length > 0;
248
+ let watchedBy = {};
249
+ let watching = {};
250
+
251
+ for (let watched of watchedByArr) {
252
+ watchedBy[watched] = watched;
253
+ }
254
+
255
+ for (let attribute of watchingArr) {
256
+ watching[attribute] = attribute;
257
+ }
258
+
259
+ return {
260
+ watchAll,
261
+ watching,
262
+ watchedBy,
263
+ isWatched,
264
+ isWatcher,
265
+ };
266
+ }
267
+
268
+ _makeGet(get) {
269
+ this._checkGetSet(get, "get");
270
+ const getter = get || ((attr) => attr);
271
+ return (value, siblings) => {
272
+ if (this.hidden) {
273
+ return;
274
+ }
275
+ value = this.unformat(value);
276
+ return getter(value, siblings);
277
+ };
278
+ }
279
+
280
+ _makeSet(set) {
281
+ this._checkGetSet(set, "set");
282
+ return set || ((attr) => attr);
283
+ }
284
+
285
+ _makeApplyFixings({
286
+ prefix = "",
287
+ postfix = "",
288
+ casing = KeyCasing.none,
289
+ } = {}) {
290
+ return (value) => {
291
+ if (value === undefined) {
292
+ return;
293
+ }
294
+
295
+ if ([AttributeTypes.string, AttributeTypes.enum].includes(this.type)) {
296
+ value = `${prefix}${value}${postfix}`;
297
+ }
298
+
299
+ return u.formatAttributeCasing(value, casing);
300
+ };
301
+ }
302
+
303
+ _makeStructureKey() {
304
+ return (key) => {
305
+ return this.applyPadding(key);
306
+ };
307
+ }
308
+
309
+ _isPaddingEligible(padding = {}) {
310
+ return !!padding && padding.length && v.isStringHasLength(padding.char);
311
+ }
312
+
313
+ _makePadding({ padding = {} }) {
314
+ return (value) => {
315
+ if (typeof value !== "string") {
316
+ return value;
317
+ } else if (this._isPaddingEligible(padding)) {
318
+ return u.addPadding({ padding, value });
319
+ } else {
320
+ return value;
321
+ }
322
+ };
323
+ }
324
+
325
+ _makeRemoveFixings({
326
+ prefix = "",
327
+ postfix = "",
328
+ casing = KeyCasing.none,
329
+ } = {}) {
330
+ return (key) => {
331
+ let value = "";
332
+ if (
333
+ ![AttributeTypes.string, AttributeTypes.enum].includes(this.type) ||
334
+ typeof key !== "string"
335
+ ) {
336
+ value = key;
337
+ } else if (prefix.length > 0 && key.length > prefix.length) {
338
+ for (let i = prefix.length; i < key.length - postfix.length; i++) {
339
+ value += key[i];
340
+ }
341
+ } else {
342
+ value = key;
343
+ }
344
+
345
+ return value;
346
+ };
347
+ }
348
+
349
+ _makeDestructureKey({
350
+ prefix = "",
351
+ postfix = "",
352
+ casing = KeyCasing.none,
353
+ padding = {},
354
+ } = {}) {
355
+ return (key) => {
356
+ let value = "";
357
+ if (
358
+ ![AttributeTypes.string, AttributeTypes.enum].includes(this.type) ||
359
+ typeof key !== "string"
360
+ ) {
361
+ return key;
362
+ } else if (key.length > prefix.length) {
363
+ value = u.removeFixings({ prefix, postfix, value: key });
364
+ } else {
365
+ value = key;
366
+ }
367
+
368
+ // 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
369
+ // if (padding && padding.length) {
370
+ // value = u.removePadding({padding, value});
371
+ // }
372
+
373
+ return value;
374
+ };
375
+ }
376
+
377
+ acceptable(val) {
378
+ return val !== undefined;
379
+ }
380
+
381
+ getPathType(type, parentType) {
382
+ if (
383
+ parentType === AttributeTypes.list ||
384
+ parentType === AttributeTypes.set
385
+ ) {
386
+ return PathTypes.item;
387
+ }
388
+ return type;
389
+ }
390
+
391
+ getAttribute(path) {
392
+ return this.traverser.getPath(path);
393
+ }
394
+
395
+ getChild(path) {
396
+ if (this.type === AttributeTypes.any) {
397
+ return this;
398
+ } else if (
399
+ !isNaN(path) &&
400
+ (this.type === AttributeTypes.list || this.type === AttributeTypes.set)
401
+ ) {
402
+ // if they're asking for a number, and this is a list, children will be under "*"
403
+ return this.traverser.getChild("*");
404
+ } else {
405
+ return this.traverser.getChild(path);
406
+ }
407
+ }
408
+
409
+ _checkGetSet(val, type) {
410
+ if (typeof val !== "function" && val !== undefined) {
411
+ throw new e.ElectroError(
412
+ e.ErrorCodes.InvalidAttributeDefinition,
413
+ `Invalid "${type}" property for attribute ${this.path}. Please ensure value is a function or undefined.`,
414
+ );
415
+ }
416
+ }
417
+
418
+ _makeCast(name, cast) {
419
+ if (cast !== undefined && !CastTypes.includes(cast)) {
420
+ throw new e.ElectroError(
421
+ e.ErrorCodes.InvalidAttributeDefinition,
422
+ `Invalid "cast" property for attribute: "${name}". Acceptable types include ${CastTypes.join(
423
+ ", ",
424
+ )}`,
425
+ );
426
+ } else if (cast === AttributeTypes.string) {
427
+ return (val) => {
428
+ if (val === undefined) {
429
+ // todo: #electroerror
430
+ throw new Error(
431
+ `Attribute ${name} is undefined and cannot be cast to type ${cast}`,
432
+ );
433
+ } else if (typeof val === "string") {
434
+ return val;
435
+ } else {
436
+ return JSON.stringify(val);
437
+ }
438
+ };
439
+ } else if (cast === AttributeTypes.number) {
440
+ return (val) => {
441
+ if (val === undefined) {
442
+ // todo: #electroerror
443
+ throw new Error(
444
+ `Attribute ${name} is undefined and cannot be cast to type ${cast}`,
445
+ );
446
+ } else if (typeof val === "number") {
447
+ return val;
448
+ } else {
449
+ let results = Number(val);
450
+ if (isNaN(results)) {
451
+ // todo: #electroerror
452
+ throw new Error(
453
+ `Attribute ${name} cannot be cast to type ${cast}. Doing so results in NaN`,
454
+ );
455
+ } else {
456
+ return results;
457
+ }
458
+ }
459
+ };
460
+ } else {
461
+ return (val) => val;
462
+ }
463
+ }
464
+
465
+ _makeValidate(definition) {
466
+ if (typeof definition === "function") {
467
+ return (val) => {
468
+ try {
469
+ let reason = definition(val);
470
+ const isValid = !reason;
471
+ if (isValid) {
472
+ return [isValid, []];
473
+ } else if (typeof reason === "boolean") {
474
+ return [
475
+ isValid,
476
+ [
477
+ new e.ElectroUserValidationError(
478
+ this.path,
479
+ "Invalid value provided",
480
+ ),
481
+ ],
482
+ ];
483
+ } else {
484
+ return [
485
+ isValid,
486
+ [new e.ElectroUserValidationError(this.path, reason)],
487
+ ];
488
+ }
489
+ } catch (err) {
490
+ return [false, [new e.ElectroUserValidationError(this.path, err)]];
491
+ }
492
+ };
493
+ } else if (definition instanceof RegExp) {
494
+ return (val) => {
495
+ if (val === undefined) {
496
+ return [true, []];
497
+ }
498
+ let isValid = definition.test(val);
499
+ let reason = [];
500
+ if (!isValid) {
501
+ reason.push(
502
+ new e.ElectroUserValidationError(
503
+ this.path,
504
+ `Invalid value for attribute "${this.path}": Failed model defined regex`,
505
+ ),
506
+ );
507
+ }
508
+ return [isValid, reason];
509
+ };
510
+ } else {
511
+ return () => [true, []];
512
+ }
513
+ }
514
+
515
+ _makeDefault(definition) {
516
+ if (typeof definition === "function") {
517
+ return () => definition();
518
+ } else {
519
+ return () => definition;
520
+ }
521
+ }
522
+
523
+ _makeType(name, definition) {
524
+ let type = "";
525
+ let enumArray = [];
526
+ if (Array.isArray(definition.type)) {
527
+ type = AttributeTypes.enum;
528
+ enumArray = [...definition.type];
529
+ // } else if (definition.type === AttributeTypes.set && Array.isArray(definition.items)) {
530
+ // type = AttributeTypes.enumSet;
531
+ // enumArray = [...definition.items];
532
+ } else {
533
+ type = definition.type || "string";
534
+ }
535
+ if (!AttributeTypeNames.includes(type)) {
536
+ throw new e.ElectroError(
537
+ e.ErrorCodes.InvalidAttributeDefinition,
538
+ `Invalid "type" property for attribute: "${name}". Acceptable types include ${AttributeTypeNames.join(
539
+ ", ",
540
+ )}`,
541
+ );
542
+ }
543
+ return { type, enumArray };
544
+ }
545
+
546
+ isWatcher() {
547
+ return this._isWatcher;
548
+ }
549
+
550
+ isWatched() {
551
+ return this._isWatched;
552
+ }
553
+
554
+ isWatching(attribute) {
555
+ return this.watching[attribute] !== undefined;
556
+ }
557
+
558
+ isWatchedBy(attribute) {
559
+ return this.watchedBy[attribute] !== undefined;
560
+ }
561
+
562
+ _isType(value) {
563
+ if (value === undefined) {
564
+ let reason = [];
565
+ if (this.required) {
566
+ reason.push(
567
+ new e.ElectroAttributeValidationError(
568
+ this.path,
569
+ `Invalid value type at entity path: "${this.path}". Value is required.`,
570
+ ),
571
+ );
572
+ }
573
+ return [!this.required, reason];
574
+ }
575
+ let isTyped = false;
576
+ let reason = [];
577
+ switch (this.type) {
578
+ case AttributeTypes.enum:
579
+ // case AttributeTypes.enumSet:
580
+ // isTyped = this.enumArray.every(enumValue => {
581
+ // const val = Array.isArray(value) ? value : [value];
582
+ // return val.includes(enumValue);
583
+ // })
584
+ isTyped = this.enumArray.includes(value);
585
+ if (!isTyped) {
586
+ reason.push(
587
+ new e.ElectroAttributeValidationError(
588
+ this.path,
589
+ `Invalid value type at entity path: "${
590
+ this.path
591
+ }". Value not found in set of acceptable values: ${u.commaSeparatedString(
592
+ this.enumArray,
593
+ )}`,
594
+ ),
595
+ );
596
+ }
597
+ break;
598
+ case AttributeTypes.any:
599
+ case AttributeTypes.static:
600
+ case AttributeTypes.custom:
601
+ isTyped = true;
602
+ break;
603
+ case AttributeTypes.string:
604
+ case AttributeTypes.number:
605
+ case AttributeTypes.boolean:
606
+ default:
607
+ isTyped = typeof value === this.type;
608
+ if (!isTyped) {
609
+ reason.push(
610
+ new e.ElectroAttributeValidationError(
611
+ this.path,
612
+ `Invalid value type at entity path: "${
613
+ this.path
614
+ }". Received value of type "${typeof value}", expected value of type "${
615
+ this.type
616
+ }"`,
617
+ ),
618
+ );
619
+ }
620
+ break;
621
+ }
622
+ return [isTyped, reason];
623
+ }
624
+
625
+ isValid(value) {
626
+ try {
627
+ let [isTyped, typeErrorReason] = this._isType(value);
628
+ let [isValid, validationError] = isTyped
629
+ ? this.validate(value)
630
+ : [false, []];
631
+ let errors = [...typeErrorReason, ...validationError].filter(
632
+ (value) => value !== undefined,
633
+ );
634
+ return [isTyped && isValid, errors];
635
+ } catch (err) {
636
+ return [false, [err]];
637
+ }
638
+ }
639
+
640
+ val(value) {
641
+ value = this.cast(value);
642
+ if (value === undefined) {
643
+ value = this.default();
644
+ }
645
+ return value;
646
+ }
647
+
648
+ getValidate(value) {
649
+ value = this.val(value);
650
+ let [isValid, validationErrors] = this.isValid(value);
651
+ if (!isValid) {
652
+ throw new e.ElectroValidationError(validationErrors);
653
+ }
654
+ return value;
655
+ }
524
656
  }
525
657
 
526
658
  class MapAttribute extends Attribute {
527
- constructor(definition) {
528
- super(definition);
529
- const properties = Attribute.buildChildMapProperties(definition, {
530
- parentType: this.type,
531
- parentPath: this.path,
532
- traverser: this.traverser
533
- });
534
- this.properties = properties;
535
- this.isRoot = !!definition.isRoot;
536
- this.get = this._makeGet(definition.get, properties);
537
- this.set = this._makeSet(definition.set, properties);
538
- }
539
-
540
- _makeGet(get, properties) {
541
- this._checkGetSet(get, "get");
542
- const getter = get || ((val) => {
543
- const isEmpty = !val || Object.keys(val).length === 0;
544
- const isNotRequired = !this.required;
545
- const doesNotHaveDefault = this.default === undefined;
546
- const isRoot = this.isRoot;
547
- if (isEmpty && isRoot && isNotRequired && doesNotHaveDefault) {
548
- return undefined;
549
- }
550
- return val;
551
- });
552
- return (values, siblings) => {
553
- const data = {};
554
-
555
- if (this.hidden) {
556
- return;
557
- }
558
-
559
- if (values === undefined) {
560
- if (!get) {
561
- return undefined;
562
- }
563
- return getter(data, siblings);
564
- }
565
-
566
- for (const name of Object.keys(properties.attributes)) {
567
- const attribute = properties.attributes[name];
568
- if (values[attribute.field] !== undefined) {
569
- let results = attribute.get(values[attribute.field], {...values});
570
- if (results !== undefined) {
571
- data[name] = results;
572
- }
573
- }
574
- }
575
-
576
-
577
- return getter(data, siblings);
578
- }
579
- }
580
-
581
- _makeSet(set, properties) {
582
- this._checkGetSet(set, "set");
583
- const setter = set || ((val) => {
584
- const isEmpty = !val || Object.keys(val).length === 0;
585
- const isNotRequired = !this.required;
586
- const doesNotHaveDefault = this.default === undefined;
587
- const defaultIsValue = this.default === val;
588
- const isRoot = this.isRoot;
589
- if (defaultIsValue) {
590
- return val;
591
- } else if (isEmpty && isRoot && isNotRequired && doesNotHaveDefault) {
592
- return undefined;
593
- } else {
594
- return val;
595
- }
596
- });
597
-
598
- return (values, siblings) => {
599
- const data = {};
600
- if (values === undefined) {
601
- if (!set) {
602
- return undefined;
603
- }
604
- return setter(values, siblings);
605
- }
606
- for (const name of Object.keys(properties.attributes)) {
607
- const attribute = properties.attributes[name];
608
- if (values[name] !== undefined) {
609
- const results = attribute.set(values[name], {...values});
610
- if (results !== undefined) {
611
- data[attribute.field] = results;
612
- }
613
- }
614
- }
615
- return setter(data, siblings);
616
- }
617
- }
618
-
619
- _isType(value) {
620
- if (value === undefined) {
621
- let reason = [];
622
- if (this.required) {
623
- reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`));
624
- }
625
- return [!this.required, reason];
626
- }
627
- const valueType = getValueType(value);
628
- if (valueType !== ValueTypes.object) {
629
- 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"`)]];
630
- }
631
- let reason = [];
632
- const [childrenAreValid, childErrors] = this._validateChildren(value);
633
- if (!childrenAreValid) {
634
- reason = childErrors;
635
- }
636
- return [childrenAreValid, reason]
637
- }
638
-
639
- _validateChildren(value) {
640
- const valueType = getValueType(value);
641
- const attributes = this.properties.attributes;
642
- let errors = [];
643
- if (valueType === ValueTypes.object) {
644
- for (const child of Object.keys(attributes)) {
645
- const [isValid, errorValues] = attributes[child].isValid(value === undefined ? value : value[child])
646
- if (!isValid) {
647
- errors = [...errors, ...errorValues]
648
- }
649
- }
650
- } else if (valueType !== ValueTypes.object) {
651
- errors.push(
652
- 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}"`)
653
- );
654
- } else if (this.properties.hasRequiredAttributes) {
655
- errors.push(
656
- 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))}`)
657
- );
658
- }
659
- return [errors.length === 0, errors];
660
- }
661
-
662
- val(value) {
663
- const incomingIsEmpty = value === undefined;
664
- let fromDefault = false;
665
- let data;
666
- if (value === undefined) {
667
- data = this.default();
668
- if (data !== undefined) {
669
- fromDefault = true;
670
- }
671
- } else {
672
- data = value;
673
- }
674
-
675
- const valueType = getValueType(data);
676
-
677
- if (data === undefined) {
678
- return data;
679
- } else if (valueType !== "object") {
680
- 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}"`);
681
- }
682
-
683
- const response = {};
684
-
685
- for (const name of Object.keys(this.properties.attributes)) {
686
- const attribute = this.properties.attributes[name];
687
- const results = attribute.val(data[attribute.name]);
688
- if (results !== undefined) {
689
- response[name] = results;
690
- }
691
- }
692
-
693
- if (Object.keys(response).length === 0 && !fromDefault && this.isRoot && !this.required && incomingIsEmpty) {
694
- return undefined;
695
- }
696
-
697
- return response;
698
- }
659
+ constructor(definition) {
660
+ super(definition);
661
+ const properties = Attribute.buildChildMapProperties(definition, {
662
+ parentType: this.type,
663
+ parentPath: this.path,
664
+ traverser: this.traverser,
665
+ });
666
+ this.properties = properties;
667
+ this.isRoot = !!definition.isRoot;
668
+ this.get = this._makeGet(definition.get, properties);
669
+ this.set = this._makeSet(definition.set, properties);
670
+ }
671
+
672
+ _makeGet(get, properties) {
673
+ this._checkGetSet(get, "get");
674
+ const getter =
675
+ get ||
676
+ ((val) => {
677
+ const isEmpty = !val || Object.keys(val).length === 0;
678
+ const isNotRequired = !this.required;
679
+ const doesNotHaveDefault = this.default === undefined;
680
+ const isRoot = this.isRoot;
681
+ if (isEmpty && isRoot && isNotRequired && doesNotHaveDefault) {
682
+ return undefined;
683
+ }
684
+ return val;
685
+ });
686
+ return (values, siblings) => {
687
+ const data = {};
688
+
689
+ if (this.hidden) {
690
+ return;
691
+ }
692
+
693
+ if (values === undefined) {
694
+ if (!get) {
695
+ return undefined;
696
+ }
697
+ return getter(data, siblings);
698
+ }
699
+
700
+ for (const name of Object.keys(properties.attributes)) {
701
+ const attribute = properties.attributes[name];
702
+ if (values[attribute.field] !== undefined) {
703
+ let results = attribute.get(values[attribute.field], { ...values });
704
+ if (results !== undefined) {
705
+ data[name] = results;
706
+ }
707
+ }
708
+ }
709
+
710
+ return getter(data, siblings);
711
+ };
712
+ }
713
+
714
+ _makeSet(set, properties) {
715
+ this._checkGetSet(set, "set");
716
+ const setter =
717
+ set ||
718
+ ((val) => {
719
+ const isEmpty = !val || Object.keys(val).length === 0;
720
+ const isNotRequired = !this.required;
721
+ const doesNotHaveDefault = this.default === undefined;
722
+ const defaultIsValue = this.default === val;
723
+ const isRoot = this.isRoot;
724
+ if (defaultIsValue) {
725
+ return val;
726
+ } else if (isEmpty && isRoot && isNotRequired && doesNotHaveDefault) {
727
+ return undefined;
728
+ } else {
729
+ return val;
730
+ }
731
+ });
732
+
733
+ return (values, siblings) => {
734
+ const data = {};
735
+ if (values === undefined) {
736
+ if (!set) {
737
+ return undefined;
738
+ }
739
+ return setter(values, siblings);
740
+ }
741
+ for (const name of Object.keys(properties.attributes)) {
742
+ const attribute = properties.attributes[name];
743
+ if (values[name] !== undefined) {
744
+ const results = attribute.set(values[name], { ...values });
745
+ if (results !== undefined) {
746
+ data[attribute.field] = results;
747
+ }
748
+ }
749
+ }
750
+ return setter(data, siblings);
751
+ };
752
+ }
753
+
754
+ _isType(value) {
755
+ if (value === undefined) {
756
+ let reason = [];
757
+ if (this.required) {
758
+ reason.push(
759
+ new e.ElectroAttributeValidationError(
760
+ this.path,
761
+ `Invalid value type at entity path: "${this.path}". Value is required.`,
762
+ ),
763
+ );
764
+ }
765
+ return [!this.required, reason];
766
+ }
767
+ const valueType = getValueType(value);
768
+ if (valueType !== ValueTypes.object) {
769
+ return [
770
+ false,
771
+ [
772
+ new e.ElectroAttributeValidationError(
773
+ this.path,
774
+ `Invalid value type at entity path "${this.path}. Received value of type "${valueType}", expected value of type "object"`,
775
+ ),
776
+ ],
777
+ ];
778
+ }
779
+ let reason = [];
780
+ const [childrenAreValid, childErrors] = this._validateChildren(value);
781
+ if (!childrenAreValid) {
782
+ reason = childErrors;
783
+ }
784
+ return [childrenAreValid, reason];
785
+ }
786
+
787
+ _validateChildren(value) {
788
+ const valueType = getValueType(value);
789
+ const attributes = this.properties.attributes;
790
+ let errors = [];
791
+ if (valueType === ValueTypes.object) {
792
+ for (const child of Object.keys(attributes)) {
793
+ const [isValid, errorValues] = attributes[child].isValid(
794
+ value === undefined ? value : value[child],
795
+ );
796
+ if (!isValid) {
797
+ errors = [...errors, ...errorValues];
798
+ }
799
+ }
800
+ } else if (valueType !== ValueTypes.object) {
801
+ errors.push(
802
+ new e.ElectroAttributeValidationError(
803
+ this.path,
804
+ `Invalid value type at entity path: "${this.path}". Expected value to be an object to fulfill attribute type "${this.type}"`,
805
+ ),
806
+ );
807
+ } else if (this.properties.hasRequiredAttributes) {
808
+ errors.push(
809
+ new e.ElectroAttributeValidationError(
810
+ this.path,
811
+ `Invalid value type at entity path: "${
812
+ this.path
813
+ }". Map attribute requires at least the properties ${u.commaSeparatedString(
814
+ Object.keys(attributes),
815
+ )}`,
816
+ ),
817
+ );
818
+ }
819
+ return [errors.length === 0, errors];
820
+ }
821
+
822
+ val(value) {
823
+ const incomingIsEmpty = value === undefined;
824
+ let fromDefault = false;
825
+ let data;
826
+ if (value === undefined) {
827
+ data = this.default();
828
+ if (data !== undefined) {
829
+ fromDefault = true;
830
+ }
831
+ } else {
832
+ data = value;
833
+ }
834
+
835
+ const valueType = getValueType(data);
836
+
837
+ if (data === undefined) {
838
+ return data;
839
+ } else if (valueType !== "object") {
840
+ throw new e.ElectroAttributeValidationError(
841
+ this.path,
842
+ `Invalid value type at entity path: "${this.path}". Expected value to be an object to fulfill attribute type "${this.type}"`,
843
+ );
844
+ }
845
+
846
+ const response = {};
847
+
848
+ for (const name of Object.keys(this.properties.attributes)) {
849
+ const attribute = this.properties.attributes[name];
850
+ const results = attribute.val(data[attribute.name]);
851
+ if (results !== undefined) {
852
+ response[name] = results;
853
+ }
854
+ }
855
+
856
+ if (
857
+ Object.keys(response).length === 0 &&
858
+ !fromDefault &&
859
+ this.isRoot &&
860
+ !this.required &&
861
+ incomingIsEmpty
862
+ ) {
863
+ return undefined;
864
+ }
865
+
866
+ return response;
867
+ }
699
868
  }
700
869
 
701
870
  class ListAttribute extends Attribute {
702
- constructor(definition) {
703
- super(definition);
704
- const items = Attribute.buildChildListItems(definition, {
705
- parentType: this.type,
706
- parentPath: this.path,
707
- traverser: this.traverser
708
- });
709
- this.items = items;
710
- this.get = this._makeGet(definition.get, items);
711
- this.set = this._makeSet(definition.set, items);
712
- }
713
-
714
- _makeGet(get, items) {
715
- this._checkGetSet(get, "get");
716
-
717
- const getter = get || ((attr) => attr);
718
-
719
- return (values, siblings) => {
720
- const data = [];
721
-
722
- if (this.hidden) {
723
- return;
724
- }
725
-
726
- if (values === undefined) {
727
- return getter(data, siblings);
728
- }
729
-
730
- for (let value of values) {
731
- const results = items.get(value, [...values]);
732
- if (results !== undefined) {
733
- data.push(results);
734
- }
735
- }
736
-
737
- return getter(data, siblings);
738
- }
739
- }
740
-
741
- _makeSet(set, items) {
742
- this._checkGetSet(set, "set");
743
- const setter = set || ((attr) => attr);
744
- return (values, siblings) => {
745
- const data = [];
746
-
747
- if (values === undefined) {
748
- return setter(values, siblings);
749
- }
750
-
751
- for (const value of values) {
752
- const results = items.set(value, [...values]);
753
- if (results !== undefined) {
754
- data.push(results);
755
- }
756
- }
757
-
758
- return setter(data, siblings);
759
- }
760
- }
761
-
762
- _validateArrayValue(value) {
763
- const reason = [];
764
- const valueType = getValueType(value);
765
- if (value !== undefined && valueType !== ValueTypes.array) {
766
- 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"`)]];
767
- } else {
768
- return [true, []];
769
- }
770
- }
771
-
772
- _isType(value) {
773
- if (value === undefined) {
774
- let reason = [];
775
- if (this.required) {
776
- reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`));
777
- }
778
- return [!this.required, reason];
779
- }
780
-
781
- const [isValidArray, errors] = this._validateArrayValue(value);
782
- if (!isValidArray) {
783
- return [isValidArray, errors];
784
- }
785
- let reason = [];
786
- const [childrenAreValid, childErrors] = this._validateChildren(value);
787
- if (!childrenAreValid) {
788
- reason = childErrors;
789
- }
790
- return [childrenAreValid, reason]
791
- }
792
-
793
- _validateChildren(value) {
794
- const valueType = getValueType(value);
795
- const errors = [];
796
- if (valueType === ValueTypes.array) {
797
- for (const i in value) {
798
- const [isValid, errorValues] = this.items.isValid(value[i]);
799
- if (!isValid) {
800
- for (const err of errorValues) {
801
- if (err instanceof e.ElectroAttributeValidationError || err instanceof e.ElectroUserValidationError) {
802
- err.index = parseInt(i);
803
- }
804
- errors.push(err);
805
- }
806
- }
807
- }
808
- } else {
809
- errors.push(
810
- 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}"`)
811
- );
812
- }
813
- return [errors.length === 0, errors];
814
- }
815
-
816
- val(value) {
817
- const getValue = (v) => {
818
- v = this.cast(v);
819
- if (v === undefined) {
820
- v = this.default();
821
- }
822
- return v;
823
- }
824
-
825
- const data = value === undefined
826
- ? getValue(value)
827
- : value;
828
-
829
- if (data === undefined) {
830
- return data;
831
- } else if (!Array.isArray(data)) {
832
- 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"`);
833
- }
834
-
835
- const response = [];
836
- for (const d of data) {
837
- const results = this.items.val(d);
838
- if (results !== undefined) {
839
- response.push(results);
840
- }
841
- }
842
-
843
- return response;
844
- }
871
+ constructor(definition) {
872
+ super(definition);
873
+ const items = Attribute.buildChildListItems(definition, {
874
+ parentType: this.type,
875
+ parentPath: this.path,
876
+ traverser: this.traverser,
877
+ });
878
+ this.items = items;
879
+ this.get = this._makeGet(definition.get, items);
880
+ this.set = this._makeSet(definition.set, items);
881
+ }
882
+
883
+ _makeGet(get, items) {
884
+ this._checkGetSet(get, "get");
885
+
886
+ const getter = get || ((attr) => attr);
887
+
888
+ return (values, siblings) => {
889
+ const data = [];
890
+
891
+ if (this.hidden) {
892
+ return;
893
+ }
894
+
895
+ if (values === undefined) {
896
+ return getter(data, siblings);
897
+ }
898
+
899
+ for (let value of values) {
900
+ const results = items.get(value, [...values]);
901
+ if (results !== undefined) {
902
+ data.push(results);
903
+ }
904
+ }
905
+
906
+ return getter(data, siblings);
907
+ };
908
+ }
909
+
910
+ _makeSet(set, items) {
911
+ this._checkGetSet(set, "set");
912
+ const setter = set || ((attr) => attr);
913
+ return (values, siblings) => {
914
+ const data = [];
915
+
916
+ if (values === undefined) {
917
+ return setter(values, siblings);
918
+ }
919
+
920
+ for (const value of values) {
921
+ const results = items.set(value, [...values]);
922
+ if (results !== undefined) {
923
+ data.push(results);
924
+ }
925
+ }
926
+
927
+ return setter(data, siblings);
928
+ };
929
+ }
930
+
931
+ _validateArrayValue(value) {
932
+ const reason = [];
933
+ const valueType = getValueType(value);
934
+ if (value !== undefined && valueType !== ValueTypes.array) {
935
+ return [
936
+ false,
937
+ [
938
+ new e.ElectroAttributeValidationError(
939
+ this.path,
940
+ `Invalid value type at entity path "${this.path}. Received value of type "${valueType}", expected value of type "array"`,
941
+ ),
942
+ ],
943
+ ];
944
+ } else {
945
+ return [true, []];
946
+ }
947
+ }
948
+
949
+ _isType(value) {
950
+ if (value === undefined) {
951
+ let reason = [];
952
+ if (this.required) {
953
+ reason.push(
954
+ new e.ElectroAttributeValidationError(
955
+ this.path,
956
+ `Invalid value type at entity path: "${this.path}". Value is required.`,
957
+ ),
958
+ );
959
+ }
960
+ return [!this.required, reason];
961
+ }
962
+
963
+ const [isValidArray, errors] = this._validateArrayValue(value);
964
+ if (!isValidArray) {
965
+ return [isValidArray, errors];
966
+ }
967
+ let reason = [];
968
+ const [childrenAreValid, childErrors] = this._validateChildren(value);
969
+ if (!childrenAreValid) {
970
+ reason = childErrors;
971
+ }
972
+ return [childrenAreValid, reason];
973
+ }
974
+
975
+ _validateChildren(value) {
976
+ const valueType = getValueType(value);
977
+ const errors = [];
978
+ if (valueType === ValueTypes.array) {
979
+ for (const i in value) {
980
+ const [isValid, errorValues] = this.items.isValid(value[i]);
981
+ if (!isValid) {
982
+ for (const err of errorValues) {
983
+ if (
984
+ err instanceof e.ElectroAttributeValidationError ||
985
+ err instanceof e.ElectroUserValidationError
986
+ ) {
987
+ err.index = parseInt(i);
988
+ }
989
+ errors.push(err);
990
+ }
991
+ }
992
+ }
993
+ } else {
994
+ errors.push(
995
+ new e.ElectroAttributeValidationError(
996
+ this.path,
997
+ `Invalid value type at entity path: "${this.path}". Expected value to be an Array to fulfill attribute type "${this.type}"`,
998
+ ),
999
+ );
1000
+ }
1001
+ return [errors.length === 0, errors];
1002
+ }
1003
+
1004
+ val(value) {
1005
+ const getValue = (v) => {
1006
+ v = this.cast(v);
1007
+ if (v === undefined) {
1008
+ v = this.default();
1009
+ }
1010
+ return v;
1011
+ };
1012
+
1013
+ const data = value === undefined ? getValue(value) : value;
1014
+
1015
+ if (data === undefined) {
1016
+ return data;
1017
+ } else if (!Array.isArray(data)) {
1018
+ throw new e.ElectroAttributeValidationError(
1019
+ this.path,
1020
+ `Invalid value type at entity path "${
1021
+ this.path
1022
+ }. Received value of type "${getValueType(
1023
+ value,
1024
+ )}", expected value of type "array"`,
1025
+ );
1026
+ }
1027
+
1028
+ const response = [];
1029
+ for (const d of data) {
1030
+ const results = this.items.val(d);
1031
+ if (results !== undefined) {
1032
+ response.push(results);
1033
+ }
1034
+ }
1035
+
1036
+ return response;
1037
+ }
845
1038
  }
846
1039
 
847
1040
  class SetAttribute extends Attribute {
848
- constructor(definition) {
849
- super(definition);
850
- const items = Attribute.buildChildSetItems(definition, {
851
- parentType: this.type,
852
- parentPath: this.path,
853
- traverser: this.traverser
854
- });
855
- this.items = items;
856
- this.get = this._makeGet(definition.get, items);
857
- this.set = this._makeSet(definition.set, items);
858
- this.validate = this._makeSetValidate(definition);
859
- }
860
-
861
- _makeSetValidate(definition) {
862
- const validate = this._makeValidate(definition.validate);
863
- return (value) => {
864
- switch (getValueType(value)) {
865
- case ValueTypes.array:
866
- return validate([...value]);
867
- case ValueTypes.aws_set:
868
- return validate([...value.values]);
869
- case ValueTypes.set:
870
- return validate(Array.from(value));
871
- default:
872
- return validate(value);
873
- }
874
- }
875
- }
876
-
877
- fromDDBSet(value) {
878
- switch (getValueType(value)) {
879
- case ValueTypes.aws_set:
880
- return [...value.values];
881
- case ValueTypes.set:
882
- return Array.from(value);
883
- default:
884
- return value;
885
- }
886
- }
887
-
888
- _createDDBSet(value) {
889
- const client = this.getClient();
890
- if (client && typeof client.createSet === "function") {
891
- value = Array.isArray(value)
892
- ? Array.from(new Set(value))
893
- : value;
894
- return client.createSet(value, { validate: true });
895
- } else {
896
- return new DynamoDBSet(value, this.items.type);
897
- }
898
- }
899
-
900
- acceptable(val) {
901
- return Array.isArray(val)
902
- ? val.length > 0
903
- : this.items.acceptable(val);
904
- }
905
-
906
- toDDBSet(value) {
907
- const valueType = getValueType(value);
908
- let array;
909
- switch(valueType) {
910
- case ValueTypes.set:
911
- array = Array.from(value);
912
- return this._createDDBSet(array);
913
- case ValueTypes.aws_set:
914
- return value;
915
- case ValueTypes.array:
916
- return this._createDDBSet(value);
917
- case ValueTypes.string:
918
- case ValueTypes.number: {
919
- this.items.getValidate(value);
920
- return this._createDDBSet(value);
921
- }
922
- default:
923
- 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.`)
924
- }
925
-
926
- }
927
-
928
- _makeGet(get, items) {
929
- this._checkGetSet(get, "get");
930
- const getter = get || ((attr) => attr);
931
- return (values, siblings) => {
932
- if (values !== undefined) {
933
- const data = this.fromDDBSet(values);
934
- return getter(data, siblings);
935
- }
936
- const data = this.fromDDBSet(values);
937
- const results = getter(data, siblings);
938
- if (results !== undefined) {
939
- // if not undefined, try to convert, else no need to return
940
- return this.fromDDBSet(results);
941
- }
942
- }
943
- }
944
-
945
- _makeSet(set, items) {
946
- this._checkGetSet(set, "set");
947
- const setter = set || ((attr) => attr);
948
- return (values, siblings) => {
949
- const results = setter(this.fromDDBSet(values), siblings);
950
- if (results !== undefined) {
951
- return this.toDDBSet(results);
952
- }
953
- }
954
- }
955
-
956
- _isType(value) {
957
- if (value === undefined) {
958
- const reason = [];
959
- if (this.required) {
960
- reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`));
961
- }
962
- return [!this.required, reason];
963
- }
964
-
965
- let reason = [];
966
- const [childrenAreValid, childErrors] = this._validateChildren(value);
967
- if (!childrenAreValid) {
968
- reason = childErrors;
969
- }
970
- return [childrenAreValid, reason]
971
- }
972
-
973
- _validateChildren(value) {
974
- const valueType = getValueType(value);
975
- let errors = [];
976
- let arr = [];
977
- if (valueType === ValueTypes.array) {
978
- arr = value;
979
- } else if (valueType === ValueTypes.set) {
980
- arr = Array.from(value);
981
- } else if (valueType === ValueTypes.aws_set) {
982
- arr = value.values;
983
- } else {
984
- errors.push(
985
- 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}"`)
986
- );
987
- }
988
- for (const item of arr) {
989
- const [isValid, errorValues] = this.items.isValid(item);
990
- if (!isValid) {
991
- errors = [...errors, ...errorValues];
992
- }
993
- }
994
- return [errors.length === 0, errors];
995
- }
996
-
997
- val(value) {
998
- if (value === undefined) {
999
- value = this.default();
1000
- }
1001
-
1002
- if (value !== undefined) {
1003
- return this.toDDBSet(value);
1004
- }
1005
- }
1041
+ constructor(definition) {
1042
+ super(definition);
1043
+ const items = Attribute.buildChildSetItems(definition, {
1044
+ parentType: this.type,
1045
+ parentPath: this.path,
1046
+ traverser: this.traverser,
1047
+ });
1048
+ this.items = items;
1049
+ this.get = this._makeGet(definition.get, items);
1050
+ this.set = this._makeSet(definition.set, items);
1051
+ this.validate = this._makeSetValidate(definition);
1052
+ }
1053
+
1054
+ _makeSetValidate(definition) {
1055
+ const validate = this._makeValidate(definition.validate);
1056
+ return (value) => {
1057
+ switch (getValueType(value)) {
1058
+ case ValueTypes.array:
1059
+ return validate([...value]);
1060
+ case ValueTypes.aws_set:
1061
+ return validate([...value.values]);
1062
+ case ValueTypes.set:
1063
+ return validate(Array.from(value));
1064
+ default:
1065
+ return validate(value);
1066
+ }
1067
+ };
1068
+ }
1069
+
1070
+ fromDDBSet(value) {
1071
+ switch (getValueType(value)) {
1072
+ case ValueTypes.aws_set:
1073
+ return [...value.values];
1074
+ case ValueTypes.set:
1075
+ return Array.from(value);
1076
+ default:
1077
+ return value;
1078
+ }
1079
+ }
1080
+
1081
+ _createDDBSet(value) {
1082
+ const client = this.getClient();
1083
+ if (client && typeof client.createSet === "function") {
1084
+ value = Array.isArray(value) ? Array.from(new Set(value)) : value;
1085
+ return client.createSet(value, { validate: true });
1086
+ } else {
1087
+ return new DynamoDBSet(value, this.items.type);
1088
+ }
1089
+ }
1090
+
1091
+ acceptable(val) {
1092
+ return Array.isArray(val) ? val.length > 0 : this.items.acceptable(val);
1093
+ }
1094
+
1095
+ toDDBSet(value) {
1096
+ const valueType = getValueType(value);
1097
+ let array;
1098
+ switch (valueType) {
1099
+ case ValueTypes.set:
1100
+ array = Array.from(value);
1101
+ return this._createDDBSet(array);
1102
+ case ValueTypes.aws_set:
1103
+ return value;
1104
+ case ValueTypes.array:
1105
+ return this._createDDBSet(value);
1106
+ case ValueTypes.string:
1107
+ case ValueTypes.number: {
1108
+ this.items.getValidate(value);
1109
+ return this._createDDBSet(value);
1110
+ }
1111
+ default:
1112
+ throw new e.ElectroAttributeValidationError(
1113
+ this.path,
1114
+ `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.`,
1115
+ );
1116
+ }
1117
+ }
1118
+
1119
+ _makeGet(get, items) {
1120
+ this._checkGetSet(get, "get");
1121
+ const getter = get || ((attr) => attr);
1122
+ return (values, siblings) => {
1123
+ if (values !== undefined) {
1124
+ const data = this.fromDDBSet(values);
1125
+ return getter(data, siblings);
1126
+ }
1127
+ const data = this.fromDDBSet(values);
1128
+ const results = getter(data, siblings);
1129
+ if (results !== undefined) {
1130
+ // if not undefined, try to convert, else no need to return
1131
+ return this.fromDDBSet(results);
1132
+ }
1133
+ };
1134
+ }
1135
+
1136
+ _makeSet(set, items) {
1137
+ this._checkGetSet(set, "set");
1138
+ const setter = set || ((attr) => attr);
1139
+ return (values, siblings) => {
1140
+ const results = setter(this.fromDDBSet(values), siblings);
1141
+ if (results !== undefined) {
1142
+ return this.toDDBSet(results);
1143
+ }
1144
+ };
1145
+ }
1146
+
1147
+ _isType(value) {
1148
+ if (value === undefined) {
1149
+ const reason = [];
1150
+ if (this.required) {
1151
+ reason.push(
1152
+ new e.ElectroAttributeValidationError(
1153
+ this.path,
1154
+ `Invalid value type at entity path: "${this.path}". Value is required.`,
1155
+ ),
1156
+ );
1157
+ }
1158
+ return [!this.required, reason];
1159
+ }
1160
+
1161
+ let reason = [];
1162
+ const [childrenAreValid, childErrors] = this._validateChildren(value);
1163
+ if (!childrenAreValid) {
1164
+ reason = childErrors;
1165
+ }
1166
+ return [childrenAreValid, reason];
1167
+ }
1168
+
1169
+ _validateChildren(value) {
1170
+ const valueType = getValueType(value);
1171
+ let errors = [];
1172
+ let arr = [];
1173
+ if (valueType === ValueTypes.array) {
1174
+ arr = value;
1175
+ } else if (valueType === ValueTypes.set) {
1176
+ arr = Array.from(value);
1177
+ } else if (valueType === ValueTypes.aws_set) {
1178
+ arr = value.values;
1179
+ } else {
1180
+ errors.push(
1181
+ new e.ElectroAttributeValidationError(
1182
+ this.path,
1183
+ `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}"`,
1184
+ ),
1185
+ );
1186
+ }
1187
+ for (const item of arr) {
1188
+ const [isValid, errorValues] = this.items.isValid(item);
1189
+ if (!isValid) {
1190
+ errors = [...errors, ...errorValues];
1191
+ }
1192
+ }
1193
+ return [errors.length === 0, errors];
1194
+ }
1195
+
1196
+ val(value) {
1197
+ if (value === undefined) {
1198
+ value = this.default();
1199
+ }
1200
+
1201
+ if (value !== undefined) {
1202
+ return this.toDDBSet(value);
1203
+ }
1204
+ }
1006
1205
  }
1007
1206
 
1008
1207
  class Schema {
1009
- constructor(properties = {}, facets = {}, {traverser = new AttributeTraverser(), getClient, parent, isRoot} = {}) {
1010
- this._validateProperties(properties, parent);
1011
- let schema = Schema.normalizeAttributes(properties, facets, {traverser, getClient, parent, isRoot});
1012
- this.getClient = getClient;
1013
- this.attributes = schema.attributes;
1014
- this.enums = schema.enums;
1015
- this.translationForTable = schema.translationForTable;
1016
- this.translationForRetrieval = schema.translationForRetrieval;
1017
- this.hiddenAttributes = schema.hiddenAttributes;
1018
- this.readOnlyAttributes = schema.readOnlyAttributes;
1019
- this.requiredAttributes = schema.requiredAttributes;
1020
- this.translationForWatching = this._formatWatchTranslations(this.attributes);
1021
- this.traverser = traverser;
1022
- this.isRoot = !!isRoot;
1023
- }
1024
-
1025
- static normalizeAttributes(attributes = {}, facets = {}, {traverser, getClient, parent, isRoot} = {}) {
1026
- const attributeHasParent = !!parent;
1027
- let invalidProperties = [];
1028
- let normalized = {};
1029
- let usedAttrs = {};
1030
- let enums = {};
1031
- let translationForTable = {};
1032
- let translationForRetrieval = {};
1033
- let watchedAttributes = {};
1034
- let requiredAttributes = new Set();
1035
- let hiddenAttributes = new Set();
1036
- let readOnlyAttributes = new Set();
1037
- let definitions = {};
1038
- for (let name in attributes) {
1039
- let attribute = attributes[name];
1040
- if (typeof attribute === AttributeTypes.string || Array.isArray(attribute)) {
1041
- attribute = {
1042
- type: attribute
1043
- };
1044
- }
1045
- const field = attribute.field || name;
1046
- let isKeyField = false;
1047
- let prefix = "";
1048
- let postfix = "";
1049
- let casing = KeyCasing.none;
1050
- if (facets.byField && facets.byField[field] !== undefined) {
1051
- for (const indexName of Object.keys(facets.byField[field])) {
1052
- let definition = facets.byField[field][indexName];
1053
- if (definition.facets.length > 1) {
1054
- throw new e.ElectroError(
1055
- e.ErrorCodes.InvalidIndexWithAttributeName,
1056
- `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}".`
1057
- )
1058
- }
1059
- if (definition.isCustom) {
1060
- const keyFieldLabels = facets.labels[indexName][definition.type].labels;
1061
- // I am not sure how more than two would happen but it would mean either
1062
- // 1. Code prior has an unknown edge-case.
1063
- // 2. Method is being incorrectly used.
1064
- if (keyFieldLabels.length > 2) {
1065
- throw new e.ElectroError(
1066
- e.ErrorCodes.InvalidIndexWithAttributeName,
1067
- `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.`
1068
- )
1069
- }
1070
- isKeyField = true;
1071
- casing = definition.casing;
1072
- // Walk through the labels, given the above exception handling, I'd expect the first element to
1073
- // be the prefix and the second element to be the postfix.
1074
- for (const value of keyFieldLabels) {
1075
- if (value.name === field) {
1076
- prefix = value.label || "";
1077
- } else {
1078
- postfix = value.label || "";
1079
- }
1080
- }
1081
- if (attribute.type !== AttributeTypes.string && !Array.isArray(attribute.type)) {
1082
- if (prefix.length > 0 || postfix.length > 0) {
1083
- 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.`);
1084
- }
1085
- }
1086
- } else {
1087
- // Upstream middleware should have taken care of this. An error here would mean:
1088
- // 1. Code prior has an unknown edge-case.
1089
- // 2. Method is being incorrectly used.
1090
- throw new e.ElectroError(
1091
- e.ErrorCodes.InvalidIndexCompositeWithAttributeName,
1092
- `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}}"`
1093
- )
1094
- }
1095
-
1096
- if (definition.inCollection) {
1097
- throw new e.ElectroError(
1098
- e.ErrorCodes.InvalidCollectionOnIndexWithAttributeFieldNames,
1099
- `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.`
1100
- )
1101
- }
1102
-
1103
- if (definition.field === field) {
1104
- if (attribute.padding !== undefined) {
1105
- throw new e.ElectroError(
1106
- e.ErrorCodes.InvalidAttributeDefinition,
1107
- `Invalid padding definition for the attribute "${name}". Padding is not currently supported for attributes that are also defined as table indexes.`
1108
- );
1109
- }
1110
- }
1111
- }
1112
- }
1113
-
1114
- let isKey = !!facets.byIndex && facets.byIndex[TableIndex].all.find((facet) => facet.name === name);
1115
- let definition = {
1116
- name,
1117
- field,
1118
- getClient,
1119
- casing,
1120
- prefix,
1121
- postfix,
1122
- traverser,
1123
- isKeyField: isKeyField || isKey,
1124
- isRoot: !!isRoot,
1125
- label: attribute.label,
1126
- required: !!attribute.required,
1127
- default: attribute.default,
1128
- validate: attribute.validate,
1129
- readOnly: !!attribute.readOnly || isKey,
1130
- hidden: !!attribute.hidden,
1131
- indexes: (facets.byAttr && facets.byAttr[name]) || [],
1132
- type: attribute.type,
1133
- get: attribute.get,
1134
- set: attribute.set,
1135
- watching: Array.isArray(attribute.watch) ? attribute.watch : [],
1136
- items: attribute.items,
1137
- properties: attribute.properties,
1138
- parentPath: attribute.parentPath,
1139
- parentType: attribute.parentType,
1140
- padding: attribute.padding,
1141
- };
1142
-
1143
- if (definition.type === AttributeTypes.custom) {
1144
- definition.type = AttributeTypes.any;
1145
- }
1146
-
1147
- if (attribute.watch !== undefined) {
1148
- if (attribute.watch === AttributeWildCard) {
1149
- definition.watchAll = true;
1150
- definition.watching = [];
1151
- } else if (Array.isArray(attribute.watch)) {
1152
- definition.watching = attribute.watch;
1153
- } else {
1154
- 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}".`);
1155
- }
1156
- } else {
1157
- definition.watching = [];
1158
- }
1159
-
1160
- if (definition.readOnly) {
1161
- readOnlyAttributes.add(name);
1162
- }
1163
-
1164
- if (definition.hidden) {
1165
- hiddenAttributes.add(name);
1166
- }
1167
-
1168
- if (definition.required) {
1169
- requiredAttributes.add(name);
1170
- }
1171
-
1172
- if (facets.byAttr && facets.byAttr[definition.name] !== undefined && (!ValidFacetTypes.includes(definition.type) && !Array.isArray(definition.type))) {
1173
- let assignedIndexes = facets.byAttr[name].map(assigned => assigned.index === "" ? "Table Index" : assigned.index);
1174
- 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 following indexes: ${assignedIndexes.join(", ")}`);
1175
- }
1176
-
1177
- if (usedAttrs[definition.field] || usedAttrs[name]) {
1178
- invalidProperties.push({
1179
- name,
1180
- property: "field",
1181
- value: definition.field,
1182
- expected: `Unique field property, already used by attribute ${
1183
- usedAttrs[definition.field]
1184
- }`,
1185
- });
1186
- } else {
1187
- usedAttrs[definition.field] = definition.name;
1188
- }
1189
-
1190
- translationForTable[definition.name] = definition.field;
1191
- translationForRetrieval[definition.field] = definition.name;
1192
-
1193
- for (let watched of definition.watching) {
1194
- watchedAttributes[watched] = watchedAttributes[watched] || [];
1195
- watchedAttributes[watched].push(name);
1196
- }
1197
-
1198
- definitions[name] = definition;
1199
- }
1200
-
1201
- for (let name of Object.keys(definitions)) {
1202
- const definition = definitions[name];
1203
-
1204
- definition.watchedBy = Array.isArray(watchedAttributes[name])
1205
- ? watchedAttributes[name]
1206
- : [];
1207
-
1208
- switch(definition.type) {
1209
- case AttributeTypes.map:
1210
- normalized[name] = new MapAttribute(definition);
1211
- break;
1212
- case AttributeTypes.list:
1213
- normalized[name] = new ListAttribute(definition);
1214
- break;
1215
- case AttributeTypes.set:
1216
- normalized[name] = new SetAttribute(definition);
1217
- break;
1218
- case AttributeTypes.any:
1219
- if (attributeHasParent) {
1220
- 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.`);
1221
- }
1222
- default:
1223
- normalized[name] = new Attribute(definition);
1224
- }
1225
- }
1226
-
1227
- let watchedWatchers = [];
1228
- let watchingUnknownAttributes = [];
1229
- for (let watched of Object.keys(watchedAttributes)) {
1230
- if (normalized[watched] === undefined) {
1231
- for (let attribute of watchedAttributes[watched]) {
1232
- watchingUnknownAttributes.push({attribute, watched});
1233
- }
1234
- } else if (normalized[watched].isWatcher()) {
1235
- for (let attribute of watchedAttributes[watched]) {
1236
- watchedWatchers.push({attribute, watched});
1237
- }
1238
- }
1239
- }
1240
-
1241
- if (watchingUnknownAttributes.length > 0) {
1242
- 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(", ")}.`);
1243
- }
1244
-
1245
- if (watchedWatchers.length > 0) {
1246
- 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(", ")}.`)
1247
- }
1248
-
1249
- let missingFacetAttributes = Array.isArray(facets.attributes)
1250
- ? facets.attributes
1251
- .filter(({ name }) => !normalized[name])
1252
- .map((facet) => `"${facet.type}: ${facet.name}"`)
1253
- : []
1254
- if (missingFacetAttributes.length > 0) {
1255
- 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(", ")}`);
1256
- }
1257
- if (invalidProperties.length > 0) {
1258
- let message = invalidProperties.map((prop) => `Schema Validation Error. Attribute "${prop.name}" property "${prop.property}". Received: "${prop.value}", Expected: "${prop.expected}"`);
1259
- throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, message);
1260
- } else {
1261
- return {
1262
- enums,
1263
- hiddenAttributes,
1264
- readOnlyAttributes,
1265
- requiredAttributes,
1266
- translationForTable,
1267
- translationForRetrieval,
1268
- attributes: normalized,
1269
- };
1270
- }
1271
- }
1272
-
1273
- _validateProperties() {}
1274
-
1275
- _formatWatchTranslations(attributes) {
1276
- let watchersToAttributes = {};
1277
- let attributesToWatchers = {};
1278
- let watchAllAttributes = {};
1279
- let hasWatchers = false;
1280
- for (let name of Object.keys(attributes)) {
1281
- if (attributes[name].isWatcher()) {
1282
- hasWatchers = true;
1283
- watchersToAttributes[name] = attributes[name].watching;
1284
- } else if (attributes[name].watchAll) {
1285
- hasWatchers = true;
1286
- watchAllAttributes[name] = name;
1287
- } else {
1288
- attributesToWatchers[name] = attributesToWatchers[name] || {};
1289
- attributesToWatchers[name] = attributes[name].watchedBy;
1290
- }
1291
- }
1292
- return {
1293
- hasWatchers,
1294
- watchAllAttributes,
1295
- watchersToAttributes,
1296
- attributesToWatchers
1297
- };
1298
- }
1299
-
1300
- getAttribute(path) {
1301
- return this.traverser.getPath(path);
1302
- }
1303
-
1304
- getLabels() {
1305
- let labels = {};
1306
- for (let name of Object.keys(this.attributes)) {
1307
- let label = this.attributes[name].label;
1308
- if (label !== undefined) {
1309
- labels[name] = label;
1310
- }
1311
- }
1312
- return labels;
1313
- };
1314
-
1315
- getLabels() {
1316
- let labels = {};
1317
- for (let name of Object.keys(this.attributes)) {
1318
- let label = this.attributes[name].label;
1319
- if (label !== undefined) {
1320
- labels[name] = label;
1321
- }
1322
- }
1323
- return labels;
1324
- };
1325
-
1326
- _applyAttributeMutation(method, include, avoid, payload) {
1327
- let data = { ...payload };
1328
- for (let path of Object.keys(include)) {
1329
- // 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
1330
- // avoid[attribute] === undefined | Attribute shouldn't be in the avoided
1331
- const attribute = this.getAttribute(path);
1332
- if (attribute !== undefined && avoid[path] === undefined) {
1333
- data[path] = attribute[method](payload[path], {...payload});
1334
- }
1335
- }
1336
- return data;
1337
- }
1338
-
1339
- _fulfillAttributeMutationMethod(method, payload) {
1340
- let watchersToTrigger = {};
1341
- // include: payload | We want to hit the getters/setters for any attributes coming in to be changed
1342
- // avoid: watchersToAttributes | We want to avoid anything that is a watcher, even if it was included
1343
- let avoid = {...this.translationForWatching.watchersToAttributes, ...this.translationForWatching.watchAllAttributes};
1344
- let data = this._applyAttributeMutation(method, payload, avoid, payload);
1345
- // `data` here will include all the original payload values, but with the mutations applied to on non-watchers
1346
- if (!this.translationForWatching.hasWatchers) {
1347
- // exit early, why not
1348
- return data;
1349
- }
1350
- for (let attribute of Object.keys(data)) {
1351
- let watchers = this.translationForWatching.attributesToWatchers[attribute];
1352
- // Any of the attributes on data have a watcher?
1353
- if (watchers !== undefined) {
1354
- watchersToTrigger = {...watchersToTrigger, ...watchers}
1355
- }
1356
- }
1357
-
1358
- // 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
1359
- // avoid: attributesToWatchers | We want to avoid hit anything that was not a watcher because they were already hit once above
1360
- let include = {...data, ...watchersToTrigger, ...this.translationForWatching.watchAllAttributes};
1361
- return this._applyAttributeMutation(method, include, this.translationForWatching.attributesToWatchers, data);
1362
- }
1363
-
1364
- applyAttributeGetters(payload = {}) {
1365
- return this._fulfillAttributeMutationMethod(AttributeMutationMethods.get, payload);
1366
- }
1367
-
1368
- applyAttributeSetters(payload = {}) {
1369
- return this._fulfillAttributeMutationMethod(AttributeMutationMethods.set, payload);
1370
- }
1371
-
1372
- translateFromFields(item = {}, options = {}) {
1373
- let { includeKeys } = options;
1374
- let data = {};
1375
- let names = this.translationForRetrieval;
1376
- for (let [attr, value] of Object.entries(item)) {
1377
- let name = names[attr];
1378
- if (name) {
1379
- data[name] = value;
1380
- } else if (includeKeys) {
1381
- data[attr] = value;
1382
- }
1383
- }
1384
- return data;
1385
- }
1386
-
1387
- translateToFields(payload = {}) {
1388
- let record = {};
1389
- for (let [name, value] of Object.entries(payload)) {
1390
- let field = this.getFieldName(name);
1391
- if (value !== undefined) {
1392
- record[field] = value;
1393
- }
1394
- }
1395
- return record;
1396
- }
1397
-
1398
- getFieldName(name) {
1399
- if (typeof name === 'string') {
1400
- return this.translationForTable[name];
1401
- }
1402
- }
1403
-
1404
- checkCreate(payload = {}) {
1405
- let record = {};
1406
- for (let attribute of Object.values(this.attributes)) {
1407
- let value = payload[attribute.name];
1408
- record[attribute.name] = attribute.getValidate(value);
1409
- }
1410
- return record;
1411
- }
1412
-
1413
- checkRemove(paths = []) {
1414
- for (const path of paths) {
1415
- const attribute = this.traverser.getPath(path);
1416
- if (!attribute) {
1417
- throw new e.ElectroAttributeValidationError(path, `Attribute "${path}" does not exist on model.`);
1418
- } else if (attribute.readOnly) {
1419
- throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Read-Only and cannot be removed`);
1420
- } else if (attribute.required) {
1421
- throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Required and cannot be removed`);
1422
- }
1423
- }
1424
- return paths;
1425
- }
1426
-
1427
- checkOperation(attribute, operation, value) {
1428
- if (attribute.required && operation === ItemOperations.remove) {
1429
- throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Required and cannot be removed`);
1430
- } else if (attribute.readOnly) {
1431
- throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Read-Only and cannot be updated`);
1432
- }
1433
-
1434
- return value === undefined
1435
- ? undefined
1436
- : attribute.getValidate(value);
1437
- }
1438
-
1439
- checkUpdate(payload = {}, { allowReadOnly } = {}) {
1440
- let record = {};
1441
- for (let [path, value] of Object.entries(payload)) {
1442
- let attribute = this.traverser.paths.get(path);
1443
- if (attribute === undefined) {
1444
- continue;
1445
- }
1446
- if (attribute.readOnly && !allowReadOnly) {
1447
- throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Read-Only and cannot be updated`);
1448
- } else {
1449
- record[path] = attribute.getValidate(value);
1450
- }
1451
- }
1452
- return record;
1453
- }
1454
-
1455
- getReadOnly() {
1456
- return Array.from(this.readOnlyAttributes);
1457
- }
1458
-
1459
- getRequired() {
1460
- return Array.from(this.requiredAttributes);
1461
- }
1462
-
1463
- formatItemForRetrieval(item, config) {
1464
- let returnAttributes = new Set(config.attributes || []);
1465
- let hasUserSpecifiedReturnAttributes = returnAttributes.size > 0;
1466
- let remapped = this.translateFromFields(item, config);
1467
- let data = this._fulfillAttributeMutationMethod("get", remapped);
1468
- if (this.hiddenAttributes.size > 0 || hasUserSpecifiedReturnAttributes) {
1469
- for (let attribute of Object.keys(data)) {
1470
- if (this.hiddenAttributes.has(attribute)) {
1471
- delete data[attribute];
1472
- }
1473
- if (hasUserSpecifiedReturnAttributes && !returnAttributes.has(attribute)) {
1474
- delete data[attribute];
1475
- }
1476
- }
1477
- }
1478
- return data;
1479
- }
1208
+ constructor(
1209
+ properties = {},
1210
+ facets = {},
1211
+ { traverser = new AttributeTraverser(), getClient, parent, isRoot } = {},
1212
+ ) {
1213
+ this._validateProperties(properties, parent);
1214
+ let schema = Schema.normalizeAttributes(properties, facets, {
1215
+ traverser,
1216
+ getClient,
1217
+ parent,
1218
+ isRoot,
1219
+ });
1220
+ this.getClient = getClient;
1221
+ this.attributes = schema.attributes;
1222
+ this.enums = schema.enums;
1223
+ this.translationForTable = schema.translationForTable;
1224
+ this.translationForRetrieval = schema.translationForRetrieval;
1225
+ this.hiddenAttributes = schema.hiddenAttributes;
1226
+ this.readOnlyAttributes = schema.readOnlyAttributes;
1227
+ this.requiredAttributes = schema.requiredAttributes;
1228
+ this.translationForWatching = this._formatWatchTranslations(
1229
+ this.attributes,
1230
+ );
1231
+ this.traverser = traverser;
1232
+ this.isRoot = !!isRoot;
1233
+ }
1234
+
1235
+ static normalizeAttributes(
1236
+ attributes = {},
1237
+ facets = {},
1238
+ { traverser, getClient, parent, isRoot } = {},
1239
+ ) {
1240
+ const attributeHasParent = !!parent;
1241
+ let invalidProperties = [];
1242
+ let normalized = {};
1243
+ let usedAttrs = {};
1244
+ let enums = {};
1245
+ let translationForTable = {};
1246
+ let translationForRetrieval = {};
1247
+ let watchedAttributes = {};
1248
+ let requiredAttributes = new Set();
1249
+ let hiddenAttributes = new Set();
1250
+ let readOnlyAttributes = new Set();
1251
+ let definitions = {};
1252
+ for (let name in attributes) {
1253
+ let attribute = attributes[name];
1254
+ if (
1255
+ typeof attribute === AttributeTypes.string ||
1256
+ Array.isArray(attribute)
1257
+ ) {
1258
+ attribute = {
1259
+ type: attribute,
1260
+ };
1261
+ }
1262
+ const field = attribute.field || name;
1263
+ let isKeyField = false;
1264
+ let prefix = "";
1265
+ let postfix = "";
1266
+ let casing = KeyCasing.none;
1267
+ if (facets.byField && facets.byField[field] !== undefined) {
1268
+ for (const indexName of Object.keys(facets.byField[field])) {
1269
+ let definition = facets.byField[field][indexName];
1270
+ if (definition.facets.length > 1) {
1271
+ throw new e.ElectroError(
1272
+ e.ErrorCodes.InvalidIndexWithAttributeName,
1273
+ `Invalid definition for "${
1274
+ definition.type
1275
+ }" field on index "${u.formatIndexNameForDisplay(
1276
+ indexName,
1277
+ )}". The ${definition.type} field "${
1278
+ definition.field
1279
+ }" 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 "${
1280
+ definition.field
1281
+ }".`,
1282
+ );
1283
+ }
1284
+ if (definition.isCustom) {
1285
+ const keyFieldLabels =
1286
+ facets.labels[indexName][definition.type].labels;
1287
+ // I am not sure how more than two would happen but it would mean either
1288
+ // 1. Code prior has an unknown edge-case.
1289
+ // 2. Method is being incorrectly used.
1290
+ if (keyFieldLabels.length > 2) {
1291
+ throw new e.ElectroError(
1292
+ e.ErrorCodes.InvalidIndexWithAttributeName,
1293
+ `Unexpected definition for "${
1294
+ definition.type
1295
+ }" field on index "${u.formatIndexNameForDisplay(
1296
+ indexName,
1297
+ )}". The ${definition.type} field "${
1298
+ definition.field
1299
+ }" 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.`,
1300
+ );
1301
+ }
1302
+ isKeyField = true;
1303
+ casing = definition.casing;
1304
+ // Walk through the labels, given the above exception handling, I'd expect the first element to
1305
+ // be the prefix and the second element to be the postfix.
1306
+ for (const value of keyFieldLabels) {
1307
+ if (value.name === field) {
1308
+ prefix = value.label || "";
1309
+ } else {
1310
+ postfix = value.label || "";
1311
+ }
1312
+ }
1313
+ if (
1314
+ attribute.type !== AttributeTypes.string &&
1315
+ !Array.isArray(attribute.type)
1316
+ ) {
1317
+ if (prefix.length > 0 || postfix.length > 0) {
1318
+ throw new e.ElectroError(
1319
+ e.ErrorCodes.InvalidIndexWithAttributeName,
1320
+ `definition for "${
1321
+ definition.type
1322
+ }" field on index "${u.formatIndexNameForDisplay(
1323
+ indexName,
1324
+ )}". Index templates may only have prefix or postfix values on "string" or "enum" type attributes. The ${
1325
+ definition.type
1326
+ } field "${field}" is type "${
1327
+ attribute.type
1328
+ }", 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.`,
1329
+ );
1330
+ }
1331
+ }
1332
+ } else {
1333
+ // Upstream middleware should have taken care of this. An error here would mean:
1334
+ // 1. Code prior has an unknown edge-case.
1335
+ // 2. Method is being incorrectly used.
1336
+ throw new e.ElectroError(
1337
+ e.ErrorCodes.InvalidIndexCompositeWithAttributeName,
1338
+ `Unexpected definition for "${
1339
+ definition.type
1340
+ }" field on index "${u.formatIndexNameForDisplay(
1341
+ indexName,
1342
+ )}". The ${definition.type} field "${
1343
+ definition.field
1344
+ }" 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 "${
1345
+ definition.type
1346
+ }" field on index "${u.formatIndexNameForDisplay(
1347
+ indexName,
1348
+ )}" with the value: "\${${definition.field}}"`,
1349
+ );
1350
+ }
1351
+
1352
+ if (definition.inCollection) {
1353
+ throw new e.ElectroError(
1354
+ e.ErrorCodes.InvalidCollectionOnIndexWithAttributeFieldNames,
1355
+ `Invalid use of a collection on index "${u.formatIndexNameForDisplay(
1356
+ indexName,
1357
+ )}". The ${definition.type} field "${
1358
+ definition.field
1359
+ }" 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.`,
1360
+ );
1361
+ }
1362
+
1363
+ if (definition.field === field) {
1364
+ if (attribute.padding !== undefined) {
1365
+ throw new e.ElectroError(
1366
+ e.ErrorCodes.InvalidAttributeDefinition,
1367
+ `Invalid padding definition for the attribute "${name}". Padding is not currently supported for attributes that are also defined as table indexes.`,
1368
+ );
1369
+ }
1370
+ }
1371
+ }
1372
+ }
1373
+
1374
+ let isKey =
1375
+ !!facets.byIndex &&
1376
+ facets.byIndex[TableIndex].all.find((facet) => facet.name === name);
1377
+ let definition = {
1378
+ name,
1379
+ field,
1380
+ getClient,
1381
+ casing,
1382
+ prefix,
1383
+ postfix,
1384
+ traverser,
1385
+ isKeyField: isKeyField || isKey,
1386
+ isRoot: !!isRoot,
1387
+ label: attribute.label,
1388
+ required: !!attribute.required,
1389
+ default: attribute.default,
1390
+ validate: attribute.validate,
1391
+ readOnly: !!attribute.readOnly || isKey,
1392
+ hidden: !!attribute.hidden,
1393
+ indexes: (facets.byAttr && facets.byAttr[name]) || [],
1394
+ type: attribute.type,
1395
+ get: attribute.get,
1396
+ set: attribute.set,
1397
+ watching: Array.isArray(attribute.watch) ? attribute.watch : [],
1398
+ items: attribute.items,
1399
+ properties: attribute.properties,
1400
+ parentPath: attribute.parentPath,
1401
+ parentType: attribute.parentType,
1402
+ padding: attribute.padding,
1403
+ };
1404
+
1405
+ if (definition.type === AttributeTypes.custom) {
1406
+ definition.type = AttributeTypes.any;
1407
+ }
1408
+
1409
+ if (attribute.watch !== undefined) {
1410
+ if (attribute.watch === AttributeWildCard) {
1411
+ definition.watchAll = true;
1412
+ definition.watching = [];
1413
+ } else if (Array.isArray(attribute.watch)) {
1414
+ definition.watching = attribute.watch;
1415
+ } else {
1416
+ throw new e.ElectroError(
1417
+ e.ErrorCodes.InvalidAttributeWatchDefinition,
1418
+ `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}".`,
1419
+ );
1420
+ }
1421
+ } else {
1422
+ definition.watching = [];
1423
+ }
1424
+
1425
+ if (definition.readOnly) {
1426
+ readOnlyAttributes.add(name);
1427
+ }
1428
+
1429
+ if (definition.hidden) {
1430
+ hiddenAttributes.add(name);
1431
+ }
1432
+
1433
+ if (definition.required) {
1434
+ requiredAttributes.add(name);
1435
+ }
1436
+
1437
+ if (
1438
+ facets.byAttr &&
1439
+ facets.byAttr[definition.name] !== undefined &&
1440
+ !ValidFacetTypes.includes(definition.type) &&
1441
+ !Array.isArray(definition.type)
1442
+ ) {
1443
+ let assignedIndexes = facets.byAttr[name].map((assigned) =>
1444
+ assigned.index === "" ? "Table Index" : assigned.index,
1445
+ );
1446
+ throw new e.ElectroError(
1447
+ e.ErrorCodes.InvalidAttributeDefinition,
1448
+ `Invalid composite attribute definition: Composite attributes must be one of the following: ${ValidFacetTypes.join(
1449
+ ", ",
1450
+ )}. The attribute "${name}" is defined as being type "${
1451
+ attribute.type
1452
+ }" but is a composite attribute of the following indexes: ${assignedIndexes.join(
1453
+ ", ",
1454
+ )}`,
1455
+ );
1456
+ }
1457
+
1458
+ if (usedAttrs[definition.field] || usedAttrs[name]) {
1459
+ invalidProperties.push({
1460
+ name,
1461
+ property: "field",
1462
+ value: definition.field,
1463
+ expected: `Unique field property, already used by attribute ${
1464
+ usedAttrs[definition.field]
1465
+ }`,
1466
+ });
1467
+ } else {
1468
+ usedAttrs[definition.field] = definition.name;
1469
+ }
1470
+
1471
+ translationForTable[definition.name] = definition.field;
1472
+ translationForRetrieval[definition.field] = definition.name;
1473
+
1474
+ for (let watched of definition.watching) {
1475
+ watchedAttributes[watched] = watchedAttributes[watched] || [];
1476
+ watchedAttributes[watched].push(name);
1477
+ }
1478
+
1479
+ definitions[name] = definition;
1480
+ }
1481
+
1482
+ for (let name of Object.keys(definitions)) {
1483
+ const definition = definitions[name];
1484
+
1485
+ definition.watchedBy = Array.isArray(watchedAttributes[name])
1486
+ ? watchedAttributes[name]
1487
+ : [];
1488
+
1489
+ switch (definition.type) {
1490
+ case AttributeTypes.map:
1491
+ normalized[name] = new MapAttribute(definition);
1492
+ break;
1493
+ case AttributeTypes.list:
1494
+ normalized[name] = new ListAttribute(definition);
1495
+ break;
1496
+ case AttributeTypes.set:
1497
+ normalized[name] = new SetAttribute(definition);
1498
+ break;
1499
+ case AttributeTypes.any:
1500
+ if (attributeHasParent) {
1501
+ throw new e.ElectroError(
1502
+ e.ErrorCodes.InvalidAttributeDefinition,
1503
+ `Invalid attribute "${definition.name}" defined within "${
1504
+ parent.parentPath
1505
+ }". Attributes with type ${u.commaSeparatedString([
1506
+ AttributeTypes.any,
1507
+ AttributeTypes.custom,
1508
+ ])} are only supported as root level attributes.`,
1509
+ );
1510
+ }
1511
+ default:
1512
+ normalized[name] = new Attribute(definition);
1513
+ }
1514
+ }
1515
+
1516
+ let watchedWatchers = [];
1517
+ let watchingUnknownAttributes = [];
1518
+ for (let watched of Object.keys(watchedAttributes)) {
1519
+ if (normalized[watched] === undefined) {
1520
+ for (let attribute of watchedAttributes[watched]) {
1521
+ watchingUnknownAttributes.push({ attribute, watched });
1522
+ }
1523
+ } else if (normalized[watched].isWatcher()) {
1524
+ for (let attribute of watchedAttributes[watched]) {
1525
+ watchedWatchers.push({ attribute, watched });
1526
+ }
1527
+ }
1528
+ }
1529
+
1530
+ if (watchingUnknownAttributes.length > 0) {
1531
+ throw new e.ElectroError(
1532
+ e.ErrorCodes.InvalidAttributeWatchDefinition,
1533
+ `Attribute Validation Error. The following attributes are defined to "watch" invalid/unknown attributes: ${watchingUnknownAttributes
1534
+ .map(({ watched, attribute }) => `"${attribute}"->"${watched}"`)
1535
+ .join(", ")}.`,
1536
+ );
1537
+ }
1538
+
1539
+ if (watchedWatchers.length > 0) {
1540
+ throw new e.ElectroError(
1541
+ e.ErrorCodes.InvalidAttributeWatchDefinition,
1542
+ `Attribute Validation Error. Attributes may only "watch" other attributes also watch attributes. The following attributes are defined with ineligible attributes to watch: ${watchedWatchers
1543
+ .map(({ attribute, watched }) => `"${attribute}"->"${watched}"`)
1544
+ .join(", ")}.`,
1545
+ );
1546
+ }
1547
+
1548
+ let missingFacetAttributes = Array.isArray(facets.attributes)
1549
+ ? facets.attributes
1550
+ .filter(({ name }) => !normalized[name])
1551
+ .map((facet) => `"${facet.type}: ${facet.name}"`)
1552
+ : [];
1553
+ if (missingFacetAttributes.length > 0) {
1554
+ throw new e.ElectroError(
1555
+ e.ErrorCodes.InvalidKeyCompositeAttributeTemplate,
1556
+ `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(
1557
+ ", ",
1558
+ )}`,
1559
+ );
1560
+ }
1561
+ if (invalidProperties.length > 0) {
1562
+ let message = invalidProperties.map(
1563
+ (prop) =>
1564
+ `Schema Validation Error. Attribute "${prop.name}" property "${prop.property}". Received: "${prop.value}", Expected: "${prop.expected}"`,
1565
+ );
1566
+ throw new e.ElectroError(
1567
+ e.ErrorCodes.InvalidAttributeDefinition,
1568
+ message,
1569
+ );
1570
+ } else {
1571
+ return {
1572
+ enums,
1573
+ hiddenAttributes,
1574
+ readOnlyAttributes,
1575
+ requiredAttributes,
1576
+ translationForTable,
1577
+ translationForRetrieval,
1578
+ attributes: normalized,
1579
+ };
1580
+ }
1581
+ }
1582
+
1583
+ _validateProperties() {}
1584
+
1585
+ _formatWatchTranslations(attributes) {
1586
+ let watchersToAttributes = {};
1587
+ let attributesToWatchers = {};
1588
+ let watchAllAttributes = {};
1589
+ let hasWatchers = false;
1590
+ for (let name of Object.keys(attributes)) {
1591
+ if (attributes[name].isWatcher()) {
1592
+ hasWatchers = true;
1593
+ watchersToAttributes[name] = attributes[name].watching;
1594
+ } else if (attributes[name].watchAll) {
1595
+ hasWatchers = true;
1596
+ watchAllAttributes[name] = name;
1597
+ } else {
1598
+ attributesToWatchers[name] = attributesToWatchers[name] || {};
1599
+ attributesToWatchers[name] = attributes[name].watchedBy;
1600
+ }
1601
+ }
1602
+ return {
1603
+ hasWatchers,
1604
+ watchAllAttributes,
1605
+ watchersToAttributes,
1606
+ attributesToWatchers,
1607
+ };
1608
+ }
1609
+
1610
+ getAttribute(path) {
1611
+ return this.traverser.getPath(path);
1612
+ }
1613
+
1614
+ getLabels() {
1615
+ let labels = {};
1616
+ for (let name of Object.keys(this.attributes)) {
1617
+ let label = this.attributes[name].label;
1618
+ if (label !== undefined) {
1619
+ labels[name] = label;
1620
+ }
1621
+ }
1622
+ return labels;
1623
+ }
1624
+
1625
+ getLabels() {
1626
+ let labels = {};
1627
+ for (let name of Object.keys(this.attributes)) {
1628
+ let label = this.attributes[name].label;
1629
+ if (label !== undefined) {
1630
+ labels[name] = label;
1631
+ }
1632
+ }
1633
+ return labels;
1634
+ }
1635
+
1636
+ _applyAttributeMutation(method, include, avoid, payload) {
1637
+ let data = { ...payload };
1638
+ for (let path of Object.keys(include)) {
1639
+ // 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
1640
+ // avoid[attribute] === undefined | Attribute shouldn't be in the avoided
1641
+ const attribute = this.getAttribute(path);
1642
+ if (attribute !== undefined && avoid[path] === undefined) {
1643
+ data[path] = attribute[method](payload[path], { ...payload });
1644
+ }
1645
+ }
1646
+ return data;
1647
+ }
1648
+
1649
+ _fulfillAttributeMutationMethod(method, payload) {
1650
+ let watchersToTrigger = {};
1651
+ // include: payload | We want to hit the getters/setters for any attributes coming in to be changed
1652
+ // avoid: watchersToAttributes | We want to avoid anything that is a watcher, even if it was included
1653
+ let avoid = {
1654
+ ...this.translationForWatching.watchersToAttributes,
1655
+ ...this.translationForWatching.watchAllAttributes,
1656
+ };
1657
+ let data = this._applyAttributeMutation(method, payload, avoid, payload);
1658
+ // `data` here will include all the original payload values, but with the mutations applied to on non-watchers
1659
+ if (!this.translationForWatching.hasWatchers) {
1660
+ // exit early, why not
1661
+ return data;
1662
+ }
1663
+ for (let attribute of Object.keys(data)) {
1664
+ let watchers =
1665
+ this.translationForWatching.attributesToWatchers[attribute];
1666
+ // Any of the attributes on data have a watcher?
1667
+ if (watchers !== undefined) {
1668
+ watchersToTrigger = { ...watchersToTrigger, ...watchers };
1669
+ }
1670
+ }
1671
+
1672
+ // 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
1673
+ // avoid: attributesToWatchers | We want to avoid hit anything that was not a watcher because they were already hit once above
1674
+ let include = {
1675
+ ...data,
1676
+ ...watchersToTrigger,
1677
+ ...this.translationForWatching.watchAllAttributes,
1678
+ };
1679
+ return this._applyAttributeMutation(
1680
+ method,
1681
+ include,
1682
+ this.translationForWatching.attributesToWatchers,
1683
+ data,
1684
+ );
1685
+ }
1686
+
1687
+ applyAttributeGetters(payload = {}) {
1688
+ return this._fulfillAttributeMutationMethod(
1689
+ AttributeMutationMethods.get,
1690
+ payload,
1691
+ );
1692
+ }
1693
+
1694
+ applyAttributeSetters(payload = {}) {
1695
+ return this._fulfillAttributeMutationMethod(
1696
+ AttributeMutationMethods.set,
1697
+ payload,
1698
+ );
1699
+ }
1700
+
1701
+ translateFromFields(item = {}, options = {}) {
1702
+ let { includeKeys } = options;
1703
+ let data = {};
1704
+ let names = this.translationForRetrieval;
1705
+ for (let [attr, value] of Object.entries(item)) {
1706
+ let name = names[attr];
1707
+ if (name) {
1708
+ data[name] = value;
1709
+ } else if (includeKeys) {
1710
+ data[attr] = value;
1711
+ }
1712
+ }
1713
+ return data;
1714
+ }
1715
+
1716
+ translateToFields(payload = {}) {
1717
+ let record = {};
1718
+ for (let [name, value] of Object.entries(payload)) {
1719
+ let field = this.getFieldName(name);
1720
+ if (value !== undefined) {
1721
+ record[field] = value;
1722
+ }
1723
+ }
1724
+ return record;
1725
+ }
1726
+
1727
+ getFieldName(name) {
1728
+ if (typeof name === "string") {
1729
+ return this.translationForTable[name];
1730
+ }
1731
+ }
1732
+
1733
+ checkCreate(payload = {}) {
1734
+ let record = {};
1735
+ for (let attribute of Object.values(this.attributes)) {
1736
+ let value = payload[attribute.name];
1737
+ record[attribute.name] = attribute.getValidate(value);
1738
+ }
1739
+ return record;
1740
+ }
1741
+
1742
+ checkRemove(paths = []) {
1743
+ for (const path of paths) {
1744
+ const attribute = this.traverser.getPath(path);
1745
+ if (!attribute) {
1746
+ throw new e.ElectroAttributeValidationError(
1747
+ path,
1748
+ `Attribute "${path}" does not exist on model.`,
1749
+ );
1750
+ } else if (attribute.readOnly) {
1751
+ throw new e.ElectroAttributeValidationError(
1752
+ attribute.path,
1753
+ `Attribute "${attribute.path}" is Read-Only and cannot be removed`,
1754
+ );
1755
+ } else if (attribute.required) {
1756
+ throw new e.ElectroAttributeValidationError(
1757
+ attribute.path,
1758
+ `Attribute "${attribute.path}" is Required and cannot be removed`,
1759
+ );
1760
+ }
1761
+ }
1762
+ return paths;
1763
+ }
1764
+
1765
+ checkOperation(attribute, operation, value) {
1766
+ if (attribute.required && operation === ItemOperations.remove) {
1767
+ throw new e.ElectroAttributeValidationError(
1768
+ attribute.path,
1769
+ `Attribute "${attribute.path}" is Required and cannot be removed`,
1770
+ );
1771
+ } else if (attribute.readOnly) {
1772
+ throw new e.ElectroAttributeValidationError(
1773
+ attribute.path,
1774
+ `Attribute "${attribute.path}" is Read-Only and cannot be updated`,
1775
+ );
1776
+ }
1777
+
1778
+ return value === undefined ? undefined : attribute.getValidate(value);
1779
+ }
1780
+
1781
+ checkUpdate(payload = {}, { allowReadOnly } = {}) {
1782
+ let record = {};
1783
+ for (let [path, value] of Object.entries(payload)) {
1784
+ let attribute = this.traverser.paths.get(path);
1785
+ if (attribute === undefined) {
1786
+ continue;
1787
+ }
1788
+ if (attribute.readOnly && !allowReadOnly) {
1789
+ throw new e.ElectroAttributeValidationError(
1790
+ attribute.path,
1791
+ `Attribute "${attribute.path}" is Read-Only and cannot be updated`,
1792
+ );
1793
+ } else {
1794
+ record[path] = attribute.getValidate(value);
1795
+ }
1796
+ }
1797
+ return record;
1798
+ }
1799
+
1800
+ getReadOnly() {
1801
+ return Array.from(this.readOnlyAttributes);
1802
+ }
1803
+
1804
+ getRequired() {
1805
+ return Array.from(this.requiredAttributes);
1806
+ }
1807
+
1808
+ formatItemForRetrieval(item, config) {
1809
+ let returnAttributes = new Set(config.attributes || []);
1810
+ let hasUserSpecifiedReturnAttributes = returnAttributes.size > 0;
1811
+ let remapped = this.translateFromFields(item, config);
1812
+ let data = this._fulfillAttributeMutationMethod("get", remapped);
1813
+ if (this.hiddenAttributes.size > 0 || hasUserSpecifiedReturnAttributes) {
1814
+ for (let attribute of Object.keys(data)) {
1815
+ if (this.hiddenAttributes.has(attribute)) {
1816
+ delete data[attribute];
1817
+ }
1818
+ if (
1819
+ hasUserSpecifiedReturnAttributes &&
1820
+ !returnAttributes.has(attribute)
1821
+ ) {
1822
+ delete data[attribute];
1823
+ }
1824
+ }
1825
+ }
1826
+ return data;
1827
+ }
1480
1828
  }
1481
1829
 
1482
1830
  function createCustomAttribute(definition = {}) {
1483
- return {
1484
- ...definition,
1485
- type: 'custom'
1486
- };
1831
+ return {
1832
+ ...definition,
1833
+ type: "custom",
1834
+ };
1487
1835
  }
1488
1836
 
1489
1837
  function CustomAttributeType(base) {
1490
- const supported = ['string', 'number', 'boolean', 'any'];
1491
- if (!supported.includes(base)) {
1492
- throw new Error(`OpaquePrimitiveType only supports base types: ${u.commaSeparatedString(supported)}`);
1493
- }
1494
- return base;
1838
+ const supported = ["string", "number", "boolean", "any"];
1839
+ if (!supported.includes(base)) {
1840
+ throw new Error(
1841
+ `OpaquePrimitiveType only supports base types: ${u.commaSeparatedString(
1842
+ supported,
1843
+ )}`,
1844
+ );
1845
+ }
1846
+ return base;
1495
1847
  }
1496
1848
 
1497
1849
  function createSchema(schema) {
1498
- v.model(schema);
1499
- return schema;
1850
+ v.model(schema);
1851
+ return schema;
1500
1852
  }
1501
1853
 
1502
1854
  module.exports = {
1503
- Schema,
1504
- Attribute,
1505
- CastTypes,
1506
- SetAttribute,
1507
- createSchema,
1508
- CustomAttributeType,
1509
- createCustomAttribute,
1855
+ Schema,
1856
+ Attribute,
1857
+ CastTypes,
1858
+ SetAttribute,
1859
+ createSchema,
1860
+ CustomAttributeType,
1861
+ createCustomAttribute,
1510
1862
  };