Skip to content

Commit ab11304

Browse files
committed
feat: Validate errors as array
1 parent d3d8bfa commit ab11304

File tree

3 files changed

+145
-115
lines changed

3 files changed

+145
-115
lines changed

README.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,32 @@ Convert array of ValidationError objects from class-validator to multiline strin
44

55
## Usage
66

7+
#### As string
8+
9+
```ts
10+
import { validationErrorsAsString } from 'class-validator-flat-formatter';
11+
12+
const errors = await validate(user);
13+
14+
const message = validationErrorsAsString(errors);
15+
/**
16+
message(String) =>
17+
name: minLength error message (minLength),\n
18+
email: email must be an email (isEmail).
19+
*/
20+
```
21+
22+
#### As array
23+
724
```ts
8-
import { classValidatorFlatFormatter } from 'class-validator-flat-formatter';
25+
import { validationErrorsAsArray } from 'class-validator-flat-formatter';
926

10-
const message = classValidatorFlatFormatter(errors as ValidationError[]);
11-
// message =>
12-
name: minLength error message (minLength),
13-
name: name should not be empty (isNotEmpty).
27+
const errors = await validate(user);
28+
const messages = validationErrorsAsArray(errors);
29+
/**
30+
messages => Array<string> {
31+
'name: minLength error message (minLength)',
32+
'email: email must be an email (isEmail)'
33+
}
34+
*/
1435
```

src/index.spec.ts

Lines changed: 78 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,92 @@
1-
import { IsNotEmpty, Length, Min, validate } from 'class-validator';
1+
import {
2+
IsEmail,
3+
IsNotEmpty,
4+
Length,
5+
Min,
6+
MinLength,
7+
validate,
8+
ValidateNested,
9+
} from 'class-validator';
210
import { stripIndents } from 'common-tags';
311
import expect from 'expect';
412

5-
import { classValidatorFlatFormatter } from '.';
13+
import { validationErrorsAsArray, validationErrorsAsString } from '.';
614
import { ValidationError } from './validation-error';
715

