metal-orm 1.0.93 → 1.0.95

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.93",
3
+ "version": "1.0.95",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -27,13 +27,14 @@
27
27
  "build": "tsup",
28
28
  "check": "tsc --noEmit",
29
29
  "gen:entities": "node scripts/generate-entities.mjs",
30
- "test": "vitest",
30
+ "test": "vitest --run",
31
+ "test:watch": "vitest",
31
32
  "test:ui": "vitest --ui",
32
33
  "test:mysql": "vitest --run tests/e2e/mysql-memory.test.ts tests/e2e/decorators-mysql-memory.test.ts tests/e2e/save-graph-mysql-memory.test.ts",
33
34
  "test:mysql:optimized": "vitest --run --config tests/e2e/mysql/vitest.config.ts",
34
35
  "test:sqlite": "vitest --run tests/e2e/sqlite-memory.test.ts tests/e2e/decorators-sqlite-memory.test.ts tests/e2e/save-graph-sqlite-memory.test.ts",
35
36
  "test:pglite": "vitest --run tests/e2e/pglite-memory.test.ts",
36
- "test:e2e": "vitest tests/e2e",
37
+ "test:e2e": "vitest --run tests/e2e",
37
38
  "show-sql": "node scripts/show-sql.mjs",
38
39
  "lint": "node scripts/run-eslint.mjs",
39
40
  "lint:fix": "node scripts/run-eslint.mjs --fix"
@@ -62,7 +63,7 @@
62
63
  "@electric-sql/pglite": "^0.3.14",
63
64
  "@typescript-eslint/eslint-plugin": "^8.20.0",
64
65
  "@typescript-eslint/parser": "^8.20.0",
65
- "@vitest/ui": "^4.0.14",
66
+ "@vitest/ui": "^4.0.18",
66
67
  "eslint": "^8.57.0",
67
68
  "eslint-plugin-deprecation": "^3.0.0",
68
69
  "express": "^5.2.1",
@@ -73,8 +74,8 @@
73
74
  "supertest": "^7.2.2",
74
75
  "tedious": "^19.1.3",
75
76
  "tsup": "^8.0.0",
77
+ "tsx": "^4.21.0",
76
78
  "typescript": "^5.5.0",
77
- "vitest": "^4.0.14"
78
- },
79
- "dependencies": {}
79
+ "vitest": "^4.0.18"
80
+ }
80
81
  }
@@ -20,8 +20,10 @@ import {
20
20
  EntityOrTableTarget,
21
21
  EntityOrTableTargetResolver,
22
22
  getAllEntityMetadata,
23
- getEntityMetadata
23
+ getEntityMetadata,
24
+ addTransformerMetadata
24
25
  } from '../orm/entity-metadata.js';
26
+ import { getDecoratorMetadata } from './decorator-metadata.js';
25
27
 
26
28
  import { tableRef, type TableRef } from '../schema/table.js';
