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.
@@ -0,0 +1,73 @@
1
+ import type { ColumnType } from '../../schema/column-types.js';
2
+
3
+ // Transform context provides metadata about the transformation
4
+ export interface TransformContext {
5
+ entityName: string;
6
+ propertyName: string;
7
+ columnType: ColumnType;
8
+ isUpdate: boolean;
9
+ originalValue?: unknown;
10
+ autoTransform: boolean; // Whether auto-correction is enabled
11
+ }
12
+
13
+ // Auto-transform result for validators that can fix data
14
+ export interface AutoTransformResult<T = unknown> {
15
+ success: boolean;
16
+ correctedValue?: T;
17
+ message?: string;
18
+ }
19
+
20
+ // Base transformer interface
21
+ export interface PropertyTransformer<TInput = unknown, TOutput = unknown> {
22
+ readonly name: string;
23
+ transform(value: TInput, context: TransformContext): TOutput;
24
+ }
25
+
26
+ // Validator interface (read-only, throws on failure)
27
+ export interface PropertyValidator<T = unknown> {
28
+ readonly name: string;
29
+ validate(value: T, context: TransformContext): ValidationResult;
30
+ }
31
+
32
+ // Sanitizer interface (mutates, never throws)
33
+ export interface PropertySanitizer<T = unknown> {
34
+ readonly name: string;
35
+ sanitize(value: T, context: TransformContext): T;
36
+ }
37
+
38
+ // Combined transformer that can validate and transform
39
+ export interface CompositeTransformer<TInput = unknown, TOutput = unknown>
40
+ extends PropertyTransformer<TInput, TOutput>,
41
+ PropertyValidator<TInput> {}
42
+
43
+ // Extended validator interface with auto-transform capability
44
+ export interface AutoTransformableValidator<T = unknown> extends PropertyValidator<T> {
45
+ /**
46
+ * Attempts to automatically correct an invalid value.
47
+ * Returns undefined if auto-correction is not possible.
48
+ */
49
+ autoTransform?(value: T, context: TransformContext): AutoTransformResult<T>;
50
+ }
51
+
52
+ // Validation result
53
+ export interface ValidationResult {
54
+ isValid: boolean;
55
+ error?: string;
56
+ message?: string;
57
+ }
58
+
59
+ // Transformer metadata stored during decoration phase
60
+ export interface TransformerMetadata {
61
+ propertyName: string;
62
+ transformers: PropertyTransformer[];
63
+ validators: PropertyValidator[];
64
+ sanitizers: PropertySanitizer[];
65
+ executionOrder: 'before-save' | 'after-load' | 'both';
66
+ }
67
+
68
+ // Transformer configuration options
69
+ export interface TransformerConfig {
70
+ auto?: boolean; // Enable auto-transform mode (default: false)
71
+ execute?: 'before-save' | 'after-load' | 'both'; // When to execute (default: 'both')
72
+ stopOnFirstError?: boolean; // Stop validation on first error (default: false)
73
+ }
@@ -0,0 +1,55 @@
1
+ import type { CountryValidator, ValidationOptions, ValidationResult, AutoCorrectionResult } from '../country-validators.js';
2
+
3
+ /**
4
+ * Brazilian CEP validator
5
+ * Format: XXXXX-XXX (8 digits)
6
+ */
7
+ export class CEPValidator implements CountryValidator<string> {
8
+ readonly countryCode = 'BR';
9
+ readonly identifierType = 'cep';
10
+ readonly name = 'br-cep';
11
+
12
+ validate(value: string, options: ValidationOptions = {}): ValidationResult {
13
+ const normalized = this.normalize(value);
14
+
15
+ // Validate format
16
+ if (!/^\d{8}$/.test(normalized)) {
17
+ return {
18
+ isValid: false,
19
+ error: options.errorMessage || 'CEP must contain exactly 8 numeric digits'
20
+ };
21
+ }
22
+
23
+ return {
24
+ isValid: true,
25
+ normalizedValue: normalized,
26
+ formattedValue: this.format(value)
27
+ };
28
+ }
29
+
30
+ normalize(value: string): string {
31
+ return value.replace(/[^0-9]/g, '');
32
+ }
33
+
34
+ format(value: string): string {
35
+ const normalized = this.normalize(value);
36
+ if (normalized.length !== 8) return value;
37
+ return normalized.replace(/(\d{5})(\d{3})/, '$1-$2');
38
+ }
39
+
40
+ autoCorrect(value: string): AutoCorrectionResult<string> {
41
+ const normalized = this.normalize(value);
42
+
43
+ if (normalized.length === 8) {
44
+ return { success: true, correctedValue: this.format(normalized) };
45
+ }
46
+
47
+ if (normalized.length < 8) {
48
+ const padded = normalized.padEnd(8, '0');
49
+ return { success: true, correctedValue: this.format(padded) };
50
+ }
51
+
52
+ const truncated = normalized.slice(0, 8);
53
+ return { success: true, correctedValue: this.format(truncated) };
54
+ }
55
+ }
@@ -0,0 +1,105 @@
1
+ import type { CountryValidator, ValidationOptions, ValidationResult, AutoCorrectionResult } from '../country-validators.js';
2
+
3
+ /**
4
+ * Brazilian CNPJ validator
5
+ * Format: XX.XXX.XXX/XXXX-XX (14 digits)
6
+ * Checksum: Mod 11 algorithm
7
+ */
8
+ export class CNPJValidator implements CountryValidator<string> {
9
+ readonly countryCode = 'BR';
10
+ readonly identifierType = 'cnpj';
11
+ readonly name = 'br-cnpj';
12
+
13
+ validate(value: string, options: ValidationOptions = {}): ValidationResult {
14
+ const normalized = this.normalize(value);
15
+
16
+ // Validate format
17
+ if (!/^\d{14}$/.test(normalized)) {
18
+ return {
19
+ isValid: false,
20
+ error: options.errorMessage || 'CNPJ must contain exactly 14 numeric digits'
21
+ };
22
+ }
23
+
24
+ // Check for known invalid CNPJs
25
+ if (this.isKnownInvalid(normalized) && options.strict !== false) {
26
+ return {
27
+ isValid: false,
28
+ error: options.errorMessage || 'Invalid CNPJ number'
29
+ };
30
+ }
31
+
32
+ // Validate checksum
33
+ if (!this.validateChecksum(normalized)) {
34
+ return {
35
+ isValid: false,
36
+ error: options.errorMessage || 'Invalid CNPJ checksum'
37
+ };
38
+ }
39
+
40
+ return {
41
+ isValid: true,
42
+ normalizedValue: normalized,
43
+ formattedValue: this.format(value)
44
+ };
45
+ }
46
+
47
+ normalize(value: string): string {
48
+ return value.replace(/[^0-9]/g, '');
49
+ }
50
+
51
+ format(value: string): string {
52
+ const normalized = this.normalize(value);
53
+ if (normalized.length !== 14) return value;
54
+ return normalized.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, '$1.$2.$3/$4-$5');
55
+ }
56
+
57
+ autoCorrect(value: string): AutoCorrectionResult<string> {
58
+ const normalized = this.normalize(value);
59
+
60
+ if (normalized.length === 14) {
61
+ return { success: true, correctedValue: this.format(normalized) };
62
+ }
63
+
64
+ if (normalized.length < 14) {
65
+ const padded = normalized.padEnd(14, '0');
66
+ return { success: true, correctedValue: this.format(padded) };
67
+ }
68
+
69
+ const truncated = normalized.slice(0, 14);
70
+ return { success: true, correctedValue: this.format(truncated) };
71
+ }
72
+
73
+ private isKnownInvalid(cnpj: string): boolean {
74
+ // Check for CNPJs with all digits the same (e.g., 00.000.000/0000-00)
75
+ return /^(\d)\1{13}$/.test(cnpj);
76
+ }
77
+
78
+ private validateChecksum(cnpj: string): boolean {
79
+ const digits = cnpj.split('').map(Number);
80
+ const weights1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
81
+ const weights2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
82
+
83
+ // Calculate first check digit
84
+ let sum = 0;
85
+ for (let i = 0; i < 12; i++) {
86
+ sum += digits[i] * weights1[i];
87
+ }
88
+ let check1 = sum % 11;
89
+ check1 = check1 < 2 ? 0 : 11 - check1;
90
+
91
+ if (check1 !== digits[12]) {
92
+ return false;
93
+ }
94
+
95
+ // Calculate second check digit
96
+ sum = 0;
97
+ for (let i = 0; i < 13; i++) {
98
+ sum += digits[i] * weights2[i];
99
+ }
100
+ let check2 = sum % 11;
101
+ check2 = check2 < 2 ? 0 : 11 - check2;
102
+
103
+ return check2 === digits[13];
104
+ }
105
+ }
@@ -0,0 +1,103 @@
1
+ import type { CountryValidator, ValidationOptions, ValidationResult, AutoCorrectionResult } from '../country-validators.js';
2
+
3
+ /**
4
+ * Brazilian CPF validator
5
+ * Format: XXX.XXX.XXX-XX (11 digits)
6
+ * Checksum: Mod 11 algorithm
7
+ */
8
+ export class CPFValidator implements CountryValidator<string> {
9
+ readonly countryCode = 'BR';
10
+ readonly identifierType = 'cpf';
11
+ readonly name = 'br-cpf';
12
+
13
+ validate(value: string, options: ValidationOptions = {}): ValidationResult {
14
+ const normalized = this.normalize(value);
15
+
16
+ // Validate format
17
+ if (!/^\d{11}$/.test(normalized)) {
18
+ return {
19
+ isValid: false,
20
+ error: options.errorMessage || 'CPF must contain exactly 11 numeric digits'
21
+ };
22
+ }
23
+
24
+ // Check for known invalid CPFs
25
+ if (this.isKnownInvalid(normalized) && options.strict !== false) {
26
+ return {
27
+ isValid: false,
28
+ error: options.errorMessage || 'Invalid CPF number'
29
+ };
30
+ }
31
+
32
+ // Validate checksum
33
+ if (!this.validateChecksum(normalized)) {
34
+ return {
35
+ isValid: false,
36
+ error: options.errorMessage || 'Invalid CPF checksum'
37
+ };
38
+ }
39
+
40
+ return {
41
+ isValid: true,
42
+ normalizedValue: normalized,
43
+ formattedValue: this.format(value)
44
+ };
45
+ }
46
+
47
+ normalize(value: string): string {
48
+ return value.replace(/[^0-9]/g, '');
49
+ }
50
+
51
+ format(value: string): string {
52
+ const normalized = this.normalize(value);
53
+ if (normalized.length !== 11) return value;
54
+ return normalized.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
55
+ }
56
+
57
+ autoCorrect(value: string): AutoCorrectionResult<string> {
58
+ const normalized = this.normalize(value);
59
+
60
+ if (normalized.length === 11) {
61
+ return { success: true, correctedValue: this.format(normalized) };
62
+ }
63
+
64
+ if (normalized.length < 11) {
65
+ const padded = normalized.padEnd(11, '0');
66
+ return { success: true, correctedValue: this.format(padded) };
67
+ }
68
+
69
+ const truncated = normalized.slice(0, 11);
70
+ return { success: true, correctedValue: this.format(truncated) };
71
+ }
72
+
73
+ private isKnownInvalid(cpf: string): boolean {
74
+ // Check for CPFs with all digits the same (e.g., 111.111.111-11)
75
+ return /^(\d)\1{10}$/.test(cpf);
76
+ }
77
+
78
+ private validateChecksum(cpf: string): boolean {
79
+ const digits = cpf.split('').map(Number);
80
+
81
+ // Calculate first check digit
82
+ let sum = 0;
83
+ for (let i = 0; i < 9; i++) {
84
+ sum += digits[i] * (10 - i);
85
+ }
86
+ let check1 = sum % 11;
87
+ check1 = check1 < 2 ? 0 : 11 - check1;
88
+
89
+ if (check1 !== digits[9]) {
90
+ return false;
91
+ }
92
+
93
+ // Calculate second check digit
94
+ sum = 0;
95
+ for (let i = 0; i < 10; i++) {
96
+ sum += digits[i] * (11 - i);
97
+ }
98
+ let check2 = sum % 11;
99
+ check2 = check2 < 2 ? 0 : 11 - check2;
100
+
101
+ return check2 === digits[10];
102
+ }
103
+ }
@@ -0,0 +1,64 @@
1
+ import type { CountryValidator, CountryValidatorFactory } from './country-validators.js';
2
+
3
+ /**
4
+ * Registry for country-specific identifier validators
5
+ * Mirrors the INFLECTOR_FACTORIES pattern from scripts/inflection/index.mjs
6
+ */
7
+ const VALIDATOR_FACTORIES = new Map<string, CountryValidatorFactory>();
8
+
9
+ /**
10
+ * Registers a country validator factory
11
+ * @param countryCode - ISO 3166-1 alpha-2 country code (e.g., 'BR', 'US')
12
+ * @param identifierType - Identifier type (e.g., 'cpf', 'ssn')
13
+ * @param factory - Factory function that creates a validator instance
14
+ */
15
+ export const registerValidator = (
16
+ countryCode: string,
17
+ identifierType: string,
18
+ factory: CountryValidatorFactory
19
+ ): void => {
20
+ const key = `${countryCode.toLowerCase()}-${identifierType.toLowerCase()}`;
21
+ if (!countryCode) throw new Error('countryCode is required');
22
+ if (!identifierType) throw new Error('identifierType is required');
23
+ if (typeof factory !== 'function') {
24
+ throw new Error('factory must be a function that returns a validator');
25
+ }
26
+ VALIDATOR_FACTORIES.set(key, factory);
27
+ };
28
+
29
+ /**
30
+ * Resolves a validator for a given country and identifier type
31
+ * @param countryCode - ISO 3166-1 alpha-2 country code
32
+ * @param identifierType - Identifier type
33
+ * @returns Validator instance or undefined if not found
34
+ */
35
+ export const resolveValidator = (
36
+ countryCode: string,
37
+ identifierType: string
38
+ ): CountryValidator | undefined => {
39
+ const key = `${countryCode.toLowerCase()}-${identifierType.toLowerCase()}`;
40
+ const factory = VALIDATOR_FACTORIES.get(key);
41
+ return factory ? factory() : undefined;
42
+ };
43
+
44
+ /**
45
+ * Gets all registered validator keys
46
+ * @returns Array of validator keys (e.g., ['br-cpf', 'us-ssn'])
47
+ */
48
+ export const getRegisteredValidators = (): string[] => {
49
+ return Array.from(VALIDATOR_FACTORIES.keys());
50
+ };
51
+
52
+ /**
53
+ * Checks if a validator is registered
54
+ * @param countryCode - ISO 3166-1 alpha-2 country code
55
+ * @param identifierType - Identifier type
56
+ * @returns True if validator is registered
57
+ */
58
+ export const hasValidator = (
59
+ countryCode: string,
60
+ identifierType: string
61
+ ): boolean => {
62
+ const key = `${countryCode.toLowerCase()}-${identifierType.toLowerCase()}`;
63
+ return VALIDATOR_FACTORIES.has(key);
64
+ };
@@ -0,0 +1,209 @@
1
+ import { getOrCreateMetadataBag } from '../decorator-metadata.js';
2
+ import { registerValidator } from './country-validator-registry.js';
3
+ import { CPFValidator } from './built-in/br-cpf-validator.js';
4
+ import { CNPJValidator } from './built-in/br-cnpj-validator.js';
5
+ import { CEPValidator } from './built-in/br-cep-validator.js';
6
+
7
+ // Register built-in validators
8
+ registerValidator('BR', 'cpf', () => new CPFValidator());
9
+ registerValidator('BR', 'cnpj', () => new CNPJValidator());
10
+ registerValidator('BR', 'cep', () => new CEPValidator());
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
+ /**
20
+ * Decorator to validate a Brazilian CPF number
21
+ * @param options - Validation options
22
+ * @returns Property decorator for CPF validation
23
+ */
24
+ export function CPF(options?: { strict?: boolean; errorMessage?: string }) {
25
+ return function (_value: unknown, context: ClassFieldDecoratorContext) {
26
+ const propertyName = normalizePropertyName(context.name);
27
+ const bag = getOrCreateMetadataBag(context);
28
+
29
+ // Find or create transformer metadata for this property
30
+ let existing = bag.transformers.find(t => t.propertyName === propertyName);
31
+ if (!existing) {
32
+ existing = {
33
+ propertyName,
34
+ metadata: {
35
+ propertyName,
36
+ transformers: [],
37
+ validators: [],
38
+ sanitizers: [],
39
+ executionOrder: 'both'
40
+ }
41
+ };
42
+ bag.transformers.push(existing);
43
+ }
44
+
45
+ // Create validator instance
46
+ const validator = new CPFValidator();
47
+
48
+ // Add validator to metadata
49
+ existing.metadata.validators.push({
50
+ name: validator.name,
51
+ validate: (value: string) => {
52
+ const result = validator.validate(value, {
53
+ strict: options?.strict ?? true,
54
+ errorMessage: options?.errorMessage
55
+ });
56
+ return {
57
+ isValid: result.isValid,
58
+ error: result.error,
59
+ message: result.error
60
+ };
61
+ },
62
+ autoTransform: (value: string) => {
63
+ const correction = validator.autoCorrect(value);
64
+ if (correction?.success) {
65
+ return {
66
+ success: true,
67
+ correctedValue: correction.correctedValue,
68
+ message: correction.message
69
+ };
70
+ }
71
+ return { success: false };
72
+ }
73
+ } as unknown as never);
74
+
75
+ // Add sanitizer to normalize and format
76
+ existing.metadata.sanitizers.push({
77
+ name: 'cpf-formatter',
78
+ sanitize: (value: string) => validator.format(value)
79
+ });
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Decorator to validate a Brazilian CNPJ number
85
+ * @param options - Validation options
86
+ * @returns Property decorator for CNPJ validation
87
+ */
88
+ export function CNPJ(options?: { strict?: boolean; errorMessage?: string }) {
89
+ return function (_value: unknown, context: ClassFieldDecoratorContext) {
90
+ const propertyName = normalizePropertyName(context.name);
91
+ const bag = getOrCreateMetadataBag(context);
92
+
93
+ // Find or create transformer metadata for this property
94
+ let existing = bag.transformers.find(t => t.propertyName === propertyName);
95
+ if (!existing) {
96
+ existing = {
97
+ propertyName,
98
+ metadata: {
99
+ propertyName,
100
+ transformers: [],
101
+ validators: [],
102
+ sanitizers: [],
103
+ executionOrder: 'both'
104
+ }
105
+ };
106
+ bag.transformers.push(existing);
107
+ }
108
+
109
+ // Create validator instance
110
+ const validator = new CNPJValidator();
111
+
112
+ // Add validator to metadata
113
+ existing.metadata.validators.push({
114
+ name: validator.name,
115
+ validate: (value: string) => {
116
+ const result = validator.validate(value, {
117
+ strict: options?.strict ?? true,
118
+ errorMessage: options?.errorMessage
119
+ });
120
+ return {
121
+ isValid: result.isValid,
122
+ error: result.error,
123
+ message: result.error
124
+ };
125
+ },
126
+ autoTransform: (value: string) => {
127
+ const correction = validator.autoCorrect(value);
128
+ if (correction?.success) {
129
+ return {
130
+ success: true,
131
+ correctedValue: correction.correctedValue,
132
+ message: correction.message
133
+ };
134
+ }
135
+ return { success: false };
136
+ }
137
+ } as unknown as never);
138
+
139
+ // Add sanitizer to normalize and format
140
+ existing.metadata.sanitizers.push({
141
+ name: 'cnpj-formatter',
142
+ sanitize: (value: string) => validator.format(value)
143
+ });
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Decorator to validate a Brazilian CEP number
149
+ * @param options - Validation options
150
+ * @returns Property decorator for CEP validation
151
+ */
152
+ export function CEP(options?: { strict?: boolean; errorMessage?: string }) {
153
+ return function (_value: unknown, context: ClassFieldDecoratorContext) {
154
+ const propertyName = normalizePropertyName(context.name);
155
+ const bag = getOrCreateMetadataBag(context);
156
+
157
+ // Find or create transformer metadata for this property
158
+ let existing = bag.transformers.find(t => t.propertyName === propertyName);
159
+ if (!existing) {
160
+ existing = {
161
+ propertyName,
162
+ metadata: {
163
+ propertyName,
164
+ transformers: [],
165
+ validators: [],
166
+ sanitizers: [],
167
+ executionOrder: 'both'
168
+ }
169
+ };
170
+ bag.transformers.push(existing);
171
+ }
172
+
173
+ // Create validator instance
174
+ const validator = new CEPValidator();
175
+
176
+ // Add validator to metadata
177
+ existing.metadata.validators.push({
178
+ name: validator.name,
179
+ validate: (value: string) => {
180
+ const result = validator.validate(value, {
181
+ strict: options?.strict ?? true,
182
+ errorMessage: options?.errorMessage
183
+ });
184
+ return {
185
+ isValid: result.isValid,
186
+ error: result.error,
187
+ message: result.error
188
+ };
189
+ },
190
+ autoTransform: (value: string) => {
191
+ const correction = validator.autoCorrect(value);
192
+ if (correction?.success) {
193
+ return {
194
+ success: true,
195
+ correctedValue: correction.correctedValue,
196
+ message: correction.message
197
+ };
198
+ }
199
+ return { success: false };
200
+ }
201
+ } as unknown as never);
202
+
203
+ // Add sanitizer to normalize and format
204
+ existing.metadata.sanitizers.push({
205
+ name: 'cep-formatter',
206
+ sanitize: (value: string) => validator.format(value)
207
+ });
208
+ };
209
+ }