Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save shane-js/58f1765e62ce6a170d3113e71c9b492c to your computer and use it in GitHub Desktop.
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
// 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