Last active
July 10, 2024 21:04
-
-
Save shane-js/58f1765e62ce6a170d3113e71c9b492c to your computer and use it in GitHub Desktop.
Quick & dirty modification of express-json-validator-middleware to support using @apideck/better-ajv-errors on errors output
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Adapted from 'express-json-validator-middleware' package https://github.com/simonplend/express-json-validator-middleware | |
// Modified to be able to use @apideck/better-ajv-errors package | |
// Relevant Github Issue: https://github.com/simonplend/express-json-validator-middleware/issues/123 | |
import { | |
betterAjvErrors, | |
ValidationError as BetterAjvErrorObject, | |
} from "@apideck/better-ajv-errors"; | |
import { Request } from "express"; | |
import { RequestHandler } from "express-serve-static-core"; | |
import { JSONSchema6 } from "json-schema"; | |
import Ajv, { ErrorObject, Options as AjvOptions } from "ajv"; | |
import { XOR } from "ts-essentials"; | |
export type RequestOptionKey = "body" | "params" | "query"; | |
type RequestOptionMap<T> = { | |
[K in RequestOptionKey]?: T; | |
}; | |
type ErrorRequestOptionMapPotentialTypes = XOR< | |
ErrorObject[], | |
BetterAjvErrorObject[] | |
>; | |
type AjvErrorsRequestOptionMap = RequestOptionMap<ErrorObject[]>; | |
type BetterAjvErrorsRequestOptionMap = RequestOptionMap<BetterAjvErrorObject[]>; | |
export type AllowedSchema = JSONSchema6; // currently JSONSchema6 is the only supported schema type of @apideck/better-ajv-errors | |
export type ValidateFunction = | |
| ((req: Request) => AllowedSchema) | |
| AllowedSchema; | |
type ValidatorParams = { | |
ajvOptions: AjvOptions; | |
useBetterAjvErrors?: boolean; | |
}; | |
type ValidateParams = { | |
requestSchemaMap: RequestOptionMap<ValidateFunction>; | |
useBetterAjvErrors?: boolean; | |
}; | |
/** | |
* Express middleware for validating requests | |
* | |
* @class Validator | |
*/ | |
export class Validator { | |
public ajv: Ajv; | |
public globalUseBetterAjvErrors = false; | |
constructor({ ajvOptions, useBetterAjvErrors = false }: ValidatorParams) { | |
this.ajv = new Ajv(ajvOptions); | |
this.globalUseBetterAjvErrors = useBetterAjvErrors; // pass in true if you want all validate functions coming from this Validator instance to use betterAjvErrors without having to specifiy on each validate call | |
this.validate = this.validate.bind(this); | |
} | |
/** | |
* Validator method to be used as middleware | |
* | |
* @param {Object} params | |
* @param {Object} params.requestSchemaMap Options in format { request_property: schema } | |
* @param {boolean} [params.useBetterAjvErrors] Whether or not to use better ajv errors, defaults to global which defaults to false if not specified when instantiating Validator | |
* @returns | |
*/ | |
validate({ | |
requestSchemaMap, | |
useBetterAjvErrors = false, | |
}: ValidateParams): RequestHandler { | |
// Self is a reference to the current Validator instance | |
// eslint-disable-next-line @typescript-eslint/no-this-alias | |
const self = this; | |
const effectiveUseBetterAjvErrors = | |
useBetterAjvErrors || self.globalUseBetterAjvErrors; | |
// Cache validate functions | |
const validateFunctions = Object.keys(requestSchemaMap).map( | |
(requestProperty) => { | |
const schema = requestSchemaMap[requestProperty as RequestOptionKey]; | |
if (typeof schema === "function") { | |
return { requestProperty, schemaFunction: schema }; | |
} | |
const validateFunction = this.ajv.compile(schema as AllowedSchema); | |
return { requestProperty, validateFunction, schema }; | |
}, | |
self | |
); | |
// The actual middleware function | |
return (req, res, next) => { | |
const validationErrors = {} as XOR< | |
AjvErrorsRequestOptionMap, | |
BetterAjvErrorsRequestOptionMap | |
>; | |
for (const { | |
requestProperty, | |
validateFunction, | |
schemaFunction, | |
schema, | |
} of validateFunctions) { | |
let effectiveSchema = schema; | |
let effectiveValidateFunction = validateFunction; | |
if (!validateFunction) { | |
// Get the schema from the dynamic schema function | |
effectiveSchema = schemaFunction ? schemaFunction(req) : {}; | |
effectiveValidateFunction = this.ajv.compile(effectiveSchema); | |
} | |
// Test if property is valid | |
const valid = | |
effectiveValidateFunction && | |
effectiveValidateFunction(req[requestProperty as RequestOptionKey]); | |
if (!valid) { | |
const errors = effectiveUseBetterAjvErrors | |
? betterAjvErrors({ | |
schema: effectiveSchema as JSONSchema6, | |
data: req[requestProperty as RequestOptionKey], | |
errors: effectiveValidateFunction | |
? effectiveValidateFunction.errors | |
: [], | |
basePath: requestProperty, | |
}) | |
: effectiveValidateFunction | |
? effectiveValidateFunction.errors | |
: []; | |
validationErrors[requestProperty as RequestOptionKey] = | |
errors || ({} as ErrorRequestOptionMapPotentialTypes); | |
} | |
} | |
if (Object.keys(validationErrors).length !== 0) { | |
next(new ValidationError(validationErrors)); | |
} else { | |
next(); | |
} | |
}; | |
} | |
} | |
type ValidationErrorConstructorErrorsParam = XOR< | |
AjvErrorsRequestOptionMap, | |
BetterAjvErrorsRequestOptionMap | |
>; | |
/** | |
* Validation Error | |
* | |
* @class ValidationError | |
* @extends {Error} | |
*/ | |
export class ValidationError extends Error { | |
public validationErrors: ValidationErrorConstructorErrorsParam; | |
constructor(validationErrors: ValidationErrorConstructorErrorsParam) { | |
super(); | |
this.name = "JsonSchemaValidationError"; | |
this.validationErrors = validationErrors; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment