@rtpaulino/entity 0.12.0 → 0.14.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 +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/entity-utils.d.ts +92 -4
- package/dist/lib/entity-utils.d.ts.map +1 -1
- package/dist/lib/entity-utils.js +279 -66
- package/dist/lib/entity-utils.js.map +1 -1
- package/dist/lib/entity.d.ts +26 -0
- package/dist/lib/entity.d.ts.map +1 -1
- package/dist/lib/entity.js +37 -1
- package/dist/lib/entity.js.map +1 -1
- package/dist/lib/primitive-deserializers.d.ts +15 -0
- package/dist/lib/primitive-deserializers.d.ts.map +1 -0
- package/dist/lib/primitive-deserializers.js +87 -0
- package/dist/lib/primitive-deserializers.js.map +1 -0
- package/dist/lib/problem.d.ts +14 -0
- package/dist/lib/problem.d.ts.map +1 -0
- package/dist/lib/problem.js +31 -0
- package/dist/lib/problem.js.map +1 -0
- package/dist/lib/property.d.ts +15 -1
- package/dist/lib/property.d.ts.map +1 -1
- package/dist/lib/property.js +26 -1
- package/dist/lib/property.js.map +1 -1
- package/dist/lib/types.d.ts +82 -1
- 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/dist/lib/validation-error.d.ts +9 -0
- package/dist/lib/validation-error.d.ts.map +1 -0
- package/dist/lib/validation-error.js +12 -0
- package/dist/lib/validation-error.js.map +1 -0
- package/dist/lib/validation-utils.d.ts +86 -0
- package/dist/lib/validation-utils.d.ts.map +1 -0
- package/dist/lib/validation-utils.js +112 -0
- package/dist/lib/validation-utils.js.map +1 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,8 @@ export * from './lib/entity.js';
|
|
|
3
3
|
export * from './lib/entity-utils.js';
|
|
4
4
|
export * from './lib/types.js';
|
|
5
5
|
export * from './lib/property.js';
|
|
6
|
+
export * from './lib/validation-error.js';
|
|
7
|
+
export * from './lib/problem.js';
|
|
8
|
+
export * from './lib/validation-utils.js';
|
|
9
|
+
export * from './lib/primitive-deserializers.js';
|
|
6
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kBAAkB,CAAC;AACjC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,9 @@ export * from './lib/entity.js';
|
|
|
3
3
|
export * from './lib/entity-utils.js';
|
|
4
4
|
export * from './lib/types.js';
|
|
5
5
|
export * from './lib/property.js';
|
|
6
|
+
export * from './lib/validation-error.js';
|
|
7
|
+
export * from './lib/problem.js';
|
|
8
|
+
export * from './lib/validation-utils.js';
|
|
9
|
+
export * from './lib/primitive-deserializers.js';
|
|
6
10
|
|
|
7
11
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import 'reflect-metadata';\n\nexport * from './lib/entity.js';\nexport * from './lib/entity-utils.js';\nexport * from './lib/types.js';\nexport * from './lib/property.js';\n"],"names":[],"mappings":"AAAA,OAAO,mBAAmB;AAE1B,cAAc,kBAAkB;AAChC,cAAc,wBAAwB;AACtC,cAAc,iBAAiB;AAC/B,cAAc,oBAAoB"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import 'reflect-metadata';\n\nexport * from './lib/entity.js';\nexport * from './lib/entity-utils.js';\nexport * from './lib/types.js';\nexport * from './lib/property.js';\nexport * from './lib/validation-error.js';\nexport * from './lib/problem.js';\nexport * from './lib/validation-utils.js';\nexport * from './lib/primitive-deserializers.js';\n"],"names":[],"mappings":"AAAA,OAAO,mBAAmB;AAE1B,cAAc,kBAAkB;AAChC,cAAc,wBAAwB;AACtC,cAAc,iBAAiB;AAC/B,cAAc,oBAAoB;AAClC,cAAc,4BAA4B;AAC1C,cAAc,mBAAmB;AACjC,cAAc,4BAA4B;AAC1C,cAAc,mCAAmC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PropertyOptions } from './types.js';
|
|
2
|
+
import { Problem } from './problem.js';
|
|
2
3
|
export declare class EntityUtils {
|
|
3
4
|
/**
|
|
4
5
|
* Checks if a given object is an instance of a class decorated with @Entity()
|
|
@@ -90,8 +91,9 @@ export declare class EntityUtils {
|
|
|
90
91
|
/**
|
|
91
92
|
* Deserializes a plain object to an entity instance
|
|
92
93
|
*
|
|
93
|
-
* @param entityClass - The entity class constructor
|
|
94
|
+
* @param entityClass - The entity class constructor. Must accept a data object parameter.
|
|
94
95
|
* @param plainObject - The plain object to deserialize
|
|
96
|
+
* @param options - Parse options (strict mode)
|
|
95
97
|
* @returns A new instance of the entity with deserialized values
|
|
96
98
|
*
|
|
97
99
|
* @remarks
|
|
@@ -103,6 +105,14 @@ export declare class EntityUtils {
|
|
|
103
105
|
* - Arrays are supported with the array: true option
|
|
104
106
|
* - Nested entities are recursively deserialized
|
|
105
107
|
* - Type conversion is strict (no coercion)
|
|
108
|
+
* - Entity constructors must accept a required data parameter
|
|
109
|
+
*
|
|
110
|
+
* Validation behavior:
|
|
111
|
+
* - If strict: true - both HARD and SOFT problems throw ValidationError
|
|
112
|
+
* - If strict: false (default) - HARD problems throw ValidationError, SOFT problems stored
|
|
113
|
+
* - Property validators run first, then entity validators
|
|
114
|
+
* - Problems are accessible via EntityUtils.problems()
|
|
115
|
+
* - Raw input data is accessible via EntityUtils.getRawInput()
|
|
106
116
|
*
|
|
107
117
|
* @example
|
|
108
118
|
* ```typescript
|
|
@@ -110,15 +120,20 @@ export declare class EntityUtils {
|
|
|
110
120
|
* class User {
|
|
111
121
|
* @Property({ type: () => String }) name!: string;
|
|
112
122
|
* @Property({ type: () => Number }) age!: number;
|
|
113
|
-
*
|
|
123
|
+
*
|
|
124
|
+
* constructor(data: Partial<User>) {
|
|
125
|
+
* Object.assign(this, data);
|
|
126
|
+
* }
|
|
114
127
|
* }
|
|
115
128
|
*
|
|
116
129
|
* const json = { name: 'John', age: 30 };
|
|
117
130
|
* const user = EntityUtils.parse(User, json);
|
|
118
|
-
*
|
|
131
|
+
* const userStrict = EntityUtils.parse(User, json, { strict: true });
|
|
119
132
|
* ```
|
|
120
133
|
*/
|
|
121
|
-
static parse<T extends object>(entityClass: new () => T, plainObject: Record<string, unknown
|
|
134
|
+
static parse<T extends object>(entityClass: new (data: any) => T, plainObject: Record<string, unknown>, options?: {
|
|
135
|
+
strict?: boolean;
|
|
136
|
+
}): T;
|
|
122
137
|
/**
|
|
123
138
|
* Deserializes a single value according to the type metadata
|
|
124
139
|
* @private
|
|
@@ -126,8 +141,81 @@ export declare class EntityUtils {
|
|
|
126
141
|
private static deserializeValue;
|
|
127
142
|
/**
|
|
128
143
|
* Deserializes a single non-array value
|
|
144
|
+
* Reports validation errors with empty property (caller will prepend context)
|
|
129
145
|
* @private
|
|
130
146
|
*/
|
|
131
147
|
private static deserializeSingleValue;
|
|
148
|
+
/**
|
|
149
|
+
* Validates a property value by running validators and nested entity validation.
|
|
150
|
+
* Prepends the property path to all returned problems.
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
private static validatePropertyValue;
|
|
154
|
+
/**
|
|
155
|
+
* Runs property validators for a given property value
|
|
156
|
+
* @private
|
|
157
|
+
*/
|
|
158
|
+
private static runPropertyValidators;
|
|
159
|
+
/**
|
|
160
|
+
* Validates an entity instance by running all property and entity validators
|
|
161
|
+
*
|
|
162
|
+
* @param instance - The entity instance to validate
|
|
163
|
+
* @returns Array of Problems found during validation (empty if valid)
|
|
164
|
+
*
|
|
165
|
+
* @remarks
|
|
166
|
+
* - Property validators run first, then entity validators
|
|
167
|
+
* - Each validator returns an array of Problems
|
|
168
|
+
* - Empty array means no problems found
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* const user = new User({ name: '', age: -5 });
|
|
173
|
+
* const problems = EntityUtils.validate(user);
|
|
174
|
+
* console.log(problems); // [Problem, Problem, ...]
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
static validate<T extends object>(instance: T): Problem[];
|
|
178
|
+
/**
|
|
179
|
+
* Gets the validation problems for an entity instance
|
|
180
|
+
*
|
|
181
|
+
* @param instance - The entity instance
|
|
182
|
+
* @returns Array of Problems (empty if no problems or instance not parsed)
|
|
183
|
+
*
|
|
184
|
+
* @remarks
|
|
185
|
+
* - Only returns problems from the last parse() call
|
|
186
|
+
* - Returns empty array if instance was not created via parse()
|
|
187
|
+
* - Returns empty array if parse() was called with strict: true
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* const user = EntityUtils.parse(User, data);
|
|
192
|
+
* const problems = EntityUtils.problems(user);
|
|
193
|
+
* console.log(problems); // [Problem, ...]
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
static problems<T extends object>(instance: T): Problem[];
|
|
197
|
+
/**
|
|
198
|
+
* Gets the raw input data that was used to create an entity instance
|
|
199
|
+
*
|
|
200
|
+
* @param instance - The entity instance
|
|
201
|
+
* @returns The raw input object, or undefined if not available
|
|
202
|
+
*
|
|
203
|
+
* @remarks
|
|
204
|
+
* - Only available for instances created via parse()
|
|
205
|
+
* - Returns a reference to the original input data (not a copy)
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* const user = EntityUtils.parse(User, { name: 'John', age: 30 });
|
|
210
|
+
* const rawInput = EntityUtils.getRawInput(user);
|
|
211
|
+
* console.log(rawInput); // { name: 'John', age: 30 }
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
static getRawInput<T extends object>(instance: T): Record<string, unknown> | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* Gets all entity validator method names for an entity
|
|
217
|
+
* @private
|
|
218
|
+
*/
|
|
219
|
+
private static getEntityValidators;
|
|
132
220
|
}
|
|
133
221
|
//# sourceMappingURL=entity-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-utils.d.ts","sourceRoot":"","sources":["../../src/lib/entity-utils.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"entity-utils.d.ts","sourceRoot":"","sources":["../../src/lib/entity-utils.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,eAAe,EAChB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAuBvC,qBAAa,WAAW;IACtB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,MAAM;IAmB5C,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO;IAQhD,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE;IAoChD,MAAM,CAAC,kBAAkB,CACvB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,eAAe,GAAG,SAAS;IA8B9B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO;IA2B9C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,MAAM,EAC1B,SAAS,EAAE,CAAC,EACZ,SAAS,EAAE,CAAC,GACX;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,EAAE;IAoC/D,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAaxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiDG;IACH,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAmBnE;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAsD7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA0CG;IACH,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAC3B,WAAW,EAAE,KAAK,IAAI,EAAE,GAAG,KAAK,CAAC,EACjC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAC7B,CAAC;IAgGJ;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IA+D/B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IA0BrC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAkCpC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAoDpC;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO,EAAE;IAkCzD;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO,EAAE;IAIzD;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,WAAW,CAAC,CAAC,SAAS,MAAM,EACjC,QAAQ,EAAE,CAAC,GACV,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAItC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;CA6BnC"}
|
package/dist/lib/entity-utils.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */ import { ENTITY_METADATA_KEY, PROPERTY_METADATA_KEY, PROPERTY_OPTIONS_METADATA_KEY } from './types.js';
|
|
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';
|
|
2
2
|
import { isEqualWith } from 'lodash-es';
|
|
3
|
+
import { ValidationError } from './validation-error.js';
|
|
4
|
+
import { Problem } from './problem.js';
|
|
5
|
+
import { prependPropertyPath, prependArrayIndex, createValidationError, combinePropertyPaths } from './validation-utils.js';
|
|
6
|
+
import { isPrimitiveConstructor, deserializePrimitive } from './primitive-deserializers.js';
|
|
7
|
+
import { ok } from 'assert';
|
|
8
|
+
/**
|
|
9
|
+
* WeakMap to store validation problems for entity instances
|
|
10
|
+
*/ const problemsStorage = new WeakMap();
|
|
11
|
+
/**
|
|
12
|
+
* WeakMap to store raw input data for entity instances
|
|
13
|
+
*/ const rawInputStorage = new WeakMap();
|
|
3
14
|
export class EntityUtils {
|
|
4
15
|
/**
|
|
5
16
|
* Checks if a given object is an instance of a class decorated with @Entity()
|
|
@@ -248,8 +259,9 @@ export class EntityUtils {
|
|
|
248
259
|
/**
|
|
249
260
|
* Deserializes a plain object to an entity instance
|
|
250
261
|
*
|
|
251
|
-
* @param entityClass - The entity class constructor
|
|
262
|
+
* @param entityClass - The entity class constructor. Must accept a data object parameter.
|
|
252
263
|
* @param plainObject - The plain object to deserialize
|
|
264
|
+
* @param options - Parse options (strict mode)
|
|
253
265
|
* @returns A new instance of the entity with deserialized values
|
|
254
266
|
*
|
|
255
267
|
* @remarks
|
|
@@ -261,6 +273,14 @@ export class EntityUtils {
|
|
|
261
273
|
* - Arrays are supported with the array: true option
|
|
262
274
|
* - Nested entities are recursively deserialized
|
|
263
275
|
* - Type conversion is strict (no coercion)
|
|
276
|
+
* - Entity constructors must accept a required data parameter
|
|
277
|
+
*
|
|
278
|
+
* Validation behavior:
|
|
279
|
+
* - If strict: true - both HARD and SOFT problems throw ValidationError
|
|
280
|
+
* - If strict: false (default) - HARD problems throw ValidationError, SOFT problems stored
|
|
281
|
+
* - Property validators run first, then entity validators
|
|
282
|
+
* - Problems are accessible via EntityUtils.problems()
|
|
283
|
+
* - Raw input data is accessible via EntityUtils.getRawInput()
|
|
264
284
|
*
|
|
265
285
|
* @example
|
|
266
286
|
* ```typescript
|
|
@@ -268,130 +288,323 @@ export class EntityUtils {
|
|
|
268
288
|
* class User {
|
|
269
289
|
* @Property({ type: () => String }) name!: string;
|
|
270
290
|
* @Property({ type: () => Number }) age!: number;
|
|
271
|
-
*
|
|
291
|
+
*
|
|
292
|
+
* constructor(data: Partial<User>) {
|
|
293
|
+
* Object.assign(this, data);
|
|
294
|
+
* }
|
|
272
295
|
* }
|
|
273
296
|
*
|
|
274
297
|
* const json = { name: 'John', age: 30 };
|
|
275
298
|
* const user = EntityUtils.parse(User, json);
|
|
276
|
-
*
|
|
299
|
+
* const userStrict = EntityUtils.parse(User, json, { strict: true });
|
|
277
300
|
* ```
|
|
278
|
-
*/ static parse(entityClass, plainObject) {
|
|
279
|
-
const
|
|
280
|
-
const keys = this.getPropertyKeys(
|
|
301
|
+
*/ static parse(entityClass, plainObject, options) {
|
|
302
|
+
const strict = options?.strict ?? false;
|
|
303
|
+
const keys = this.getPropertyKeys(entityClass.prototype);
|
|
304
|
+
const data = {};
|
|
305
|
+
const hardProblems = [];
|
|
281
306
|
for (const key of keys){
|
|
282
|
-
const
|
|
283
|
-
if (!
|
|
284
|
-
|
|
307
|
+
const propertyOptions = this.getPropertyOptions(entityClass.prototype, key);
|
|
308
|
+
if (!propertyOptions) {
|
|
309
|
+
hardProblems.push(new Problem({
|
|
310
|
+
property: key,
|
|
311
|
+
message: `Property has no metadata. This should not happen if @Property() was used correctly.`
|
|
312
|
+
}));
|
|
313
|
+
continue;
|
|
285
314
|
}
|
|
286
|
-
if (
|
|
315
|
+
if (propertyOptions.passthrough === true) {
|
|
287
316
|
const value = plainObject[key];
|
|
288
|
-
|
|
317
|
+
data[key] = value;
|
|
289
318
|
continue;
|
|
290
319
|
}
|
|
291
320
|
const value = plainObject[key];
|
|
292
|
-
const isOptional =
|
|
321
|
+
const isOptional = propertyOptions.optional === true;
|
|
293
322
|
if (!(key in plainObject)) {
|
|
294
323
|
if (!isOptional) {
|
|
295
|
-
|
|
324
|
+
hardProblems.push(new Problem({
|
|
325
|
+
property: key,
|
|
326
|
+
message: 'Required property is missing from input'
|
|
327
|
+
}));
|
|
296
328
|
}
|
|
297
329
|
continue;
|
|
298
330
|
}
|
|
299
331
|
if (value === null || value === undefined) {
|
|
300
332
|
if (!isOptional) {
|
|
301
|
-
|
|
333
|
+
hardProblems.push(new Problem({
|
|
334
|
+
property: key,
|
|
335
|
+
message: 'Cannot be null or undefined'
|
|
336
|
+
}));
|
|
302
337
|
}
|
|
303
|
-
|
|
338
|
+
data[key] = value;
|
|
304
339
|
continue;
|
|
305
340
|
}
|
|
306
|
-
|
|
341
|
+
try {
|
|
342
|
+
data[key] = this.deserializeValue(value, propertyOptions);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (error instanceof ValidationError) {
|
|
345
|
+
const problems = prependPropertyPath(key, error);
|
|
346
|
+
hardProblems.push(...problems);
|
|
347
|
+
} else if (error instanceof Error) {
|
|
348
|
+
hardProblems.push(new Problem({
|
|
349
|
+
property: key,
|
|
350
|
+
message: error.message
|
|
351
|
+
}));
|
|
352
|
+
} else {
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (hardProblems.length > 0) {
|
|
358
|
+
throw new ValidationError(hardProblems);
|
|
359
|
+
}
|
|
360
|
+
const instance = new entityClass(data);
|
|
361
|
+
rawInputStorage.set(instance, plainObject);
|
|
362
|
+
const problems = this.validate(instance);
|
|
363
|
+
if (problems.length > 0) {
|
|
364
|
+
if (strict) {
|
|
365
|
+
throw new ValidationError(problems);
|
|
366
|
+
} else {
|
|
367
|
+
problemsStorage.set(instance, problems);
|
|
368
|
+
}
|
|
307
369
|
}
|
|
308
370
|
return instance;
|
|
309
371
|
}
|
|
310
372
|
/**
|
|
311
373
|
* Deserializes a single value according to the type metadata
|
|
312
374
|
* @private
|
|
313
|
-
*/ static deserializeValue(value, options
|
|
375
|
+
*/ static deserializeValue(value, options) {
|
|
314
376
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
315
377
|
const typeConstructor = options.type();
|
|
316
378
|
const isArray = options.array === true;
|
|
317
379
|
const isSparse = options.sparse === true;
|
|
318
380
|
if (isArray) {
|
|
319
381
|
if (!Array.isArray(value)) {
|
|
320
|
-
throw
|
|
382
|
+
throw createValidationError(`Expects an array but received ${typeof value}`);
|
|
321
383
|
}
|
|
322
|
-
|
|
384
|
+
const arrayProblems = [];
|
|
385
|
+
const result = [];
|
|
386
|
+
for(let index = 0; index < value.length; index++){
|
|
387
|
+
const item = value[index];
|
|
323
388
|
if (item === null || item === undefined) {
|
|
324
389
|
if (!isSparse) {
|
|
325
|
-
|
|
390
|
+
arrayProblems.push(new Problem({
|
|
391
|
+
property: `[${index}]`,
|
|
392
|
+
message: 'Cannot be null or undefined.'
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
result.push(item);
|
|
396
|
+
} else {
|
|
397
|
+
try {
|
|
398
|
+
if (options.deserialize) {
|
|
399
|
+
result.push(options.deserialize(item));
|
|
400
|
+
} else {
|
|
401
|
+
result.push(this.deserializeSingleValue(item, typeConstructor));
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (error instanceof ValidationError) {
|
|
405
|
+
const problems = prependArrayIndex(index, error);
|
|
406
|
+
arrayProblems.push(...problems);
|
|
407
|
+
} else {
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
326
410
|
}
|
|
327
|
-
return item;
|
|
328
|
-
}
|
|
329
|
-
if (options.deserialize) {
|
|
330
|
-
return options.deserialize(item);
|
|
331
411
|
}
|
|
332
|
-
|
|
333
|
-
|
|
412
|
+
}
|
|
413
|
+
if (arrayProblems.length > 0) {
|
|
414
|
+
throw new ValidationError(arrayProblems);
|
|
415
|
+
}
|
|
416
|
+
return result;
|
|
334
417
|
}
|
|
335
418
|
if (options.deserialize) {
|
|
336
419
|
return options.deserialize(value);
|
|
337
420
|
}
|
|
338
|
-
return this.deserializeSingleValue(value, typeConstructor
|
|
421
|
+
return this.deserializeSingleValue(value, typeConstructor);
|
|
339
422
|
}
|
|
340
423
|
/**
|
|
341
424
|
* Deserializes a single non-array value
|
|
425
|
+
* Reports validation errors with empty property (caller will prepend context)
|
|
342
426
|
* @private
|
|
343
|
-
*/ static deserializeSingleValue(value, typeConstructor
|
|
344
|
-
if (typeConstructor
|
|
345
|
-
|
|
346
|
-
throw new Error(`Property '${propertyKey}' expects a string but received ${typeof value}`);
|
|
347
|
-
}
|
|
348
|
-
return value;
|
|
427
|
+
*/ static deserializeSingleValue(value, typeConstructor) {
|
|
428
|
+
if (isPrimitiveConstructor(typeConstructor)) {
|
|
429
|
+
return deserializePrimitive(value, typeConstructor);
|
|
349
430
|
}
|
|
350
|
-
if (typeConstructor
|
|
351
|
-
if (typeof value !== '
|
|
352
|
-
throw
|
|
431
|
+
if (this.isEntity(typeConstructor)) {
|
|
432
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
433
|
+
throw createValidationError(`Expects an object but received ${typeof value}`);
|
|
353
434
|
}
|
|
354
|
-
return value;
|
|
435
|
+
return this.parse(typeConstructor, value);
|
|
355
436
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
437
|
+
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.`);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Validates a property value by running validators and nested entity validation.
|
|
441
|
+
* Prepends the property path to all returned problems.
|
|
442
|
+
* @private
|
|
443
|
+
*/ static validatePropertyValue(propertyPath, value, validators) {
|
|
444
|
+
const problems = [];
|
|
445
|
+
if (validators) {
|
|
446
|
+
for (const validator of validators){
|
|
447
|
+
const validatorProblems = validator({
|
|
448
|
+
value
|
|
449
|
+
});
|
|
450
|
+
// Prepend propertyPath to all problems
|
|
451
|
+
for (const problem of validatorProblems){
|
|
452
|
+
problems.push(new Problem({
|
|
453
|
+
property: combinePropertyPaths(propertyPath, problem.property),
|
|
454
|
+
message: problem.message
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
359
457
|
}
|
|
360
|
-
return value;
|
|
361
458
|
}
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
|
|
459
|
+
if (EntityUtils.isEntity(value)) {
|
|
460
|
+
const nestedProblems = EntityUtils.validate(value);
|
|
461
|
+
const prependedProblems = prependPropertyPath(propertyPath, new ValidationError(nestedProblems));
|
|
462
|
+
problems.push(...prependedProblems);
|
|
463
|
+
}
|
|
464
|
+
return problems;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Runs property validators for a given property value
|
|
468
|
+
* @private
|
|
469
|
+
*/ static runPropertyValidators(key, value, options) {
|
|
470
|
+
const problems = [];
|
|
471
|
+
const isArray = options?.array === true;
|
|
472
|
+
const isPassthrough = options?.passthrough === true;
|
|
473
|
+
if (isPassthrough || !isArray) {
|
|
474
|
+
const valueProblems = this.validatePropertyValue(key, value, options.validators);
|
|
475
|
+
problems.push(...valueProblems);
|
|
476
|
+
} else {
|
|
477
|
+
ok(Array.isArray(value), 'Value must be an array for array property');
|
|
478
|
+
const arrayValidators = options.arrayValidators || [];
|
|
479
|
+
for (const validator of arrayValidators){
|
|
480
|
+
const validatorProblems = validator({
|
|
481
|
+
value
|
|
482
|
+
});
|
|
483
|
+
for (const problem of validatorProblems){
|
|
484
|
+
problems.push(new Problem({
|
|
485
|
+
property: combinePropertyPaths(key, problem.property),
|
|
486
|
+
message: problem.message
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
365
489
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
490
|
+
const validators = options.validators || [];
|
|
491
|
+
if (validators.length > 0) {
|
|
492
|
+
for(let i = 0; i < value.length; i++){
|
|
493
|
+
const element = value[i];
|
|
494
|
+
if (element !== null && element !== undefined) {
|
|
495
|
+
const elementPath = `${key}[${i}]`;
|
|
496
|
+
const elementProblems = this.validatePropertyValue(elementPath, element, validators);
|
|
497
|
+
problems.push(...elementProblems);
|
|
498
|
+
}
|
|
371
499
|
}
|
|
372
500
|
}
|
|
373
|
-
throw new Error(`Property '${propertyKey}' expects a bigint or string but received ${typeof value}`);
|
|
374
501
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
502
|
+
return problems;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Validates an entity instance by running all property and entity validators
|
|
506
|
+
*
|
|
507
|
+
* @param instance - The entity instance to validate
|
|
508
|
+
* @returns Array of Problems found during validation (empty if valid)
|
|
509
|
+
*
|
|
510
|
+
* @remarks
|
|
511
|
+
* - Property validators run first, then entity validators
|
|
512
|
+
* - Each validator returns an array of Problems
|
|
513
|
+
* - Empty array means no problems found
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* ```typescript
|
|
517
|
+
* const user = new User({ name: '', age: -5 });
|
|
518
|
+
* const problems = EntityUtils.validate(user);
|
|
519
|
+
* console.log(problems); // [Problem, Problem, ...]
|
|
520
|
+
* ```
|
|
521
|
+
*/ static validate(instance) {
|
|
522
|
+
if (!this.isEntity(instance)) {
|
|
523
|
+
throw new Error('Cannot validate non-entity instance');
|
|
524
|
+
}
|
|
525
|
+
const problems = [];
|
|
526
|
+
const keys = this.getPropertyKeys(instance);
|
|
527
|
+
for (const key of keys){
|
|
528
|
+
const options = this.getPropertyOptions(instance, key);
|
|
529
|
+
if (options) {
|
|
530
|
+
const value = instance[key];
|
|
531
|
+
if (value != null) {
|
|
532
|
+
const validationProblems = this.runPropertyValidators(key, value, options);
|
|
533
|
+
problems.push(...validationProblems);
|
|
383
534
|
}
|
|
384
|
-
return date;
|
|
385
535
|
}
|
|
386
|
-
throw new Error(`Property '${propertyKey}' expects a Date or ISO string but received ${typeof value}`);
|
|
387
536
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
537
|
+
const entityValidators = this.getEntityValidators(instance);
|
|
538
|
+
for (const validatorMethod of entityValidators){
|
|
539
|
+
const validatorProblems = instance[validatorMethod]();
|
|
540
|
+
if (Array.isArray(validatorProblems)) {
|
|
541
|
+
problems.push(...validatorProblems);
|
|
391
542
|
}
|
|
392
|
-
return this.parse(typeConstructor, value);
|
|
393
543
|
}
|
|
394
|
-
|
|
544
|
+
return problems;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Gets the validation problems for an entity instance
|
|
548
|
+
*
|
|
549
|
+
* @param instance - The entity instance
|
|
550
|
+
* @returns Array of Problems (empty if no problems or instance not parsed)
|
|
551
|
+
*
|
|
552
|
+
* @remarks
|
|
553
|
+
* - Only returns problems from the last parse() call
|
|
554
|
+
* - Returns empty array if instance was not created via parse()
|
|
555
|
+
* - Returns empty array if parse() was called with strict: true
|
|
556
|
+
*
|
|
557
|
+
* @example
|
|
558
|
+
* ```typescript
|
|
559
|
+
* const user = EntityUtils.parse(User, data);
|
|
560
|
+
* const problems = EntityUtils.problems(user);
|
|
561
|
+
* console.log(problems); // [Problem, ...]
|
|
562
|
+
* ```
|
|
563
|
+
*/ static problems(instance) {
|
|
564
|
+
return problemsStorage.get(instance) || [];
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Gets the raw input data that was used to create an entity instance
|
|
568
|
+
*
|
|
569
|
+
* @param instance - The entity instance
|
|
570
|
+
* @returns The raw input object, or undefined if not available
|
|
571
|
+
*
|
|
572
|
+
* @remarks
|
|
573
|
+
* - Only available for instances created via parse()
|
|
574
|
+
* - Returns a reference to the original input data (not a copy)
|
|
575
|
+
*
|
|
576
|
+
* @example
|
|
577
|
+
* ```typescript
|
|
578
|
+
* const user = EntityUtils.parse(User, { name: 'John', age: 30 });
|
|
579
|
+
* const rawInput = EntityUtils.getRawInput(user);
|
|
580
|
+
* console.log(rawInput); // { name: 'John', age: 30 }
|
|
581
|
+
* ```
|
|
582
|
+
*/ static getRawInput(instance) {
|
|
583
|
+
return rawInputStorage.get(instance);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Gets all entity validator method names for an entity
|
|
587
|
+
* @private
|
|
588
|
+
*/ static getEntityValidators(target) {
|
|
589
|
+
let currentProto;
|
|
590
|
+
if (target.constructor && target === target.constructor.prototype) {
|
|
591
|
+
currentProto = target;
|
|
592
|
+
} else {
|
|
593
|
+
currentProto = Object.getPrototypeOf(target);
|
|
594
|
+
}
|
|
595
|
+
const validators = [];
|
|
596
|
+
const seen = new Set();
|
|
597
|
+
while(currentProto && currentProto !== Object.prototype){
|
|
598
|
+
const protoValidators = Reflect.getOwnMetadata(ENTITY_VALIDATOR_METADATA_KEY, currentProto) || [];
|
|
599
|
+
for (const validator of protoValidators){
|
|
600
|
+
if (!seen.has(validator)) {
|
|
601
|
+
seen.add(validator);
|
|
602
|
+
validators.push(validator);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
currentProto = Object.getPrototypeOf(currentProto);
|
|
606
|
+
}
|
|
607
|
+
return validators;
|
|
395
608
|
}
|
|
396
609
|
}
|
|
397
610
|
|