Update validation middleware

- Move to separate directory
- Add TypedRequest utility fn
- Utilize as middleware instead of function
This commit is contained in:
jeffvli
2022-10-16 02:29:28 -07:00
parent 7b55ca2fa8
commit a0c634da2f
9 changed files with 274 additions and 99 deletions
-99
View File
@@ -1,99 +0,0 @@
// Taken from zod-express-middleware: https://github.com/Aquila169/zod-express-middleware
import { z, ZodError, ZodSchema } from 'zod';
import { ApiError } from './api-error';
export enum ValidationType {
BODY = 'Body',
PARAMS = 'Params',
QUERY = 'Query',
}
type RequestValidation<TParams, TQuery, TBody> = {
body?: ZodSchema<TBody>;
params?: ZodSchema<TParams>;
query?: ZodSchema<TQuery>;
};
type ErrorListItem = {
errors: ZodError<any>;
type: ValidationType;
};
export const validateRequest = (
req: any,
schemas: RequestValidation<any, any, any>
) => {
const { params, query, body } = schemas;
const errors: Array<ErrorListItem> = [];
if (params) {
const parsed = params.safeParse(req.params);
if (!parsed.success) {
errors.push({ errors: parsed.error, type: ValidationType.PARAMS });
}
}
if (query) {
const parsed = query.safeParse(req.query);
if (!parsed.success) {
errors.push({ errors: parsed.error, type: ValidationType.QUERY });
}
}
if (body) {
const parsed = body.safeParse(req.body);
if (!parsed.success) {
errors.push({ errors: parsed.error, type: ValidationType.BODY });
}
}
if (errors.length > 0) {
const message = JSON.stringify(
[
`(${errors[0].type})`,
`[${errors[0].errors.issues[0].path[0]}]`,
errors[0].errors.issues[0].message,
].join(' ')
);
throw ApiError.badRequest(message);
}
};
const requiredErrorMessage = (
type: 'Query' | 'Body' | 'Params',
key: string
) => {
return `(${type}) [${key}] Required`;
};
export const paginationValidation = {
skip: z.preprocess(
(a) =>
parseInt(
z
.string({
required_error: requiredErrorMessage(ValidationType.QUERY, 'skip'),
})
.parse(a),
10
),
z.number().min(0, { message: 'Must have skip' })
),
take: z.preprocess(
(a) =>
parseInt(
z
.string({
required_error: requiredErrorMessage(ValidationType.QUERY, 'take'),
})
.parse(a),
10
),
z.number().min(0)
),
};
export const idValidation = {
id: z.preprocess((a) => parseInt(z.string().parse(a), 10), z.number()),
};
@@ -0,0 +1,22 @@
import { z } from 'zod';
import { paginationValidation, idValidation } from './shared.validation';
export const list = {
body: z.object({}),
params: z.object({}),
query: z.object({
...paginationValidation,
serverFolderIds: z.string().min(1),
}),
};
export const detail = {
body: z.object({}),
params: z.object({ ...idValidation }),
query: z.object({}),
};
export const albumArtistsValidation = {
detail,
list,
};
@@ -0,0 +1,30 @@
import { z } from 'zod';
import { AlbumSort } from '../helpers/albums.helpers';
import {
idValidation,
orderByValidation,
paginationValidation,
serverFolderIdValidation,
} from './shared.validation';
const list = {
body: z.object({}),
params: z.object({}),
query: z.object({
...paginationValidation,
...serverFolderIdValidation,
...orderByValidation,
sortBy: z.nativeEnum(AlbumSort),
}),
};
const detail = {
body: z.object({}),
params: z.object(idValidation),
query: z.object({}),
};
export const albumsValidation = {
detail,
list,
};
@@ -0,0 +1,30 @@
import { z } from 'zod';
import { AlbumSort } from '../helpers/albums.helpers';
import {
idValidation,
orderByValidation,
paginationValidation,
serverFolderIdValidation,
} from './shared.validation';
const list = {
body: z.object({}),
params: z.object({}),
query: z.object({
...paginationValidation,
...serverFolderIdValidation,
...orderByValidation,
sortBy: z.nativeEnum(AlbumSort),
}),
};
const detail = {
body: z.object({}),
params: z.object(idValidation),
query: z.object({}),
};
export const artistsValidation = {
detail,
list,
};
+13
View File
@@ -0,0 +1,13 @@
import { albumsValidation } from './albums.validation';
import { serversValidation } from './servers.validation';
import { songsValidation } from './songs.validation';
import { usersValidation } from './users.validation';
export { validateRequest, TypedRequest } from './shared.validation';
export const validation = {
albums: albumsValidation,
servers: serversValidation,
songs: songsValidation,
users: usersValidation,
};
@@ -0,0 +1,45 @@
import { ServerType } from '@prisma/client';
import { z } from 'zod';
import { idValidation } from './shared.validation';
const detail = {
body: z.object({}),
params: z.object(idValidation),
query: z.object({}),
};
const create = {
body: z.object({
legacy: z.boolean().optional(),
name: z.string(),
password: z.string(),
type: z.enum([
ServerType.JELLYFIN,
ServerType.SUBSONIC,
ServerType.NAVIDROME,
]),
url: z.string(),
username: z.string(),
}),
params: z.object({}),
query: z.object({}),
};
const scan = {
body: z.object({ serverFolderId: z.string().array().optional() }),
params: z.object(idValidation),
query: z.object({}),
};
const refresh = {
body: z.object({}),
params: z.object(idValidation),
query: z.object({}),
};
export const serversValidation = {
create,
detail,
refresh,
scan,
};
+100
View File
@@ -0,0 +1,100 @@
// Modified from zod-express-middleware: https://github.com/Aquila169/zod-express-middleware
import { Request, RequestHandler } from 'express';
import { z, ZodError, ZodSchema } from 'zod';
import { SortOrder } from '../types/types';
import { ApiError } from '../utils';
export type TypedRequest<
S extends {
body: z.AnyZodObject;
params: z.AnyZodObject;
query: z.AnyZodObject;
}
> = Request<z.infer<S['params']>, any, z.infer<S['body']>, z.infer<S['query']>>;
export enum ValidationType {
BODY = 'Body',
PARAMS = 'Params',
QUERY = 'Query',
}
type RequestValidation<TParams, TQuery, TBody> = {
body?: ZodSchema<TBody>;
params?: ZodSchema<TParams>;
query?: ZodSchema<TQuery>;
};
type ErrorListItem = {
errors: ZodError<any>;
type: ValidationType;
};
export const validateRequest: <TParams = any, TQuery = any, TBody = any>(
schemas: RequestValidation<TParams, TQuery, TBody>
) => RequestHandler<TParams, any, TBody, TQuery> =
({ params, query, body }) =>
(req, _res, next) => {
const errors: Array<ErrorListItem> = [];
if (params) {
const parsed = params.safeParse(req.params);
if (!parsed.success) {
errors.push({ errors: parsed.error, type: ValidationType.PARAMS });
}
}
if (query) {
const parsed = query.safeParse(req.query);
if (!parsed.success) {
errors.push({ errors: parsed.error, type: ValidationType.QUERY });
}
}
if (body) {
const parsed = body.safeParse(req.body);
if (!parsed.success) {
errors.push({ errors: parsed.error, type: ValidationType.BODY });
}
}
if (errors.length > 0) {
const message = JSON.stringify(
[
`(${errors[0].type})`,
`[${errors[0].errors.issues[0].path[0]}]`,
errors[0].errors.issues[0].message,
].join(' ')
);
throw ApiError.badRequest(message);
}
return next();
};
// const requiredErrorMessage = (
// type: 'Query' | 'Body' | 'Params',
// key: string
// ) => {
// return `(${type}) [${key}] Required`;
// };
export const paginationValidation = {
skip: z.string().refine((value) => {
const parsed = Number(value);
return !Number.isNaN(parsed) && parsed >= 0;
}),
take: z.string().refine((value) => {
const parsed = Number(value);
return !Number.isNaN(parsed) && parsed >= 0;
}),
};
export const idValidation = {
id: z.string().uuid(),
};
export const serverFolderIdValidation = {
serverFolderId: z.optional(z.string().uuid().array()),
};
export const orderByValidation = {
orderBy: z.nativeEnum(SortOrder),
};
@@ -0,0 +1,22 @@
import { z } from 'zod';
import {
idValidation,
paginationValidation,
serverFolderIdValidation,
} from './shared.validation';
const list = {
body: z.object({}),
params: z.object(idValidation),
query: z.object({
...paginationValidation,
...serverFolderIdValidation,
albumIds: z.optional(z.string()),
artistIds: z.optional(z.string()),
songIds: z.optional(z.string()),
}),
};
export const songsValidation = {
list,
};
@@ -0,0 +1,12 @@
import { z } from 'zod';
import { idValidation } from './shared.validation';
const detail = {
body: z.object({}),
params: z.object(idValidation),
query: z.object({}),
};
export const usersValidation = {
detail,
};