27
29
  import {
@@ -156,7 +158,15 @@ export const bootstrapEntities = (): TableDef[] => {
156
158
  const metas = getAllEntityMetadata();
157
159
  const tableMap = new Map<EntityConstructor, TableDef>();
158
160
 
161
+ // Process decorator metadata for each entity
159
162
  for (const meta of metas) {
163
+ const decoratorMetadata = getDecoratorMetadata(meta.target);
164
+ if (decoratorMetadata?.transformers) {
165
+ for (const { propertyName, metadata } of decoratorMetadata.transformers) {
166
+ addTransformerMetadata(meta.target, propertyName, metadata);
167
+ }
168
+ }
169
+
160
170
  const table = buildTableDef(meta);
161
171
  tableMap.set(meta.target, table);
162
172
  }
@@ -1,4 +1,5 @@
1
1
  import { ColumnDefLike, RelationMetadata } from '../orm/entity-metadata.js';
2
+ import type { TransformerMetadata } from './transformers/transformer-metadata.js';
2
3
 
3
4
  /**
4
5
  * Bag for storing decorator metadata during the decoration phase.
@@ -6,6 +7,7 @@ import { ColumnDefLike, RelationMetadata } from '../orm/entity-metadata.js';
6
7
  export interface DecoratorMetadataBag {
7
8
  columns: Array<{ propertyName: string; column: ColumnDefLike }>;
8
9
  relations: Array<{ propertyName: string; relation: RelationMetadata }>;
10
+ transformers: Array<{ propertyName: string; metadata: TransformerMetadata }>;
9
11
  }
10
12
 
11
13
  const METADATA_KEY = 'metal-orm:decorators';
@@ -23,7 +25,7 @@ export const getOrCreateMetadataBag = (context: MetadataCarrier): DecoratorMetad
23
25
  const metadata = context.metadata || (context.metadata = {} as Record<PropertyKey, unknown>);
24
26
  let bag = metadata[METADATA_KEY] as DecoratorMetadataBag | undefined;
25
27
  if (!bag) {
26
- bag = { columns: [], relations: [] };
28
+ bag = { columns: [], relations: [], transformers: [] };
27
29
  metadata[METADATA_KEY] = bag;
28
30
  }
29
31
  return bag;
@@ -7,6 +7,33 @@ export * from './relations.js';
7
7
  export * from './bootstrap.js';
8
8
  export { getDecoratorMetadata } from './decorator-metadata.js';
9
9
 
10
+ // Transformer Decorators
11
+ export * from './transformers/transformer-decorators.js';
12
+ export type {
13
+ PropertyTransformer,
14
+ PropertyValidator,
15
+ PropertySanitizer,
16
+ CompositeTransformer,
17
+ AutoTransformableValidator,
18
+ TransformContext,
19
+ AutoTransformResult,
20
+ ValidationResult,
21
+ TransformerMetadata,
22
+ TransformerConfig
23
+ } from './transformers/transformer-metadata.js';
24
+
25
+ // Country Identifier Validators
26
+ export * from './validators/country-validators-decorators.js';
27
+ export { registerValidator, resolveValidator, getRegisteredValidators, hasValidator } from './validators/country-validator-registry.js';
28
+ export type {
29
+ CountryValidator,
30
+ CountryValidatorFactory,
31
+ ValidationOptions,
32
+ ValidationResult as CountryValidationResult,
33
+ AutoCorrectionResult,
34
+ ValidatorFactoryOptions
35
+ } from './validators/country-validators.js';
36
+
10
37
  // Entity Materialization - convert query results to real class instances
11
38
  export { materializeAs, DefaultEntityMaterializer, PrototypeMaterializationStrategy, ConstructorMaterializationStrategy } from '../orm/entity-materializer.js';
12
39
  export type { EntityMaterializer, EntityMaterializationStrategy } from '../orm/entity-materializer.js';
@@ -0,0 +1,231 @@
1
+ import type { PropertySanitizer, AutoTransformableValidator, AutoTransformResult, ValidationResult } from '../transformer-metadata.js';
2
+
3
+ // TrimTransformer
4
+ export class TrimTransformer implements PropertySanitizer<string> {
5
+ readonly name = 'trim';
6
+
7
+ constructor(private readonly options: { trimStart?: boolean; trimEnd?: boolean; trimAll?: boolean } = {}) {}
8
+
9
+ sanitize(value: string): string {
10
+ if (typeof value !== 'string') return value;
11
+
12
+ if (this.options.trimAll) {
13
+ return value.trim();
14
+ }
15
+
16
+ let result = value;
17
+ if (this.options.trimStart) {
18
+ result = result.trimStart();
19
+ }
20
+ if (this.options.trimEnd) {
21
+ result = result.trimEnd();
22
+ }
23
+ if (!this.options.trimStart && !this.options.trimEnd && !this.options.trimAll) {
24
+ result = result.trim();
25
+ }
26
+
27
+ return result;
28
+ }
29
+ }
30
+
31
+ // CaseTransformer
32
+ export class CaseTransformer implements PropertySanitizer<string> {
33
+ readonly name: string;
34
+
35
+ constructor(private readonly caseType: 'lower' | 'upper' | 'capitalize' | 'title') {
36
+ this.name = `case-${caseType}`;
37
+ }
38
+
39
+ sanitize(value: string): string {
40
+ if (typeof value !== 'string') return value;
41
+
42
+ switch (this.caseType) {
43
+ case 'lower':
44
+ return value.toLowerCase();
45
+ case 'upper':
46
+ return value.toUpperCase();
47
+ case 'capitalize':
48
+ return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
49
+ case 'title':
50
+ return value.split(' ').map(word =>
51
+ word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
52
+ ).join(' ');
53
+ default:
54
+ return value;
55
+ }
56
+ }
57
+ }
58
+
59
+ // AlphanumericValidator
60
+ export class AlphanumericValidator implements AutoTransformableValidator<string> {
61
+ readonly name = 'alphanumeric';
62
+
63
+ constructor(private readonly options: { allowSpaces?: boolean; allowUnderscores?: boolean; allowHyphens?: boolean } = {}) {}
64
+
65
+ validate(value: string): ValidationResult {
66
+ if (typeof value !== 'string') {
67
+ return { isValid: false, error: 'Value must be a string' };
68
+ }
69
+
70
+ const pattern = new RegExp(
71
+ `^[a-zA-Z0-9${this.options.allowSpaces ? ' ' : ''}${this.options.allowUnderscores ? '_' : ''}${this.options.allowHyphens ? '-' : ''}]*$`
72
+ );
73
+
74
+ return pattern.test(value)
75
+ ? { isValid: true }
76
+ : { isValid: false, error: 'Value must contain only alphanumeric characters' };
77
+ }
78
+
79
+ autoTransform(value: string): AutoTransformResult<string> {
80
+ if (typeof value !== 'string') {
81
+ return { success: false };
82
+ }
83
+
84
+ let result = value;
85
+ result = result.replace(/[^a-zA-Z0-9]/g, char => {
86
+ if (char === ' ' && this.options.allowSpaces) return ' ';
87
+ if (char === '_' && this.options.allowUnderscores) return '_';
88
+ if (char === '-' && this.options.allowHyphens) return '-';
89
+ return '';
90
+ });
91
+
92
+ return {
93
+ success: true,
94
+ correctedValue: result,
95
+ message: 'Removed non-alphanumeric characters'
96
+ };
97
+ }
98
+ }
99
+
100
+ // EmailValidator
101
+ export class EmailValidator implements AutoTransformableValidator<string> {
102
+ readonly name = 'email';
103
+
104
+ constructor(private readonly options: { allowPlus?: boolean; requireTLD?: boolean } = {}) {}
105
+
106
+ validate(value: string): ValidationResult {
107
+ if (typeof value !== 'string') {
108
+ return { isValid: false, error: 'Value must be a string' };
109
+ }
110
+
111
+ // Simple email regex - RFC 5322 is very complex, this covers most cases
112
+ const emailPattern = this.options.allowPlus
113
+ ? /^[^\s@]+@[^\s@]+\.[^\s@]+$/
114
+ : /^[^\s@+]+@[^\s@]+\.[^\s@]+$/;
115
+
116
+ if (!emailPattern.test(value)) {
117
+ return { isValid: false, error: 'Value must be a valid email address' };
118
+ }
119
+
120
+ if (this.options.requireTLD) {
121
+ const parts = value.split('.');
122
+ if (parts.length < 3 || parts[parts.length - 1].length < 2) {
123
+ return { isValid: false, error: 'Email must have a valid top-level domain' };
124
+ }
125
+ }
126
+
127
+ return { isValid: true };
128
+ }
129
+
130
+ autoTransform(value: string): AutoTransformResult<string> {
131
+ if (typeof value !== 'string') {
132
+ return { success: false };
133
+ }
134
+
135
+ let result = value.trim().toLowerCase();
136
+ if (!this.options.allowPlus) {
137
+ result = result.replace(/\+.*@/, '@');
138
+ }
139
+
140
+ return {
141
+ success: true,
142
+ correctedValue: result,
143
+ message: 'Trimmed and lowercased email'
144
+ };
145
+ }
146
+ }
147
+
148
+ // LengthValidator
149
+ export class LengthValidator implements AutoTransformableValidator<string> {
150
+ readonly name = 'length';
151
+
152
+ constructor(private readonly options: { min?: number; max?: number; exact?: number } = {}) {}
153
+
154
+ validate(value: string): ValidationResult {
155
+ if (typeof value !== 'string') {
156
+ return { isValid: false, error: 'Value must be a string' };
157
+ }
158
+
159
+ if (this.options.exact !== undefined && value.length !== this.options.exact) {
160
+ return { isValid: false, error: `Value must be exactly ${this.options.exact} characters long` };
161
+ }
162
+
163
+ if (this.options.min !== undefined && value.length < this.options.min) {
164
+ return { isValid: false, error: `Value must be at least ${this.options.min} characters long` };
165
+ }
166
+
167
+ if (this.options.max !== undefined && value.length > this.options.max) {
168
+ return { isValid: false, error: `Value must be at most ${this.options.max} characters long` };
169
+ }
170
+
171
+ return { isValid: true };
172
+ }
173
+
174
+ autoTransform(value: string): AutoTransformResult<string> {
175
+ if (typeof value !== 'string') {
176
+ return { success: false };
177
+ }
178
+
179
+ let result = value;
180
+
181
+ if (this.options.max !== undefined && result.length > this.options.max) {
182
+ result = result.slice(0, this.options.max);
183
+ }
184
+
185
+ if (this.options.min !== undefined && result.length < this.options.min) {
186
+ result = result.padEnd(this.options.min);
187
+ }
188
+
189
+ return {
190
+ success: true,
191
+ correctedValue: result,
192
+ message: 'Adjusted string length'
193
+ };
194
+ }
195
+ }
196
+
197
+ // PatternValidator
198
+ export class PatternValidator implements AutoTransformableValidator<string> {
199
+ readonly name = 'pattern';
200
+
201
+ constructor(private readonly options: { pattern: RegExp; flags?: string; errorMessage?: string; replacement?: string } = { pattern: /.*/ }) {}
202
+
203
+ validate(value: string): ValidationResult {
204
+ if (typeof value !== 'string') {
205
+ return { isValid: false, error: 'Value must be a string' };
206
+ }
207
+
208
+ const pattern = new RegExp(this.options.pattern.source, this.options.flags);
209
+
210
+ if (!pattern.test(value)) {
211
+ return { isValid: false, error: this.options.errorMessage || 'Value does not match required pattern' };
212
+ }
213
+
214
+ return { isValid: true };
215
+ }
216
+
217
+ autoTransform(value: string): AutoTransformResult<string> {
218
+ if (typeof value !== 'string' || !this.options.replacement) {
219
+ return { success: false };
220
+ }
221
+
222
+ const pattern = new RegExp(this.options.pattern.source, this.options.flags);
223
+ const result = value.replace(pattern, this.options.replacement);
224
+
225
+ return {
226
+ success: true,
227
+ correctedValue: result,
228
+ message: 'Replaced pattern matches'
229
+ };
230
+ }
231
+ }
@@ -0,0 +1,137 @@
1
+ import { getOrCreateMetadataBag } from '../decorator-metadata.js';
2
+ import type { TransformerMetadata } from './transformer-metadata.js';
3
+ import {
4
+ TrimTransformer,
5
+ CaseTransformer,
6
+ AlphanumericValidator,
7
+ EmailValidator,
8
+ LengthValidator,
9
+ PatternValidator
10
+ } from './built-in/string-transformers.js';
11
+
12
+ const normalizePropertyName = (name: string | symbol): string => {
13
+ if (typeof name === 'symbol') {
14
+ return name.description ?? name.toString();
15
+ }
16
+ return name;
17
+ };
18
+
19
+ const registerTransformerMetadata = (
20
+ context: ClassFieldDecoratorContext,
21
+ metadata: Partial<TransformerMetadata>
22
+ ): void => {
23
+ const propertyName = normalizePropertyName(context.name);
24
+ const bag = getOrCreateMetadataBag(context);
25
+
26
+ // Find existing transformer metadata for this property
27
+ let existing = bag.transformers.find(t => t.propertyName === propertyName);
28
+
29
+ if (!existing) {
30
+ existing = {
31
+ propertyName,
32
+ metadata: {
33
+ propertyName,
34
+ transformers: [],
35
+ validators: [],
36
+ sanitizers: [],
37
+ executionOrder: 'both'
38
+ }
39
+ };
40
+ bag.transformers.push(existing);
41
+ }
42
+
43
+ // Merge metadata
44
+ if (metadata.transformers) {
45
+ existing.metadata.transformers.push(...metadata.transformers);
46
+ }
47
+ if (metadata.validators) {
48
+ existing.metadata.validators.push(...metadata.validators);
49
+ }
50
+ if (metadata.sanitizers) {
51
+ existing.metadata.sanitizers.push(...metadata.sanitizers);
52
+ }
53
+ if (metadata.executionOrder) {
54
+ existing.metadata.executionOrder = metadata.executionOrder;
55
+ }
56
+ };
57
+
58
+ // Trim decorator
59
+ export function Trim(options?: { trimStart?: boolean; trimEnd?: boolean; trimAll?: boolean }) {
60
+ return function (_value: unknown, context: ClassFieldDecoratorContext) {
61
+ registerTransformerMetadata(context, {
62
+ sanitizers: [new TrimTransformer(options)]
63
+ });
64
+ };
65
+ }
66
+
67
+ // Lower case decorator
68
+ export function Lower() {
69
+ return function (_value: unknown, context: ClassFieldDecoratorContext) {
70
+ registerTransformerMetadata(context, {
71
+ sanitizers: [new CaseTransformer('lower')]
72
+ });
73
+ };
74
+ }
75
+
76
+ // Upper case decorator
77
+ export function Upper() {
78
+ return function (_value: unknown, context: ClassFieldDecoratorContext) {
79
+ registerTransformerMetadata(context, {
80
+ sanitizers: [new CaseTransformer('upper')]
81
+ });
82
+ };
83
+ }
84
+
85
+ // Capitalize decorator
86
+ export function Capitalize() {
87
+ return function (_value: unknown, context: ClassFieldDecoratorContext) {
88
+ registerTransformerMetadata(context, {
89
+ sanitizers: [new CaseTransformer('capitalize')]
90
+ });
91
+ };
92
+ }
93
+
94
+ // Title case decorator
95
+ export function Title() {
96
+ return function (_value: unknown, context: ClassFieldDecoratorContext) {
97
+ registerTransformerMetadata(context, {
98
+ sanitizers: [new CaseTransformer('title')]
99
+ });
100
+ };
101
+ }
102
+
103
+ // Alphanumeric decorator
104
+ export function Alphanumeric(options?: { allowSpaces?: boolean; allowUnderscores?: boolean; allowHyphens?: boolean }) {
105
+ return function (_value: unknown, context: ClassFieldDecoratorContext) {
106
+ registerTransformerMetadata(context, {
107
+ validators: [new AlphanumericValidator(options)]
108
+ });
109
+ };
110
+ }
111
+
112
+ // Email decorator
113
+ export function Email(options?: { allowPlus?: boolean; requireTLD?: boolean }) {
114
+ return function (_value: unknown, context: ClassFieldDecoratorContext) {
115
+ registerTransformerMetadata(context, {
116
+ validators: [new EmailValidator(options)]
117
+ });
118
+ };
119
+ }
120
+
121
+ // Length decorator
122
+ export function Length(options: { min?: number; max?: number; exact?: number }) {
123
+ return function (_value: unknown, context: ClassFieldDecoratorContext) {
124
+ registerTransformerMetadata(context, {
125
+ validators: [new LengthValidator(options)]
126
+ });
127
+ };
128
+ }
129
+
130
+ // Pattern decorator
131
+ export function Pattern(options: { pattern: RegExp; flags?: string; errorMessage?: string; replacement?: string }) {
132
+ return function (_value: unknown, context: ClassFieldDecoratorContext) {
133
+ registerTransformerMetadata(context, {
134
+ validators: [new PatternValidator(options)]
135
+ });
136
+ };
137
+ }
@@ -0,0 +1,172 @@
1
+ import { getEntityMetadata } from '../../orm/entity-metadata.js';
2
+ import type { EntityConstructor, EntityMetadata } from '../../orm/entity-metadata.js';
3
+ import type {
4
+ TransformContext,
5
+ TransformerConfig,
6
+ PropertyTransformer,
7
+ PropertyValidator,
8
+ PropertySanitizer,
9
+ TransformerMetadata
10
+ } from './transformer-metadata.js';
11
+
12
+ export class TransformerExecutor {
13
+ private config: TransformerConfig = {
14
+ auto: false,
15
+ execute: 'both',
16
+ stopOnFirstError: false
17
+ };
18
+
19
+ constructor(options?: TransformerConfig) {
20
+ this.config = { ...this.config, ...options };
21
+ }
22
+
23
+ /**
24
+ * Applies all transformers to an entity instance
25
+ */
26
+ async applyTransformers(entity: Record<string, unknown>, entityClass: EntityConstructor, context: Partial<TransformContext> = {}): Promise<void> {
27
+ const meta = getEntityMetadata(entityClass);
28
+ if (!meta || !meta.transformers) return;
29
+
30
+ // Get column types from entity metadata
31
+ const columnTypes = this.getColumnTypes(meta);
32
+
33
+ for (const [propertyName, transformerMeta] of Object.entries(meta.transformers)) {
34
+ const typedMeta = transformerMeta as unknown as TransformerMetadata;
35
+
36
+ // Skip if transformer should not execute in current context
37
+ if (!this.shouldExecute(typedMeta.executionOrder, context.isUpdate)) {
38
+ continue;
39
+ }
40
+
41
+ // Get current value
42
+ const value = entity[propertyName];
43
+
44
+ // Skip if value is null or undefined
45
+ if (value === null || value === undefined) continue;
46
+
47
+ // Create transform context
48
+ const transformContext: TransformContext = {
49
+ entityName: entityClass.name,
50
+ propertyName,
51
+ columnType: columnTypes[propertyName] || 'VARCHAR',
52
+ isUpdate: context.isUpdate || false,
53
+ originalValue: context.originalValue,
54
+ autoTransform: this.config.auto
55
+ };
56
+
57
+ try {
58
+ // Apply sanitizers first
59
+ let transformedValue = this.applySanitizers(value, typedMeta.sanitizers, transformContext);
60
+
61
+ // Apply transformers
62
+ transformedValue = this.applyTransformersToValue(transformedValue, typedMeta.transformers, transformContext);
63
+
64
+ // Apply validation
65
+ this.applyValidators(transformedValue, typedMeta.validators, transformContext);
66
+
67
+ // Update the entity with the transformed value
68
+ entity[propertyName] = transformedValue;
69
+ } catch (_error) {
70
+ // Handle errors based on config
71
+ if (this.config.stopOnFirstError) {
72
+ throw _error;
73
+ }
74
+ // Continue with other properties
75
+ }
76
+ }
77
+ }
78
+
79
+ private shouldExecute(executionOrder: 'before-save' | 'after-load' | 'both', isUpdate: boolean): boolean {
80
+ if (executionOrder === 'both') return true;
81
+ if (isUpdate && executionOrder === 'before-save') return true;
82
+ if (!isUpdate && executionOrder === 'after-load') return true;
83
+ return false;
84
+ }
85
+
86
+ private getColumnTypes(meta: EntityMetadata): Record<string, string> {
87
+ if (!meta || !meta.columns) return {};
88
+
89
+ const columnTypes: Record<string, string> = {};
90
+ for (const [propertyName, columnDef] of Object.entries(meta.columns)) {
91
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
+ columnTypes[propertyName] = (columnDef as any).type || 'VARCHAR';
93
+ }
94
+ return columnTypes;
95
+ }
96
+
97
+ private applySanitizers(value: unknown, sanitizers: PropertySanitizer[], context: TransformContext): unknown {
98
+ let result = value;
99
+ for (const sanitizer of sanitizers) {
100
+ try {
101
+ result = sanitizer.sanitize(result, context);
102
+ } catch (_error: unknown) {
103
+ // Continue with next sanitizer if this one fails
104
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
105
+ const error = _error;
106
+ }
107
+ }
108
+ return result;
109
+ }
110
+
111
+ private applyTransformersToValue(value: unknown, transformers: PropertyTransformer[], context: TransformContext): unknown {
112
+ let result = value;
113
+ for (const transformer of transformers) {
114
+ try {
115
+ result = transformer.transform(result, context);
116
+ } catch (_error: unknown) {
117
+ // Continue with next transformer if this one fails
118
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
119
+ const error = _error;
120
+ }
121
+ }
122
+ return result;
123
+ }
124
+
125
+ private applyValidators(value: unknown, validators: PropertyValidator[], context: TransformContext): void {
126
+ const errors: string[] = [];
127
+
128
+ for (const validator of validators) {
129
+ try {
130
+ const result = validator.validate(value, context);
131
+
132
+ if (!result.isValid) {
133
+ if (context.autoTransform && 'autoTransform' in validator) {
134
+ const autoResult = (validator as { autoTransform?: (v: unknown, c: TransformContext) => { success: boolean; correctedValue?: unknown; message?: string } }).autoTransform?.(value, context);
135
+ if (typeof autoResult === 'object' && autoResult !== null && 'success' in autoResult && autoResult.success && 'correctedValue' in autoResult && autoResult.correctedValue !== undefined) {
136
+ // Auto-correct the value
137
+ (context as { correctedValue?: unknown }).correctedValue = autoResult.correctedValue;
138
+ return; // Value is now valid
139
+ }
140
+ }
141
+ errors.push(result.error || `Validation failed for ${validator.name}`);
142
+
143
+ if (this.config.stopOnFirstError) {
144
+ break;
145
+ }
146
+ }
147
+ } catch (_error: unknown) {
148
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
149
+ const error = _error;
150
+ if (_error instanceof Error) {
151
+ errors.push(_error.message);
152
+ } else {
153
+ errors.push('Validation failed');
154
+ }
155
+ if (this.config.stopOnFirstError) {
156
+ break;
157
+ }
158
+ }
159
+ }
160
+
161
+ if (errors.length > 0) {
162
+ throw new Error(errors.join(', '));
163
+ }
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Creates a transformer executor with specified config
169
+ */
170
+ export const createTransformerExecutor = (config?: TransformerConfig): TransformerExecutor => {
171
+ return new TransformerExecutor(config);
172
+ };