metal-orm 1.0.92 → 1.0.94
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.cjs +728 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +225 -1
- package/dist/index.d.ts +225 -1
- package/dist/index.js +712 -23
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/decorators/bootstrap.ts +11 -1
- package/src/decorators/decorator-metadata.ts +3 -1
- package/src/decorators/index.ts +27 -0
- package/src/decorators/transformers/built-in/string-transformers.ts +231 -0
- package/src/decorators/transformers/transformer-decorators.ts +137 -0
- package/src/decorators/transformers/transformer-executor.ts +172 -0
- package/src/decorators/transformers/transformer-metadata.ts +73 -0
- package/src/decorators/validators/built-in/br-cep-validator.ts +55 -0
- package/src/decorators/validators/built-in/br-cnpj-validator.ts +105 -0
- package/src/decorators/validators/built-in/br-cpf-validator.ts +103 -0
- package/src/decorators/validators/country-validator-registry.ts +64 -0
- package/src/decorators/validators/country-validators-decorators.ts +209 -0
- package/src/decorators/validators/country-validators.ts +105 -0
- package/src/decorators/validators/validator-metadata.ts +59 -0
- package/src/dto/openapi/utilities.ts +5 -0
- package/src/orm/entity-metadata.ts +64 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metal-orm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.94",
|
|
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"
|
|
@@ -59,9 +60,10 @@
|
|
|
59
60
|
}
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
63
|
+
"@electric-sql/pglite": "^0.3.14",
|
|
62
64
|
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
|
63
65
|
"@typescript-eslint/parser": "^8.20.0",
|
|
64
|
-
"@vitest/ui": "^4.0.
|
|
66
|
+
"@vitest/ui": "^4.0.18",
|
|
65
67
|
"eslint": "^8.57.0",
|
|
66
68
|
"eslint-plugin-deprecation": "^3.0.0",
|
|
67
69
|
"express": "^5.2.1",
|
|
@@ -72,10 +74,8 @@
|
|
|
72
74
|
"supertest": "^7.2.2",
|
|
73
75
|
"tedious": "^19.1.3",
|
|
74
76
|
"tsup": "^8.0.0",
|
|
77
|
+
"tsx": "^4.21.0",
|
|
75
78
|
"typescript": "^5.5.0",
|
|
76
|
-
"vitest": "^4.0.
|
|
77
|
-
},
|
|
78
|
-
"dependencies": {
|
|
79
|
-
"@electric-sql/pglite": "^0.3.14"
|
|
79
|
+
"vitest": "^4.0.18"
|
|
80
80
|
}
|
|
81
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;
|
package/src/decorators/index.ts
CHANGED
|
@@ -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
|
+
};
|