Skip to content

Commit d3544f4

Browse files
committed
Totally refactor the validation layer
1 parent 649159e commit d3544f4

30 files changed

+290
-310
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "strontium",
3-
"version": "2.2.4",
3+
"version": "2.3.0",
44
"description": "Strontium is a TypeScript toolkit for High Performance API servers built for Production not Projects.",
55
"main": "lib/src/index.js",
66
"types": "lib/src/index.d.ts",

src/errors/StrontiumError.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
1-
export abstract class StrontiumError extends Error {}
1+
export abstract class StrontiumError {
2+
public stack?: string
3+
4+
constructor(public message?: string) {
5+
let error = new Error()
6+
7+
this.stack = error.stack
8+
}
9+
}

src/errors/http/HTTPError.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,4 @@ export abstract class HTTPError extends StrontiumError {
1717
errorMessage: this.externalMessage,
1818
}
1919
}
20-
21-
public static isHTTPError(e: any): e is HTTPError {
22-
return (
23-
e !== undefined &&
24-
typeof e.statusCode === "number" &&
25-
typeof e.toResponseBody === "function"
26-
)
27-
}
2820
}

src/http/drivers/FastifyServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export class FastifyServer implements Process {
189189
} catch (e) {
190190
// Detect Input Validation issues.
191191
// Any other errors will be thrown directly if they are HTTPError compatible or 500 and logged if not.
192-
if (HTTPError.isHTTPError(e)) {
192+
if (e instanceof HTTPError) {
193193
response.code(e.statusCode)
194194
return e.toResponseBody()
195195
} else {
Lines changed: 39 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { ValidationError } from "../../../errors/http/ValidationError"
22
import { ValidatorFunction } from "../../abstract/ValidatorFunction"
3-
import { compact } from "../../../utils/list"
43

54
export function either<I, O1, O2>(
65
V1: ValidatorFunction<I, O1>,
@@ -36,111 +35,50 @@ export function either<I, O1, O2, O3, O4, O5>(
3635
| ValidatorFunction<I, O1 | O2 | O3>
3736
| ValidatorFunction<I, O1 | O2 | O3 | O4>
3837
| ValidatorFunction<I, O1 | O2 | O3 | O4 | O5> {
39-
// This is split into if statements so Type completion is rigid. It's possible this could
40-
// be better written in the future but for now TypeScript is happy.
41-
if (V5 !== undefined && V4 !== undefined && V3 !== undefined) {
42-
return async (i: I) => {
43-
let outputs = await Promise.all([
44-
runAndWrap(i, V1),
45-
runAndWrap(i, V2),
46-
runAndWrap(i, V3),
47-
runAndWrap(i, V4),
48-
runAndWrap(i, V5),
49-
])
50-
let [o1, o2, o3, o4, o5] = outputs
51-
let eitherValue =
52-
o1.value || o2.value || o3.value || o4.value || o5.value
53-
if (eitherValue !== undefined) {
54-
return eitherValue
55-
} else {
56-
throw new ValidationError(
57-
"EITHER",
58-
buildEitherErrorMessage(outputs),
59-
"This value did not match any validators."
60-
)
61-
}
62-
}
63-
} else if (V4 !== undefined && V3 !== undefined) {
64-
return async (i: I) => {
65-
let outputs = await Promise.all([
66-
runAndWrap(i, V1),
67-
runAndWrap(i, V2),
68-
runAndWrap(i, V3),
69-
runAndWrap(i, V4),
70-
])
71-
let [o1, o2, o3, o4] = outputs
72-
let eitherValue = o1.value || o2.value || o3.value || o4.value
73-
if (eitherValue !== undefined) {
74-
return eitherValue
75-
} else {
76-
throw new ValidationError(
77-
"EITHER",
78-
buildEitherErrorMessage(outputs),
79-
"This value did not match any validators."
80-
)
81-
}
82-
}
83-
} else if (V3 !== undefined) {
84-
return async (i: I) => {
85-
let outputs = await Promise.all([
86-
runAndWrap(i, V1),
87-
runAndWrap(i, V2),
88-
runAndWrap(i, V3),
89-
])
90-
let [o1, o2, o3] = outputs
91-
let eitherValue = o1.value || o2.value || o3.value
92-
if (eitherValue !== undefined) {
93-
return eitherValue
94-
} else {
95-
throw new ValidationError(
96-
"EITHER",
97-
buildEitherErrorMessage(outputs),
98-
"This value did not match any validators."
99-
)
38+
return async (i: I) => {
39+
let errors: Array<unknown> = []
40+
41+
// Iterate over each validator in descending order until one succeeds.
42+
for (let validator of [V1, V2, V3, V4, V5]) {
43+
if (validator !== undefined) {
44+
try {
45+
return await validator(i)
46+
} catch (e) {
47+
errors.push(e)
48+
}
10049
}
10150
}
102-
} else {
103-
return async (i: I) => {
104-
let outputs = await Promise.all([
105-
runAndWrap(i, V1),
106-
runAndWrap(i, V2),
107-
])
108-
let [o1, o2] = outputs
109-
let eitherValue = o1.value || o2.value
110-
if (eitherValue !== undefined) {
111-
return eitherValue
51+
52+
// If we get to this stage then we have failed the validator - throw a Validation Error unless one of
53+
// the validators threw a different error type
54+
let failedConstraints: Array<string> = []
55+
let failedInternalMessages: Array<string> = []
56+
let failedExternalMessages: Array<string> = []
57+
58+
for (let error of errors) {
59+
if (error instanceof ValidationError) {
60+
failedConstraints.push(error.constraintName)
61+
62+
if (error.internalMessage) {
63+
failedInternalMessages.push(error.internalMessage)
64+
}
65+
66+
if (error.externalMessage) {
67+
failedExternalMessages.push(error.externalMessage)
68+
}
11269
} else {
113-
throw new ValidationError(
114-
"EITHER",
115-
buildEitherErrorMessage(outputs),
116-
"This value did not match any validators."
117-
)
70+
throw error
11871
}
11972
}
120-
}
121-
}
12273

123-
interface WrappedValidatorOutput<O> {
124-
value?: O
125-
err?: ValidationError
126-
}
127-
128-
// Used internally to catch validation errors and return undefined
129-
const runAndWrap = async <I, O>(
130-
input: I,
131-
validator: ValidatorFunction<I, O>
132-
): Promise<WrappedValidatorOutput<O>> => {
133-
try {
134-
return { value: await validator(input) }
135-
} catch (e) {
136-
return { err: e }
74+
throw new ValidationError(
75+
`EITHER(${failedConstraints.join(",")})`,
76+
`No compatible validators found: (${failedInternalMessages.join(
77+
", "
78+
)})`,
79+
`This value did not match any of the following validators: (${failedExternalMessages.join(
80+
" | "
81+
)})`
82+
)
13783
}
13884
}
139-
140-
const buildEitherErrorMessage = (
141-
outputs: WrappedValidatorOutput<any>[]
142-
): string => {
143-
return compact(outputs.map(({ err }) => err && err.internalMessage)).join(
144-
", "
145-
)
146-
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { either } from "./either"
2+
import { isUndefined } from "../validators/isUndefined"
3+
4+
import { ValidatorFunction } from "../.."
5+
6+
export const isOptional = <V extends ValidatorFunction<I, O>, I, O>(
7+
validator: V
8+
) => either(isUndefined, validator)

src/validation/drivers/sanitizers/normalizeEmail.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ import NormalizeEmailOptions = ValidatorJS.NormalizeEmailOptions
55
export const normalizeEmail = (options?: NormalizeEmailOptions) => <I>(
66
input: I
77
): string | undefined => {
8-
if (input === undefined) {
9-
return input
10-
}
11-
128
let normalizedEmail = Validator.normalizeEmail(String(input), options)
139
let isValid = Validator.isEmail(String(normalizedEmail))
1410

src/validation/drivers/validators/isBoolean.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { ValidationError } from "../../../errors/http/ValidationError"
22

3-
export const isBoolean = (input?: unknown): boolean | undefined => {
4-
if (input === undefined) {
5-
return undefined
6-
}
7-
3+
export const isBoolean = (input?: unknown): boolean => {
84
if (typeof input === "boolean") {
95
return input
106
}

src/validation/drivers/validators/isISOCountry.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import { ValidationError } from "../../../errors/http/ValidationError"
22
import * as Validator from "validator"
33

4-
export const isISOAlpha2CountryCode = (input?: unknown): string | undefined => {
5-
if (input === undefined) {
6-
return undefined
7-
}
8-
4+
export const isISOAlpha2CountryCode = (input: unknown): string => {
95
if (Validator.isISO31661Alpha2(String(input))) {
106
return String(input)
117
} else {

src/validation/drivers/validators/isISODate.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import { ValidationError } from "../../../errors/http/ValidationError"
22
import * as Validator from "validator"
33

4-
export const isISODate = (input?: unknown): Date | undefined => {
5-
if (input === undefined) {
6-
return undefined
7-
}
8-
4+
export const isISODate = (input: unknown): Date => {
95
if (Validator.isISO8601(String(input))) {
106
return new Date(String(input))
117
} else {

0 commit comments

Comments
 (0)