@rtpaulino/entity 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/entity-utils.d.ts +134 -4
- package/dist/lib/entity-utils.d.ts.map +1 -1
- package/dist/lib/entity-utils.js +268 -98
- package/dist/lib/entity-utils.js.map +1 -1
- package/dist/lib/entity.d.ts +57 -1
- package/dist/lib/entity.d.ts.map +1 -1
- package/dist/lib/entity.js +52 -2
- package/dist/lib/entity.js.map +1 -1
- package/dist/lib/property.d.ts.map +1 -1
- package/dist/lib/property.js +0 -14
- package/dist/lib/property.js.map +1 -1
- package/dist/lib/types.d.ts +4 -10
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +3 -0
- package/dist/lib/types.js.map +1 -1
- package/package.json +1 -1
- package/dist/lib/collection-property.d.ts +0 -38
- package/dist/lib/collection-property.d.ts.map +0 -1
- package/dist/lib/collection-property.js +0 -47
- package/dist/lib/collection-property.js.map +0 -1
package/dist/lib/entity-utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */ import { ENTITY_METADATA_KEY, ENTITY_VALIDATOR_METADATA_KEY, PROPERTY_METADATA_KEY, PROPERTY_OPTIONS_METADATA_KEY } from './types.js';
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-function-type */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ENTITY_METADATA_KEY, ENTITY_OPTIONS_METADATA_KEY, ENTITY_VALIDATOR_METADATA_KEY, PROPERTY_METADATA_KEY, PROPERTY_OPTIONS_METADATA_KEY } from './types.js';
|
|
2
2
|
import { getInjectedPropertyNames, getInjectedPropertyOptions } from './injected-property.js';
|
|
3
3
|
import { EntityDI } from './entity-di.js';
|
|
4
4
|
import { isEqualWith } from 'lodash-es';
|
|
@@ -48,6 +48,42 @@ export class EntityUtils {
|
|
|
48
48
|
const constructor = Object.getPrototypeOf(obj).constructor;
|
|
49
49
|
return Reflect.hasMetadata(ENTITY_METADATA_KEY, constructor);
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Gets the entity options for a given constructor
|
|
53
|
+
*
|
|
54
|
+
* @param entityOrClass - The entity class constructor or instance
|
|
55
|
+
* @returns EntityOptions object (empty object if no options are defined)
|
|
56
|
+
* @private
|
|
57
|
+
*/ static getEntityOptions(entityOrClass) {
|
|
58
|
+
const constructor = typeof entityOrClass === 'function' ? entityOrClass : Object.getPrototypeOf(entityOrClass).constructor;
|
|
59
|
+
const options = Reflect.getMetadata(ENTITY_OPTIONS_METADATA_KEY, constructor);
|
|
60
|
+
return options ?? {};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Checks if a given entity is marked as a collection entity
|
|
64
|
+
*
|
|
65
|
+
* @param entityOrClass - The entity instance or class to check
|
|
66
|
+
* @returns true if the entity is a collection entity, false otherwise
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* @CollectionEntity()
|
|
71
|
+
* class Tags {
|
|
72
|
+
* @ArrayProperty(() => String)
|
|
73
|
+
* collection: string[];
|
|
74
|
+
* }
|
|
75
|
+
*
|
|
76
|
+
* const tags = new Tags({ collection: ['a', 'b'] });
|
|
77
|
+
* console.log(EntityUtils.isCollectionEntity(tags)); // true
|
|
78
|
+
* console.log(EntityUtils.isCollectionEntity(Tags)); // true
|
|
79
|
+
* ```
|
|
80
|
+
*/ static isCollectionEntity(entityOrClass) {
|
|
81
|
+
if (!this.isEntity(entityOrClass)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const options = this.getEntityOptions(entityOrClass);
|
|
85
|
+
return options.collection === true;
|
|
86
|
+
}
|
|
51
87
|
static sameEntity(a, b) {
|
|
52
88
|
if (!this.isEntity(a) || !this.isEntity(b)) {
|
|
53
89
|
return false;
|
|
@@ -161,7 +197,7 @@ export class EntityUtils {
|
|
|
161
197
|
* Serializes an entity to a plain object, converting only properties decorated with @Property()
|
|
162
198
|
*
|
|
163
199
|
* @param entity - The entity instance to serialize
|
|
164
|
-
* @returns A plain object containing only the serialized decorated properties
|
|
200
|
+
* @returns A plain object containing only the serialized decorated properties, or an array for collection entities
|
|
165
201
|
*
|
|
166
202
|
* @remarks
|
|
167
203
|
* Serialization rules:
|
|
@@ -174,6 +210,7 @@ export class EntityUtils {
|
|
|
174
210
|
* - undefined values are excluded from the output
|
|
175
211
|
* - null values are included in the output
|
|
176
212
|
* - Circular references are not supported (will cause stack overflow)
|
|
213
|
+
* - Collection entities (@CollectionEntity) are unwrapped to just their array
|
|
177
214
|
*
|
|
178
215
|
* @example
|
|
179
216
|
* ```typescript
|
|
@@ -205,8 +242,28 @@ export class EntityUtils {
|
|
|
205
242
|
* // address: { street: '123 Main St', city: 'Boston' },
|
|
206
243
|
* // createdAt: '2024-01-01T00:00:00.000Z'
|
|
207
244
|
* // }
|
|
245
|
+
*
|
|
246
|
+
* @CollectionEntity()
|
|
247
|
+
* class Tags {
|
|
248
|
+
* @ArrayProperty(() => String)
|
|
249
|
+
* collection: string[];
|
|
250
|
+
* }
|
|
251
|
+
*
|
|
252
|
+
* const tags = new Tags({ collection: ['a', 'b'] });
|
|
253
|
+
* const json = EntityUtils.toJSON(tags);
|
|
254
|
+
* // ['a', 'b'] - unwrapped to array
|
|
208
255
|
* ```
|
|
209
256
|
*/ static toJSON(entity) {
|
|
257
|
+
if (this.isCollectionEntity(entity)) {
|
|
258
|
+
const collectionPropertyOptions = this.getPropertyOptions(entity, 'collection');
|
|
259
|
+
if (!collectionPropertyOptions) {
|
|
260
|
+
throw new Error(`Collection entity 'collection' property is missing metadata`);
|
|
261
|
+
}
|
|
262
|
+
if (!collectionPropertyOptions.array) {
|
|
263
|
+
throw new Error(`Collection entity 'collection' property must be an array`);
|
|
264
|
+
}
|
|
265
|
+
return this.serializeValue(entity.collection, collectionPropertyOptions);
|
|
266
|
+
}
|
|
210
267
|
const result = {};
|
|
211
268
|
const keys = this.getPropertyKeys(entity);
|
|
212
269
|
for (const key of keys){
|
|
@@ -234,16 +291,6 @@ export class EntityUtils {
|
|
|
234
291
|
if (passthrough) {
|
|
235
292
|
return value;
|
|
236
293
|
}
|
|
237
|
-
if (options?.collection === true) {
|
|
238
|
-
if (!this.isEntity(value)) {
|
|
239
|
-
throw new Error(`Expected collection entity but received non-entity value`);
|
|
240
|
-
}
|
|
241
|
-
const collectionArray = value.collection;
|
|
242
|
-
if (!Array.isArray(collectionArray)) {
|
|
243
|
-
throw new Error(`Collection entity must have a 'collection' property that is an array`);
|
|
244
|
-
}
|
|
245
|
-
return collectionArray.map((item)=>this.serializeValue(item));
|
|
246
|
-
}
|
|
247
294
|
if (Array.isArray(value)) {
|
|
248
295
|
if (options?.serialize) {
|
|
249
296
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
@@ -269,49 +316,14 @@ export class EntityUtils {
|
|
|
269
316
|
throw new Error(`Cannot serialize value of type '${typeof value}'. Use passthrough: true in @Property() to explicitly allow serialization of unknown types.`);
|
|
270
317
|
}
|
|
271
318
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
* Deserialization rules:
|
|
281
|
-
* - All @Property() decorators must include type metadata for parse() to work
|
|
282
|
-
* - Properties without type metadata will throw an error
|
|
283
|
-
* - Required properties (optional !== true) must be present and not null/undefined
|
|
284
|
-
* - Optional properties (optional === true) can be undefined or null
|
|
285
|
-
* - Arrays are supported with the array: true option
|
|
286
|
-
* - Nested entities are recursively deserialized
|
|
287
|
-
* - Type conversion is strict (no coercion)
|
|
288
|
-
* - Entity constructors must accept a required data parameter
|
|
289
|
-
*
|
|
290
|
-
* Validation behavior:
|
|
291
|
-
* - If strict: true - both HARD and SOFT problems throw ValidationError
|
|
292
|
-
* - If strict: false (default) - HARD problems throw ValidationError, SOFT problems stored
|
|
293
|
-
* - Property validators run first, then entity validators
|
|
294
|
-
* - Validators can be synchronous or asynchronous
|
|
295
|
-
* - Problems are accessible via EntityUtils.getProblems()
|
|
296
|
-
* - Raw input data is accessible via EntityUtils.getRawInput()
|
|
297
|
-
*
|
|
298
|
-
* @example
|
|
299
|
-
* ```typescript
|
|
300
|
-
* @Entity()
|
|
301
|
-
* class User {
|
|
302
|
-
* @Property({ type: () => String }) name!: string;
|
|
303
|
-
* @Property({ type: () => Number }) age!: number;
|
|
304
|
-
*
|
|
305
|
-
* constructor(data: Partial<User>) {
|
|
306
|
-
* Object.assign(this, data);
|
|
307
|
-
* }
|
|
308
|
-
* }
|
|
309
|
-
*
|
|
310
|
-
* const json = { name: 'John', age: 30 };
|
|
311
|
-
* const user = await EntityUtils.parse(User, json);
|
|
312
|
-
* const userStrict = await EntityUtils.parse(User, json, { strict: true });
|
|
313
|
-
* ```
|
|
314
|
-
*/ static async parse(entityClass, plainObject, parseOptions = {}, knownProblems) {
|
|
319
|
+
* Internal parse implementation with extended options
|
|
320
|
+
* @private
|
|
321
|
+
*/ static async _parseInternal(entityClass, plainObject, options = {}) {
|
|
322
|
+
if (this.isCollectionEntity(entityClass)) {
|
|
323
|
+
plainObject = {
|
|
324
|
+
collection: plainObject
|
|
325
|
+
};
|
|
326
|
+
}
|
|
315
327
|
if (plainObject == null) {
|
|
316
328
|
throw createValidationError(`Expects an object but received ${typeof plainObject}`);
|
|
317
329
|
}
|
|
@@ -321,7 +333,9 @@ export class EntityUtils {
|
|
|
321
333
|
if (typeof plainObject !== 'object') {
|
|
322
334
|
throw createValidationError(`Expects an object but received ${typeof plainObject}`);
|
|
323
335
|
}
|
|
324
|
-
const strict =
|
|
336
|
+
const strict = options.strict ?? false;
|
|
337
|
+
const skipDefaults = options.skipDefaults ?? false;
|
|
338
|
+
const skipMissing = options.skipMissing ?? false;
|
|
325
339
|
const keys = this.getPropertyKeys(entityClass.prototype);
|
|
326
340
|
const data = {};
|
|
327
341
|
const hardProblems = [];
|
|
@@ -341,8 +355,11 @@ export class EntityUtils {
|
|
|
341
355
|
}
|
|
342
356
|
const isOptional = propertyOptions.optional === true;
|
|
343
357
|
if (!(key in plainObject) || value == null) {
|
|
358
|
+
if (skipMissing) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
344
361
|
let valueToSet = value;
|
|
345
|
-
if (propertyOptions.default !== undefined) {
|
|
362
|
+
if (!skipDefaults && propertyOptions.default !== undefined) {
|
|
346
363
|
valueToSet = typeof propertyOptions.default === 'function' ? await propertyOptions.default() : propertyOptions.default;
|
|
347
364
|
}
|
|
348
365
|
if (!isOptional && valueToSet == null) {
|
|
@@ -355,7 +372,10 @@ export class EntityUtils {
|
|
|
355
372
|
continue;
|
|
356
373
|
}
|
|
357
374
|
try {
|
|
358
|
-
|
|
375
|
+
// Only pass strict to nested deserialization, not skipDefaults/skipMissing
|
|
376
|
+
data[key] = await this.deserializeValue(value, propertyOptions, {
|
|
377
|
+
strict
|
|
378
|
+
});
|
|
359
379
|
} catch (error) {
|
|
360
380
|
if (error instanceof ValidationError) {
|
|
361
381
|
const problems = prependPropertyPath(key, error);
|
|
@@ -370,13 +390,66 @@ export class EntityUtils {
|
|
|
370
390
|
}
|
|
371
391
|
}
|
|
372
392
|
}
|
|
393
|
+
return {
|
|
394
|
+
data,
|
|
395
|
+
hardProblems
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Deserializes a plain object to an entity instance
|
|
400
|
+
*
|
|
401
|
+
* @param entityClass - The entity class constructor. Must accept a data object parameter.
|
|
402
|
+
* @param plainObject - The plain object to deserialize
|
|
403
|
+
* @param parseOptions - Parse options (strict mode)
|
|
404
|
+
* @returns Promise resolving to a new instance of the entity with deserialized values
|
|
405
|
+
*
|
|
406
|
+
* @remarks
|
|
407
|
+
* Deserialization rules:
|
|
408
|
+
* - All @Property() decorators must include type metadata for parse() to work
|
|
409
|
+
* - Properties without type metadata will throw an error
|
|
410
|
+
* - Required properties (optional !== true) must be present and not null/undefined
|
|
411
|
+
* - Optional properties (optional === true) can be undefined or null
|
|
412
|
+
* - Arrays are supported with the array: true option
|
|
413
|
+
* - Nested entities are recursively deserialized
|
|
414
|
+
* - Type conversion is strict (no coercion)
|
|
415
|
+
* - Entity constructors must accept a required data parameter
|
|
416
|
+
*
|
|
417
|
+
* Validation behavior:
|
|
418
|
+
* - If strict: true - both HARD and SOFT problems throw ValidationError
|
|
419
|
+
* - If strict: false (default) - HARD problems throw ValidationError, SOFT problems stored
|
|
420
|
+
* - Property validators run first, then entity validators
|
|
421
|
+
* - Validators can be synchronous or asynchronous
|
|
422
|
+
* - Problems are accessible via EntityUtils.getProblems()
|
|
423
|
+
* - Raw input data is accessible via EntityUtils.getRawInput()
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```typescript
|
|
427
|
+
* @Entity()
|
|
428
|
+
* class User {
|
|
429
|
+
* @Property({ type: () => String }) name!: string;
|
|
430
|
+
* @Property({ type: () => Number }) age!: number;
|
|
431
|
+
*
|
|
432
|
+
* constructor(data: Partial<User>) {
|
|
433
|
+
* Object.assign(this, data);
|
|
434
|
+
* }
|
|
435
|
+
* }
|
|
436
|
+
*
|
|
437
|
+
* const json = { name: 'John', age: 30 };
|
|
438
|
+
* const user = await EntityUtils.parse(User, json);
|
|
439
|
+
* const userStrict = await EntityUtils.parse(User, json, { strict: true });
|
|
440
|
+
* ```
|
|
441
|
+
*/ static async parse(entityClass, plainObject, parseOptions = {}) {
|
|
442
|
+
const strict = parseOptions?.strict ?? false;
|
|
443
|
+
const { data, hardProblems } = await this._parseInternal(entityClass, plainObject, {
|
|
444
|
+
strict
|
|
445
|
+
});
|
|
373
446
|
if (hardProblems.length > 0) {
|
|
374
447
|
throw new ValidationError(hardProblems);
|
|
375
448
|
}
|
|
376
449
|
await this.addInjectedDependencies(data, entityClass.prototype);
|
|
377
450
|
const instance = new entityClass(data);
|
|
378
451
|
rawInputStorage.set(instance, plainObject);
|
|
379
|
-
const problems = await this.validate(instance
|
|
452
|
+
const problems = await this.validate(instance);
|
|
380
453
|
if (problems.length > 0 && strict) {
|
|
381
454
|
throw new ValidationError(problems);
|
|
382
455
|
}
|
|
@@ -440,6 +513,119 @@ export class EntityUtils {
|
|
|
440
513
|
}
|
|
441
514
|
}
|
|
442
515
|
/**
|
|
516
|
+
* Partially deserializes a plain object, returning a plain object with only present properties
|
|
517
|
+
*
|
|
518
|
+
* @param entityClass - The entity class constructor
|
|
519
|
+
* @param plainObject - The plain object to deserialize
|
|
520
|
+
* @param options - Options with strict mode
|
|
521
|
+
* @returns Promise resolving to a plain object with deserialized properties (Partial<T>)
|
|
522
|
+
*
|
|
523
|
+
* @remarks
|
|
524
|
+
* Differences from parse():
|
|
525
|
+
* - Returns a plain object, not an entity instance
|
|
526
|
+
* - Ignores missing properties (does not include them in result)
|
|
527
|
+
* - Does NOT apply default values to missing properties
|
|
528
|
+
* - When strict: false (default), properties with HARD problems are excluded from result but problems are tracked
|
|
529
|
+
* - When strict: true, any HARD problem throws ValidationError
|
|
530
|
+
* - Nested entities/arrays are still fully deserialized and validated as normal
|
|
531
|
+
*
|
|
532
|
+
* @example
|
|
533
|
+
* ```typescript
|
|
534
|
+
* @Entity()
|
|
535
|
+
* class User {
|
|
536
|
+
* @Property({ type: () => String }) name!: string;
|
|
537
|
+
* @Property({ type: () => Number, default: 0 }) age!: number;
|
|
538
|
+
*
|
|
539
|
+
* constructor(data: Partial<User>) {
|
|
540
|
+
* Object.assign(this, data);
|
|
541
|
+
* }
|
|
542
|
+
* }
|
|
543
|
+
*
|
|
544
|
+
* const partial = await EntityUtils.partialParse(User, { name: 'John' });
|
|
545
|
+
* // partial = { name: 'John' } (age is not included, default not applied)
|
|
546
|
+
*
|
|
547
|
+
* const partialWithError = await EntityUtils.partialParse(User, { name: 'John', age: 'invalid' });
|
|
548
|
+
* // partialWithError = { name: 'John' } (age excluded due to HARD problem)
|
|
549
|
+
* // Access problems via second return value
|
|
550
|
+
* ```
|
|
551
|
+
*/ static async partialParse(entityClass, plainObject, options = {}) {
|
|
552
|
+
const result = await this.safePartialParse(entityClass, plainObject, options);
|
|
553
|
+
if (!result.success) {
|
|
554
|
+
throw new ValidationError(result.problems);
|
|
555
|
+
}
|
|
556
|
+
return result.data;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Safely performs partial deserialization without throwing errors
|
|
560
|
+
*
|
|
561
|
+
* @param entityClass - The entity class constructor
|
|
562
|
+
* @param plainObject - The plain object to deserialize
|
|
563
|
+
* @param options - Options with strict mode
|
|
564
|
+
* @returns Promise resolving to a result object with success flag, partial data, and problems
|
|
565
|
+
*
|
|
566
|
+
* @remarks
|
|
567
|
+
* Similar to partialParse() but returns a result object instead of throwing errors:
|
|
568
|
+
* - On success with strict: true - returns { success: true, data: Partial<T>, problems: [] }
|
|
569
|
+
* - On success with strict: false - returns { success: true, data: Partial<T>, problems: [...] } (includes hard problems for excluded properties)
|
|
570
|
+
* - On failure (strict mode only) - returns { success: false, data: undefined, problems: [...] }
|
|
571
|
+
*
|
|
572
|
+
* All partial deserialization rules from partialParse() apply.
|
|
573
|
+
* See partialParse() documentation for detailed behavior.
|
|
574
|
+
*
|
|
575
|
+
* @example
|
|
576
|
+
* ```typescript
|
|
577
|
+
* @Entity()
|
|
578
|
+
* class User {
|
|
579
|
+
* @Property({ type: () => String }) name!: string;
|
|
580
|
+
* @Property({ type: () => Number }) age!: number;
|
|
581
|
+
*
|
|
582
|
+
* constructor(data: Partial<User>) {
|
|
583
|
+
* Object.assign(this, data);
|
|
584
|
+
* }
|
|
585
|
+
* }
|
|
586
|
+
*
|
|
587
|
+
* const result = await EntityUtils.safePartialParse(User, { name: 'John', age: 'invalid' });
|
|
588
|
+
* if (result.success) {
|
|
589
|
+
* console.log(result.data); // { name: 'John' }
|
|
590
|
+
* console.log(result.problems); // [Problem for age property]
|
|
591
|
+
* } else {
|
|
592
|
+
* console.log(result.problems); // Hard problems (only in strict mode)
|
|
593
|
+
* }
|
|
594
|
+
* ```
|
|
595
|
+
*/ static async safePartialParse(entityClass, plainObject, options) {
|
|
596
|
+
const strict = options?.strict ?? false;
|
|
597
|
+
const { data, hardProblems } = await this._parseInternal(entityClass, plainObject, {
|
|
598
|
+
strict,
|
|
599
|
+
skipDefaults: true,
|
|
600
|
+
skipMissing: true
|
|
601
|
+
});
|
|
602
|
+
if (strict && hardProblems.length > 0) {
|
|
603
|
+
return {
|
|
604
|
+
success: false,
|
|
605
|
+
data: undefined,
|
|
606
|
+
problems: hardProblems
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
const propertyProblems = await this.validateProperties(data, entityClass.prototype);
|
|
610
|
+
const validationProblems = [
|
|
611
|
+
...hardProblems,
|
|
612
|
+
...propertyProblems
|
|
613
|
+
];
|
|
614
|
+
if (strict && propertyProblems.length > 0) {
|
|
615
|
+
return {
|
|
616
|
+
success: false,
|
|
617
|
+
data: undefined,
|
|
618
|
+
problems: validationProblems
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
this.setProblems(data, validationProblems);
|
|
622
|
+
return {
|
|
623
|
+
success: true,
|
|
624
|
+
data: data,
|
|
625
|
+
problems: validationProblems
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
443
629
|
* Updates an entity instance with new values, respecting preventUpdates flags on properties
|
|
444
630
|
*
|
|
445
631
|
* @param instance - The entity instance to update. Must be an Entity.
|
|
@@ -567,25 +753,7 @@ export class EntityUtils {
|
|
|
567
753
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
568
754
|
const typeConstructor = options.type();
|
|
569
755
|
const isArray = options.array === true;
|
|
570
|
-
const isCollection = options.collection === true;
|
|
571
756
|
const isSparse = options.sparse === true;
|
|
572
|
-
const problems = [];
|
|
573
|
-
if (isCollection) {
|
|
574
|
-
if (!this.isEntity(typeConstructor)) {
|
|
575
|
-
throw createValidationError(`Collection property type must be an @Entity() class`);
|
|
576
|
-
}
|
|
577
|
-
if (!Array.isArray(value)) {
|
|
578
|
-
problems.push(new Problem({
|
|
579
|
-
message: `Collection property expects an array but received ${typeof value}`
|
|
580
|
-
}));
|
|
581
|
-
}
|
|
582
|
-
const collectionInstance = await this.parse(typeConstructor, {
|
|
583
|
-
collection: Array.isArray(value) ? value : []
|
|
584
|
-
}, parseOptions, problems);
|
|
585
|
-
// override raw input to be the array only
|
|
586
|
-
rawInputStorage.set(collectionInstance, value);
|
|
587
|
-
return collectionInstance;
|
|
588
|
-
}
|
|
589
757
|
if (isArray) {
|
|
590
758
|
if (!Array.isArray(value)) {
|
|
591
759
|
throw createValidationError(`Expects an array but received ${typeof value}`);
|
|
@@ -638,9 +806,6 @@ export class EntityUtils {
|
|
|
638
806
|
return deserializePrimitive(value, typeConstructor);
|
|
639
807
|
}
|
|
640
808
|
if (this.isEntity(typeConstructor)) {
|
|
641
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
642
|
-
throw createValidationError(`Expects an object but received ${typeof value}`);
|
|
643
|
-
}
|
|
644
809
|
return await this.parse(typeConstructor, value, parseOptions);
|
|
645
810
|
}
|
|
646
811
|
throw createValidationError(`Has unknown type constructor. Supported types are: String, Number, Boolean, Date, BigInt, and @Entity() classes. Use passthrough: true to explicitly allow unknown types.`);
|
|
@@ -711,6 +876,24 @@ export class EntityUtils {
|
|
|
711
876
|
}
|
|
712
877
|
return problems;
|
|
713
878
|
}
|
|
879
|
+
/**
|
|
880
|
+
* Validates all properties on an object (entity instance or plain object)
|
|
881
|
+
* @private
|
|
882
|
+
*/ static async validateProperties(dataOrInstance, prototype) {
|
|
883
|
+
const problems = [];
|
|
884
|
+
const keys = Object.keys(dataOrInstance);
|
|
885
|
+
for (const key of keys){
|
|
886
|
+
const options = this.getPropertyOptions(prototype, key);
|
|
887
|
+
if (options) {
|
|
888
|
+
const value = dataOrInstance[key];
|
|
889
|
+
if (value != null) {
|
|
890
|
+
const validationProblems = await this.runPropertyValidators(key, value, options);
|
|
891
|
+
problems.push(...validationProblems);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return problems;
|
|
896
|
+
}
|
|
714
897
|
static async addInjectedDependencies(data, prototype) {
|
|
715
898
|
const injectedPropertyNames = getInjectedPropertyNames(prototype);
|
|
716
899
|
if (injectedPropertyNames.length === 0) {
|
|
@@ -742,22 +925,13 @@ export class EntityUtils {
|
|
|
742
925
|
* const problems = await EntityUtils.validate(user);
|
|
743
926
|
* console.log(problems); // [Problem, Problem, ...]
|
|
744
927
|
* ```
|
|
745
|
-
*/ static async validate(instance
|
|
928
|
+
*/ static async validate(instance) {
|
|
746
929
|
if (!this.isEntity(instance)) {
|
|
747
930
|
throw new Error('Cannot validate non-entity instance');
|
|
748
931
|
}
|
|
749
932
|
const problems = [];
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
const options = this.getPropertyOptions(instance, key);
|
|
753
|
-
if (options) {
|
|
754
|
-
const value = instance[key];
|
|
755
|
-
if (value != null) {
|
|
756
|
-
const validationProblems = await this.runPropertyValidators(key, value, options);
|
|
757
|
-
problems.push(...validationProblems);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
933
|
+
const propertyProblems = await this.validateProperties(instance, instance);
|
|
934
|
+
problems.push(...propertyProblems);
|
|
761
935
|
const entityValidators = this.getEntityValidators(instance);
|
|
762
936
|
for (const validatorMethod of entityValidators){
|
|
763
937
|
const validatorProblems = await instance[validatorMethod]();
|
|
@@ -765,12 +939,8 @@ export class EntityUtils {
|
|
|
765
939
|
problems.push(...validatorProblems);
|
|
766
940
|
}
|
|
767
941
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
...problems
|
|
771
|
-
];
|
|
772
|
-
EntityUtils.setProblems(instance, allProblems);
|
|
773
|
-
return allProblems;
|
|
942
|
+
EntityUtils.setProblems(instance, problems);
|
|
943
|
+
return problems;
|
|
774
944
|
}
|
|
775
945
|
/**
|
|
776
946
|
* Gets the validation problems for an entity instance
|