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