@rtpaulino/entity 0.13.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.
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 +86 -1
  6. package/dist/lib/entity-utils.d.ts.map +1 -1
  7. package/dist/lib/entity-utils.js +270 -60
  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 +14 -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 +79 -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,6 +93,7 @@ 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
96
+ * @param options - Parse options (strict mode)
95
97
  * @returns A new instance of the entity with deserialized values
96
98
  *
97
99
  * @remarks
@@ -105,6 +107,13 @@ 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
+ * - Problems are accessible via EntityUtils.problems()
115
+ * - Raw input data is accessible via EntityUtils.getRawInput()
116
+ *
108
117
  * @example
109
118
  * ```typescript
110
119
  * @Entity()
@@ -119,9 +128,12 @@ export declare class EntityUtils {
119
128
  *
120
129
  * const json = { name: 'John', age: 30 };
121
130
  * const user = EntityUtils.parse(User, json);
131
+ * const userStrict = EntityUtils.parse(User, json, { strict: true });
122
132
  * ```
123
133
  */
124
- static parse<T extends object>(entityClass: new (data: any) => T, plainObject: Record<string, unknown>): T;
134
+ static parse<T extends object>(entityClass: new (data: any) => T, plainObject: Record<string, unknown>, options?: {
135
+ strict?: boolean;
136
+ }): T;
125
137
  /**
126
138
  * Deserializes a single value according to the type metadata
127
139
  * @private
@@ -129,8 +141,81 @@ export declare class EntityUtils {
129
141
  private static deserializeValue;
130
142
  /**
131
143
  * Deserializes a single non-array value
144
+ * Reports validation errors with empty property (caller will prepend context)
132
145
  * @private
133
146
  */
134
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;
135
220
  }
136
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,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
@@ -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,6 +261,7 @@ 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
264
+ * @param options - Parse options (strict mode)
253
265
  * @returns A new instance of the entity with deserialized values
254
266
  *
255
267
  * @remarks
@@ -263,6 +275,13 @@ 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
+ * - Problems are accessible via EntityUtils.problems()
283
+ * - Raw input data is accessible via EntityUtils.getRawInput()
284
+ *
266
285
  * @example
267
286
  * ```typescript
268
287
  * @Entity()
@@ -277,124 +296,315 @@ export class EntityUtils {
277
296
  *
278
297
  * const json = { name: 'John', age: 30 };
279
298
  * const user = EntityUtils.parse(User, json);
299
+ * const userStrict = EntityUtils.parse(User, json, { strict: true });
280
300
  * ```
281
- */ static parse(entityClass, plainObject) {
301
+ */ static parse(entityClass, plainObject, options) {
302
+ const strict = options?.strict ?? false;
282
303
  const keys = this.getPropertyKeys(entityClass.prototype);
283
304
  const data = {};
305
+ const hardProblems = [];
284
306
  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.`);
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;
288
314
  }
289
- if (options.passthrough === true) {
315
+ if (propertyOptions.passthrough === true) {
290
316
  const value = plainObject[key];
291
317
  data[key] = value;
292
318
  continue;
293
319
  }
294
320
  const value = plainObject[key];
295
- const isOptional = options.optional === true;
321
+ const isOptional = propertyOptions.optional === true;
296
322
  if (!(key in plainObject)) {
297
323
  if (!isOptional) {
298
- throw new Error(`Property '${key}' is required but missing from input`);
324
+ hardProblems.push(new Problem({
325
+ property: key,
326
+ message: 'Required property is missing from input'
327
+ }));
299
328
  }
300
329
  continue;
301
330
  }
302
331
  if (value === null || value === undefined) {
303
332
  if (!isOptional) {
304
- throw new Error(`Property '${key}' cannot be null or undefined`);
333
+ hardProblems.push(new Problem({
334
+ property: key,
335
+ message: 'Cannot be null or undefined'
336
+ }));
305
337
  }
306
338
  data[key] = value;
307
339
  continue;
308
340
  }
309
- data[key] = this.deserializeValue(value, options, key);
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);
310
359
  }
311
- return new entityClass(data);
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
+ }
369
+ }
370
+ return instance;
312
371
  }
313
372
  /**
314
373
  * Deserializes a single value according to the type metadata
315
374
  * @private
316
- */ static deserializeValue(value, options, propertyKey) {
375
+ */ static deserializeValue(value, options) {
317
376
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
318
377
  const typeConstructor = options.type();
319
378
  const isArray = options.array === true;
320
379
  const isSparse = options.sparse === true;
321
380
  if (isArray) {
322
381
  if (!Array.isArray(value)) {
323
- throw new Error(`Property '${propertyKey}' expects an array but received ${typeof value}`);
382
+ throw createValidationError(`Expects an array but received ${typeof value}`);
324
383
  }
325
- return value.map((item, index)=>{
384
+ const arrayProblems = [];
385
+ const result = [];
386
+ for(let index = 0; index < value.length; index++){
387
+ const item = value[index];
326
388
  if (item === null || item === undefined) {
327
389
  if (!isSparse) {
328
- throw new Error(`Property '${propertyKey}[${index}]' cannot be null or undefined. Use sparse: true to allow null/undefined elements in arrays.`);
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
+ }
329
410
  }
330
- return item;
331
- }
332
- if (options.deserialize) {
333
- return options.deserialize(item);
334
411
  }
335
- return this.deserializeSingleValue(item, typeConstructor, `${propertyKey}[${index}]`);
336
- });
412
+ }
413
+ if (arrayProblems.length > 0) {
414
+ throw new ValidationError(arrayProblems);
415
+ }
416
+ return result;
337
417
  }
338
418
  if (options.deserialize) {
339
419
  return options.deserialize(value);
340
420
  }
341
- return this.deserializeSingleValue(value, typeConstructor, propertyKey);
421
+ return this.deserializeSingleValue(value, typeConstructor);
342
422
  }
343
423
  /**
344
424
  * Deserializes a single non-array value
425
+ * Reports validation errors with empty property (caller will prepend context)
345
426
  * @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;
427
+ */ static deserializeSingleValue(value, typeConstructor) {
428
+ if (isPrimitiveConstructor(typeConstructor)) {
429
+ return deserializePrimitive(value, typeConstructor);
352
430
  }
353
- if (typeConstructor === Number) {
354
- if (typeof value !== 'number') {
355
- throw new Error(`Property '${propertyKey}' expects a number but received ${typeof value}`);
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}`);
356
434
  }
357
- return value;
435
+ return this.parse(typeConstructor, value);
358
436
  }
359
- if (typeConstructor === Boolean) {
360
- if (typeof value !== 'boolean') {
361
- throw new Error(`Property '${propertyKey}' expects a boolean but received ${typeof value}`);
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
+ }
362
457
  }
363
- return value;
364
458
  }
365
- if (typeConstructor === BigInt) {
366
- if (typeof value === 'bigint') {
367
- return value;
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
+ }
368
489
  }
369
- if (typeof value === 'string') {
370
- try {
371
- return BigInt(value);
372
- } catch {
373
- throw new Error(`Property '${propertyKey}' cannot parse '${value}' as BigInt`);
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
+ }
374
499
  }
375
500
  }
376
- throw new Error(`Property '${propertyKey}' expects a bigint or string but received ${typeof value}`);
377
501
  }
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`);
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);
386
534
  }
387
- return date;
388
535
  }
389
- throw new Error(`Property '${propertyKey}' expects a Date or ISO string but received ${typeof value}`);
390
536
  }
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}`);
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);
394
542
  }
395
- return this.parse(typeConstructor, value);
396
543
  }
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.`);
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;
398
608
  }
399
609
  }
400
610