mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
Update validation middleware
- Move to separate directory - Add TypedRequest utility fn - Utilize as middleware instead of function
This commit is contained in:
@@ -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,
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user