mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 20:40:15 +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