2022-09-20 03:11:34 +02:00
|
|
|
import { JSONPath } from '../../utils/json.ts'
|
2022-09-30 14:56:39 +02:00
|
|
|
import type { JSONObject, JSONPrimitive, JSONArray } from '../../utils/json.ts'
|
2022-09-20 03:11:34 +02:00
|
|
|
import { rule } from './rule.ts'
|
|
|
|
import { sanitizer } from './sanitizer.ts'
|
|
|
|
|
|
|
|
type Target = 'query' | 'header' | 'body' | 'json'
|
2022-09-30 14:56:39 +02:00
|
|
|
type Type = JSONPrimitive | JSONObject | JSONArray | File
|
2022-09-20 03:11:34 +02:00
|
|
|
type Rule = (value: Type) => boolean
|
|
|
|
type Sanitizer = (value: Type) => Type
|
|
|
|
|
|
|
|
export class Validator {
|
|
|
|
query = (key: string): VString => new VString({ target: 'query', key: key })
|
|
|
|
header = (key: string): VString => new VString({ target: 'header', key: key })
|
|
|
|
body = (key: string): VString => new VString({ target: 'body', key: key })
|
|
|
|
json = (key: string): VString => new VString({ target: 'json', key: key })
|
2022-09-13 00:54:06 +02:00
|
|
|
}
|
|
|
|
|
2022-09-20 03:11:34 +02:00
|
|
|
export type ValidateResult = {
|
|
|
|
isValid: boolean
|
|
|
|
message: string | undefined
|
|
|
|
target: Target
|
|
|
|
key: string
|
|
|
|
value: Type
|
|
|
|
}
|
2022-09-13 00:54:06 +02:00
|
|
|
|
2022-09-20 03:11:34 +02:00
|
|
|
type VOptions = {
|
|
|
|
target: Target
|
|
|
|
key: string
|
|
|
|
type?: 'string' | 'number' | 'boolean' | 'object'
|
2022-09-30 14:56:39 +02:00
|
|
|
isArray?: boolean
|
2022-09-20 03:11:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export abstract class VBase {
|
|
|
|
type: 'string' | 'number' | 'boolean' | 'object'
|
|
|
|
target: Target
|
|
|
|
key: string
|
|
|
|
rules: Rule[]
|
|
|
|
sanitizers: Sanitizer[]
|
2022-09-30 14:56:39 +02:00
|
|
|
isArray: boolean
|
2022-09-20 03:11:34 +02:00
|
|
|
private _message: string | undefined
|
2022-09-27 11:19:50 +02:00
|
|
|
private _optional: boolean
|
2022-09-20 03:11:34 +02:00
|
|
|
constructor(options: VOptions) {
|
|
|
|
this.target = options.target
|
|
|
|
this.key = options.key
|
|
|
|
this.type = options.type || 'string'
|
|
|
|
this.rules = []
|
|
|
|
this.sanitizers = []
|
2022-09-30 14:56:39 +02:00
|
|
|
this.isArray = options.isArray || false
|
2022-09-27 11:19:50 +02:00
|
|
|
this._optional = false
|
2022-09-20 03:11:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
addRule = (rule: Rule) => {
|
|
|
|
this.rules.push(rule)
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
addSanitizer = (sanitizer: Sanitizer) => {
|
|
|
|
this.sanitizers.push(sanitizer)
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
isRequired = () => {
|
|
|
|
return this.addRule((value: unknown) => {
|
2022-09-30 14:56:39 +02:00
|
|
|
if (value !== undefined && value !== null && value !== '') return true
|
2022-09-20 03:11:34 +02:00
|
|
|
return false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
isOptional = () => {
|
2022-09-27 11:19:50 +02:00
|
|
|
this._optional = true
|
2022-09-20 03:11:34 +02:00
|
|
|
return this.addRule(() => true)
|
|
|
|
}
|
|
|
|
|
|
|
|
isEqual = (comparison: unknown) => {
|
|
|
|
return this.addRule((value: unknown) => {
|
|
|
|
return value === comparison
|
|
|
|
})
|
|
|
|
}
|
2022-09-13 00:54:06 +02:00
|
|
|
|
2022-09-20 03:11:34 +02:00
|
|
|
asNumber = () => {
|
|
|
|
const newVNumber = new VNumber(this)
|
|
|
|
return newVNumber
|
|
|
|
}
|
2022-09-15 17:10:37 +02:00
|
|
|
|
2022-09-20 03:11:34 +02:00
|
|
|
asBoolean = () => {
|
|
|
|
const newVBoolean = new VBoolean(this)
|
|
|
|
return newVBoolean
|
|
|
|
}
|
|
|
|
|
|
|
|
asObject = () => {
|
|
|
|
const newVObject = new VObject(this)
|
|
|
|
return newVObject
|
|
|
|
}
|
|
|
|
|
|
|
|
message(value: string) {
|
|
|
|
this._message = value
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
validate = async (req: Request): Promise<ValidateResult> => {
|
|
|
|
const result: ValidateResult = {
|
|
|
|
isValid: true,
|
|
|
|
message: undefined,
|
|
|
|
target: this.target,
|
|
|
|
key: this.key,
|
|
|
|
value: undefined,
|
|
|
|
}
|
|
|
|
|
|
|
|
let value: Type = undefined
|
|
|
|
if (this.target === 'query') {
|
|
|
|
value = req.query(this.key)
|
|
|
|
}
|
|
|
|
if (this.target === 'header') {
|
|
|
|
value = req.header(this.key)
|
|
|
|
}
|
|
|
|
if (this.target === 'body') {
|
|
|
|
const body = await req.parseBody()
|
|
|
|
value = body[this.key]
|
|
|
|
}
|
|
|
|
if (this.target === 'json') {
|
2022-10-03 23:16:25 +02:00
|
|
|
try {
|
|
|
|
const obj = (await req.json()) as JSONObject
|
|
|
|
value = JSONPath(obj, this.key)
|
|
|
|
} catch (e) {
|
|
|
|
throw new Error('Malformed JSON in request body')
|
|
|
|
}
|
2022-09-20 03:11:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
result.value = value
|
|
|
|
result.isValid = this.validateValue(value)
|
|
|
|
|
2022-09-30 14:56:39 +02:00
|
|
|
if (result.isValid === false) {
|
2022-09-20 03:11:34 +02:00
|
|
|
if (this._message) {
|
|
|
|
result.message = this._message
|
|
|
|
} else {
|
2022-09-30 14:56:39 +02:00
|
|
|
const valToStr = Array.isArray(value)
|
|
|
|
? `[${value
|
|
|
|
.map((val) =>
|
|
|
|
val === undefined ? 'undefined' : typeof val === 'string' ? `"${val}"` : val
|
|
|
|
)
|
|
|
|
.join(', ')}]`
|
|
|
|
: value
|
2022-09-20 03:11:34 +02:00
|
|
|
switch (this.target) {
|
|
|
|
case 'query':
|
2022-09-30 14:56:39 +02:00
|
|
|
result.message = `Invalid Value: the query parameter "${this.key}" is invalid - ${valToStr}`
|
2022-09-20 03:11:34 +02:00
|
|
|
break
|
|
|
|
case 'header':
|
2022-09-30 14:56:39 +02:00
|
|
|
result.message = `Invalid Value: the request header "${this.key}" is invalid - ${valToStr}`
|
2022-09-20 03:11:34 +02:00
|
|
|
break
|
|
|
|
case 'body':
|
2022-09-30 14:56:39 +02:00
|
|
|
result.message = `Invalid Value: the request body "${this.key}" is invalid - ${valToStr}`
|
2022-09-20 03:11:34 +02:00
|
|
|
break
|
|
|
|
case 'json':
|
2022-09-30 14:56:39 +02:00
|
|
|
result.message = `Invalid Value: the JSON body "${this.key}" is invalid - ${valToStr}`
|
2022-09-20 03:11:34 +02:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
2022-09-15 17:10:37 +02:00
|
|
|
|
2022-09-20 03:11:34 +02:00
|
|
|
private validateValue = (value: Type): boolean => {
|
|
|
|
// Check type
|
2022-09-30 14:56:39 +02:00
|
|
|
if (this.isArray) {
|
|
|
|
if (!Array.isArray(value)) {
|
2022-09-27 11:19:50 +02:00
|
|
|
return false
|
|
|
|
}
|
2022-09-14 01:30:46 +02:00
|
|
|
|
2022-09-30 14:56:39 +02:00
|
|
|
for (const val of value) {
|
|
|
|
if (typeof val !== this.type) {
|
|
|
|
// Value is of wrong type here
|
|
|
|
// If not optional, or optional and not undefined, return false
|
|
|
|
if (!this._optional || typeof val !== 'undefined') return false
|
|
|
|
}
|
|
|
|
}
|
2022-09-14 01:30:46 +02:00
|
|
|
|
2022-09-30 14:56:39 +02:00
|
|
|
// Sanitize
|
|
|
|
for (const sanitizer of this.sanitizers) {
|
|
|
|
value = value.map((innerVal) => sanitizer(innerVal)) as JSONArray
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const rule of this.rules) {
|
|
|
|
for (const val of value) {
|
|
|
|
if (!rule(val)) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
if (typeof value !== this.type) {
|
|
|
|
if (this._optional && typeof value === 'undefined') {
|
|
|
|
// Do nothing.
|
|
|
|
// The value is allowed to be `undefined` if it is `optional`
|
|
|
|
} else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sanitize
|
|
|
|
for (const sanitizer of this.sanitizers) {
|
|
|
|
value = sanitizer(value)
|
2022-09-20 03:11:34 +02:00
|
|
|
}
|
2022-09-30 14:56:39 +02:00
|
|
|
|
|
|
|
for (const rule of this.rules) {
|
|
|
|
if (!rule(value)) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
2022-09-20 03:11:34 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-09-13 00:54:06 +02:00
|
|
|
|
2022-09-20 03:11:34 +02:00
|
|
|
export class VString extends VBase {
|
|
|
|
constructor(options: VOptions) {
|
|
|
|
super(options)
|
|
|
|
this.type = 'string'
|
|
|
|
}
|
2022-09-13 00:54:06 +02:00
|
|
|
|
2022-09-30 14:56:39 +02:00
|
|
|
asArray = () => {
|
|
|
|
return new VStringArray(this)
|
|
|
|
}
|
|
|
|
|
2022-09-20 03:11:34 +02:00
|
|
|
isEmpty = (
|
2022-09-13 00:54:06 +02:00
|
|
|
options: {
|
|
|
|
ignore_whitespace: boolean
|
|
|
|
} = { ignore_whitespace: false }
|
2022-09-20 03:11:34 +02:00
|
|
|
) => {
|
|
|
|
return this.addRule((value) => rule.isEmpty(value as string, options))
|
|
|
|
}
|
|
|
|
|
|
|
|
isLength = (options: Partial<{ min: number; max: number }> | number, arg2?: number) => {
|
|
|
|
return this.addRule((value) => rule.isLength(value as string, options, arg2))
|
|
|
|
}
|
|
|
|
|
|
|
|
isAlpha = () => {
|
|
|
|
return this.addRule((value) => rule.isAlpha(value as string))
|
|
|
|
}
|
|
|
|
|
|
|
|
isNumeric = () => {
|
|
|
|
return this.addRule((value) => rule.isNumeric(value as string))
|
|
|
|
}
|
|
|
|
|
|
|
|
contains = (
|
2022-09-13 00:54:06 +02:00
|
|
|
elem: string,
|
|
|
|
options: Partial<{ ignoreCase: boolean; minOccurrences: number }> = {
|
|
|
|
ignoreCase: false,
|
|
|
|
minOccurrences: 1,
|
|
|
|
}
|
2022-09-20 03:11:34 +02:00
|
|
|
) => {
|
|
|
|
return this.addRule((value) => rule.contains(value as string, elem, options))
|
|
|
|
}
|
|
|
|
|
|
|
|
isIn = (options: string[]) => {
|
|
|
|
return this.addRule((value) => rule.isIn(value as string, options))
|
|
|
|
}
|
|
|
|
|
|
|
|
match = (regExp: RegExp) => {
|
|
|
|
return this.addRule((value) => rule.match(value as string, regExp))
|
|
|
|
}
|
|
|
|
|
|
|
|
trim = () => {
|
|
|
|
return this.addSanitizer((value) => sanitizer.trim(value as string))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class VNumber extends VBase {
|
|
|
|
constructor(options: VOptions) {
|
|
|
|
super(options)
|
|
|
|
this.type = 'number'
|
|
|
|
}
|
|
|
|
|
2022-09-30 14:56:39 +02:00
|
|
|
asArray = () => {
|
|
|
|
return new VNumberArray(this)
|
|
|
|
}
|
|
|
|
|
2022-09-20 03:11:34 +02:00
|
|
|
isGte = (min: number) => {
|
|
|
|
return this.addRule((value) => rule.isGte(value as number, min))
|
|
|
|
}
|
|
|
|
|
|
|
|
isLte = (min: number) => {
|
|
|
|
return this.addRule((value) => rule.isLte(value as number, min))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class VBoolean extends VBase {
|
|
|
|
constructor(options: VOptions) {
|
|
|
|
super(options)
|
|
|
|
this.type = 'boolean'
|
|
|
|
}
|
|
|
|
|
2022-09-30 14:56:39 +02:00
|
|
|
asArray = () => {
|
|
|
|
return new VBooleanArray(this)
|
|
|
|
}
|
|
|
|
|
2022-09-20 03:11:34 +02:00
|
|
|
isTrue = () => {
|
|
|
|
return this.addRule((value) => rule.isTrue(value as boolean))
|
|
|
|
}
|
|
|
|
|
|
|
|
isFalse = () => {
|
|
|
|
return this.addRule((value) => rule.isFalse(value as boolean))
|
|
|
|
}
|
|
|
|
}
|
2022-09-13 00:54:06 +02:00
|
|
|
|
2022-09-20 03:11:34 +02:00
|
|
|
export class VObject extends VBase {
|
|
|
|
constructor(options: VOptions) {
|
|
|
|
super(options)
|
|
|
|
this.type = 'object'
|
|
|
|
}
|
2022-09-13 00:54:06 +02:00
|
|
|
}
|
2022-09-30 14:56:39 +02:00
|
|
|
|
|
|
|
export class VNumberArray extends VNumber {
|
|
|
|
isArray: true
|
|
|
|
constructor(options: VOptions) {
|
|
|
|
super(options)
|
|
|
|
this.isArray = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
export class VStringArray extends VString {
|
|
|
|
isArray: true
|
|
|
|
constructor(options: VOptions) {
|
|
|
|
super(options)
|
|
|
|
this.isArray = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
export class VBooleanArray extends VBoolean {
|
|
|
|
isArray: true
|
|
|
|
constructor(options: VOptions) {
|
|
|
|
super(options)
|
|
|
|
this.isArray = true
|
|
|
|
}
|
|
|
|
}
|