@nextrush/errors 3.0.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.
- package/LICENSE +21 -0
- package/README.md +681 -0
- package/dist/index.d.ts +583 -0
- package/dist/index.js +1021 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
- package/src/__tests__/base.test.ts +180 -0
- package/src/__tests__/factory.test.ts +312 -0
- package/src/__tests__/http-errors.test.ts +458 -0
- package/src/__tests__/middleware.test.ts +385 -0
- package/src/__tests__/validation.test.ts +315 -0
- package/src/base.ts +162 -0
- package/src/factory.ts +205 -0
- package/src/http-errors.ts +409 -0
- package/src/index.ts +101 -0
- package/src/middleware.ts +183 -0
- package/src/validation.ts +210 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nextrush/errors - Validation Error Classes
|
|
3
|
+
*
|
|
4
|
+
* Specialized errors for input validation.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { NextRushError } from './base';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Single validation issue
|
|
13
|
+
*/
|
|
14
|
+
export interface ValidationIssue {
|
|
15
|
+
/** Field path (e.g., 'user.email' or 'items[0].name') */
|
|
16
|
+
path: string;
|
|
17
|
+
/** Error message for this field */
|
|
18
|
+
message: string;
|
|
19
|
+
/** Validation rule that failed */
|
|
20
|
+
rule?: string;
|
|
21
|
+
/** Expected value or constraint */
|
|
22
|
+
expected?: unknown;
|
|
23
|
+
/** Actual value received */
|
|
24
|
+
received?: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validation error with multiple issues
|
|
29
|
+
*/
|
|
30
|
+
export class ValidationError extends NextRushError {
|
|
31
|
+
/** List of validation issues */
|
|
32
|
+
readonly issues: ValidationIssue[];
|
|
33
|
+
|
|
34
|
+
constructor(issues: ValidationIssue[], message = 'Validation failed') {
|
|
35
|
+
super(message, {
|
|
36
|
+
status: 400,
|
|
37
|
+
code: 'VALIDATION_ERROR',
|
|
38
|
+
expose: true,
|
|
39
|
+
});
|
|
40
|
+
this.issues = issues;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create from a single field error
|
|
45
|
+
*/
|
|
46
|
+
static fromField(path: string, message: string, rule?: string): ValidationError {
|
|
47
|
+
return new ValidationError([{ path, message, rule }]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create from multiple field errors
|
|
52
|
+
*/
|
|
53
|
+
static fromFields(errors: Record<string, string>): ValidationError {
|
|
54
|
+
const issues = Object.entries(errors).map(([path, message]) => ({
|
|
55
|
+
path,
|
|
56
|
+
message,
|
|
57
|
+
}));
|
|
58
|
+
return new ValidationError(issues);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if a specific field has errors
|
|
63
|
+
*/
|
|
64
|
+
hasErrorFor(path: string): boolean {
|
|
65
|
+
return this.issues.some((issue) => issue.path === path);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get errors for a specific field
|
|
70
|
+
*/
|
|
71
|
+
getErrorsFor(path: string): ValidationIssue[] {
|
|
72
|
+
return this.issues.filter((issue) => issue.path === path);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get first error message for a field
|
|
77
|
+
*/
|
|
78
|
+
getFirstError(path: string): string | undefined {
|
|
79
|
+
return this.issues.find((issue) => issue.path === path)?.message;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Convert to flat error object
|
|
84
|
+
*/
|
|
85
|
+
toFlatObject(): Record<string, string> {
|
|
86
|
+
const result: Record<string, string> = {};
|
|
87
|
+
for (const issue of this.issues) {
|
|
88
|
+
if (!result[issue.path]) {
|
|
89
|
+
result[issue.path] = issue.message;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override toJSON(): Record<string, unknown> {
|
|
96
|
+
return {
|
|
97
|
+
error: this.name,
|
|
98
|
+
message: this.message,
|
|
99
|
+
code: this.code,
|
|
100
|
+
status: this.status,
|
|
101
|
+
// Strip `received` to prevent leaking sensitive input values (passwords, tokens)
|
|
102
|
+
issues: this.issues.map(({ path, message, rule, expected }) => ({
|
|
103
|
+
path,
|
|
104
|
+
message,
|
|
105
|
+
...(rule !== undefined && { rule }),
|
|
106
|
+
...(expected !== undefined && { expected }),
|
|
107
|
+
})),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Required field missing
|
|
114
|
+
*/
|
|
115
|
+
export class RequiredFieldError extends ValidationError {
|
|
116
|
+
constructor(field: string) {
|
|
117
|
+
super(
|
|
118
|
+
[{ path: field, message: `${field} is required`, rule: 'required' }],
|
|
119
|
+
`${field} is required`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Field type mismatch
|
|
126
|
+
*/
|
|
127
|
+
export class TypeMismatchError extends ValidationError {
|
|
128
|
+
constructor(field: string, expected: string, received: string) {
|
|
129
|
+
super(
|
|
130
|
+
[
|
|
131
|
+
{
|
|
132
|
+
path: field,
|
|
133
|
+
message: `Expected ${expected}, received ${received}`,
|
|
134
|
+
rule: 'type',
|
|
135
|
+
expected,
|
|
136
|
+
received,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
`${field} must be of type ${expected}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Value out of range
|
|
146
|
+
*/
|
|
147
|
+
export class RangeValidationError extends ValidationError {
|
|
148
|
+
constructor(field: string, min?: number, max?: number) {
|
|
149
|
+
const parts: string[] = [];
|
|
150
|
+
if (min !== undefined) parts.push(`at least ${min}`);
|
|
151
|
+
if (max !== undefined) parts.push(`at most ${max}`);
|
|
152
|
+
const message = `${field} must be ${parts.join(' and ')}`;
|
|
153
|
+
|
|
154
|
+
super([{ path: field, message, rule: 'range', expected: { min, max } }], message);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* String length violation
|
|
160
|
+
*/
|
|
161
|
+
export class LengthError extends ValidationError {
|
|
162
|
+
constructor(field: string, min?: number, max?: number) {
|
|
163
|
+
const parts: string[] = [];
|
|
164
|
+
if (min !== undefined) parts.push(`at least ${min} characters`);
|
|
165
|
+
if (max !== undefined) parts.push(`at most ${max} characters`);
|
|
166
|
+
const message = `${field} must be ${parts.join(' and ')}`;
|
|
167
|
+
|
|
168
|
+
super([{ path: field, message, rule: 'length', expected: { min, max } }], message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Pattern mismatch
|
|
174
|
+
*/
|
|
175
|
+
export class PatternError extends ValidationError {
|
|
176
|
+
constructor(field: string, pattern: string, message?: string) {
|
|
177
|
+
super(
|
|
178
|
+
[
|
|
179
|
+
{
|
|
180
|
+
path: field,
|
|
181
|
+
message: message ?? `${field} does not match required pattern`,
|
|
182
|
+
rule: 'pattern',
|
|
183
|
+
expected: pattern,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
message ?? `${field} does not match required pattern`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Invalid email format
|
|
193
|
+
*/
|
|
194
|
+
export class InvalidEmailError extends ValidationError {
|
|
195
|
+
constructor(field = 'email') {
|
|
196
|
+
super(
|
|
197
|
+
[{ path: field, message: 'Invalid email address', rule: 'email' }],
|
|
198
|
+
'Invalid email address'
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Invalid URL format
|
|
205
|
+
*/
|
|
206
|
+
export class InvalidUrlError extends ValidationError {
|
|
207
|
+
constructor(field = 'url') {
|
|
208
|
+
super([{ path: field, message: 'Invalid URL', rule: 'url' }], 'Invalid URL');
|
|
209
|
+
}
|
|
210
|
+
}
|