8-
describe('classValidatorFlatFormatter', () => {
9-
it('built in to string', async () => {
10-
class User {
11-
@Length(3, 30) name!: string;
12-
@IsNotEmpty() @Min(18) age!: number;
13-
}
14-
const user = new User();
15-
const result = await validate(user);
16-
expect(result).toBeTruthy();
17-
});
16+
it('built in to string', async () => {
17+
class User {
18+
@Length(3, 30) name!: string;
19+
@IsNotEmpty() @Min(18) age!: number;
20+
}
21+
const user = new User();
22+
const errors = await validate(user);
23+
expect(errors).toBeTruthy();
24+
});
1825

19-
it('single error object', () => {
20-
const error: ValidationError = {
21-
value: 'aa@mai1l1',
22-
property: 'email',
23-
children: [],
24-
constraints: {
25-
isEmail: 'email must be an email',
26-
} as Record<string, string>,
27-
};
28-
expect(classValidatorFlatFormatter([error])).toEqual(stripIndents`
29-
email: email must be an email (isEmail).
30-
`);
31-
});
26+
it('single error object', async () => {
27+
class User {
28+
@IsEmail() email = '';
29+
}
30+
31+
const user = new User();
32+
const errors = await validate(user);
33+
34+
expect(validationErrorsAsString(errors)).toEqual(
35+
'email: email must be an email (isEmail).',
36+
);
37+
});
38+
39+
it('fault tollerance', () => {
40+
const errors: Partial<ValidationError>[] = [{}];
41+
expect(validationErrorsAsString(errors as ValidationError[])).toEqual('');
42+
expect(validationErrorsAsString(undefined as any)).toEqual('');
43+
expect(validationErrorsAsString(null as any)).toEqual('');
44+
});
45+
46+
it('several errors with single constraint', async () => {
47+
class User {
48+
@MinLength(3) name = '';
49+
@IsNotEmpty() password = '';
50+
}
3251

33-
it('several errors with single constraint', () => {
34-
const errors: Partial<ValidationError>[] = [
35-
{
36-
value: '',
37-
property: 'name',
38-
children: [],
39-
constraints: {
40-
minLength: 'name must be longer than or equal to 3 characters',
41-
},
42-
},
43-
{
44-
value: '',
45-
property: 'password',
46-
children: [],
47-
constraints: {
48-
isNotEmpty: 'password should not be empty',
49-
},
50-
},
51-
];
52-
expect(classValidatorFlatFormatter(errors as ValidationError[]))
53-
.toEqual(stripIndents`
52+
const user = new User();
53+
const errors = await validate(user);
54+
55+
expect(validationErrorsAsString(errors)).toEqual(stripIndents`
5456
name: name must be longer than or equal to 3 characters (minLength),
5557
password: password should not be empty (isNotEmpty).
5658
`);
57-
});
59+
});
5860

59-
it('single error with multiple constraints', () => {
60-
const errors: Partial<ValidationError>[] = [
61-
{
62-
value: '',
63-
property: 'name',
64-
children: [],
65-
constraints: {
66-
minLength: 'minLength error message',
67-
isNotEmpty: 'name should not be empty',
68-
},
69-
},
70-
];
71-
expect(classValidatorFlatFormatter(errors as ValidationError[]))
72-
.toEqual(stripIndents`
73-
name: minLength error message (minLength),
74-
name: name should not be empty (isNotEmpty).
75-
`);
76-
});
61+
it('validationErrorsAsArray', async () => {
62+
class EmailObject {
63+
@IsEmail()
64+
value = '';
65+
}
66+
class User {
67+
@Length(3, 30) name = '';
68+
@Min(18) age = 0;
69+
@ValidateNested() email = new EmailObject();
70+
}
71+
const user = new User();
72+
const errors = await validate(user);
7773

78-
it('single with nested', () => {
79-
const errors: Partial<ValidationError>[] = [
80-
{
81-
property: 'user',
82-
children: [
83-
({
84-
property: 'name',
85-
constraints: { alnum: 'alnum error' },
86-
} as unknown) as ValidationError,
87-
],
88-
constraints: {
89-
isNotEmpty: 'should not be empty',
90-
},
91-
},
92-
];
93-
expect(classValidatorFlatFormatter(errors as ValidationError[]))
94-
.toEqual(stripIndents`
95-
user: should not be empty (isNotEmpty),
96-
user.name: alnum error (alnum).
97-
`);
98-
});
74+
const result = validationErrorsAsArray(errors);
75+
76+
expect(result).toBeInstanceOf(Array);
77+
expect(result).toHaveLength(3);
78+
expect(result).toContainEqual('email.value: value must be an email (isEmail)');
79+
});
80+
81+
it('coerce to array', async () => {
82+
class User {
83+
@IsEmail() email = '';
84+
}
85+
86+
const user = new User();
87+
const errors = await validate(user);
9988

100-
it('fault tollerance', () => {
101-
const errors: Partial<ValidationError>[] = [{}];
102-
expect(classValidatorFlatFormatter(errors as ValidationError[])).toEqual('');
103-
expect(classValidatorFlatFormatter(undefined as any)).toEqual('');
104-
expect(classValidatorFlatFormatter(null as any)).toEqual('');
105-
});
89+
expect(validationErrorsAsArray(errors[0] as ValidationError)).toEqual([
90+
'email: email must be an email (isEmail)',
91+
]);
10692
});

src/index.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ValidationError } from './validation-error';
44
* Slightly refactored ValidationError.toString()
55
* https://github.com/typestack/class-validator/blob/master/src/validation/ValidationError.ts
66
*/
7-
export function classValidatorFlatFormatter(
7+
export function validationErrorsAsString(
88
errors: ValidationError[] | ValidationError,
99
parentPath = '',
1010
): string {
@@ -13,30 +13,53 @@ export function classValidatorFlatFormatter(
1313
}
1414
let result = '';
1515
if (errors.length > 0) {
16-
result = errors.map(err => formatError(err, parentPath)).join(',\n');
16+
result = errors.flatMap(err => formatError(err, parentPath)).join(',\n');
1717
if (result) {
1818
result += '.';
1919
}
2020
}
2121
return result;
2222
}
2323

24-
function formatError(error: ValidationError, parentPath: string) {
25-
if (!isValidationError(error) || !error.constraints) {
26-
return '';
24+
export function validationErrorsAsArray(
25+
errors: ValidationError[] | ValidationError,
26+
parentPath = '',
27+
) {
28+
if (!Array.isArray(errors)) {
29+
errors = [errors];
30+
}
31+
const result: string[] = errors.flatMap(err => formatError(err, parentPath));
32+
33+
return result;
34+
}
35+
36+
function formatError(error: ValidationError, parentPath: string): string[] {
37+
if (!isValidationError(error)) {
38+
return [];
39+
}
40+
41+
const constraints = Object.entries(error.constraints || []);
42+
43+
const result = constraints.flatMap(([constraintName, constraintMessage]) => {
44+
const property = propertyPath(parentPath, error.property);
45+
const errors = [`${property}: ${constraintMessage} (${constraintName})`];
46+
if (error.children?.length) {
47+
const childErrors = error.children.flatMap(err =>
48+
formatError(err, property),
49+
);
50+
errors.push(...childErrors);
51+
}
52+
return errors;
53+
});
54+
55+
if (constraints.length === 0 && error.children) {
56+
const childErrors = error.children.flatMap(err =>
57+
formatError(err, error.property),
58+
);
59+
result.push(...childErrors);
2760
}
28-
return Object.entries(error.constraints)
29-
.map(([constraintName, constraintMessage]) => {
30-
const property = propertyPath(parentPath, error.property);
31-
let result = `${property}: ${constraintMessage} (${constraintName})`;
32-
if (error.children && error.children.length > 0) {
33-
result += `,\n${error.children
34-
.map(err => formatError(err, property))
35-
.join(',\n')}`;
36-
}
37-
return result;
38-
})
39-
.join(',\n');
61+
62+
return result;
4063
}
4164

4265
function propertyPath(parent: string, name: string) {
@@ -53,5 +76,5 @@ function propertyPath(parent: string, name: string) {
5376
export function isValidationError(
5477
error?: Partial<ValidationError>,
5578
): error is ValidationError {
56-
return Boolean(error && error.constraints && error.property);
79+
return Boolean(error?.property && (error.constraints || error.children));
5780
}

0 commit comments

Comments
 (0)