diff --git a/src/server/utils/zod-validation.ts b/src/server/utils/zod-validation.ts deleted file mode 100644 index 5b8e885a2..000000000 --- a/src/server/utils/zod-validation.ts +++ /dev/null @@ -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 = { - body?: ZodSchema; - params?: ZodSchema; - query?: ZodSchema; -}; - -type ErrorListItem = { - errors: ZodError; - type: ValidationType; -}; - -export const validateRequest = ( - req: any, - schemas: RequestValidation -) => { - const { params, query, body } = schemas; - const errors: Array = []; - - 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()), -}; diff --git a/src/server/validations/album-artists.validation.ts b/src/server/validations/album-artists.validation.ts new file mode 100644 index 000000000..c10e08bab --- /dev/null +++ b/src/server/validations/album-artists.validation.ts @@ -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, +}; diff --git a/src/server/validations/albums.validation.ts b/src/server/validations/albums.validation.ts new file mode 100644 index 000000000..d955e9a30 --- /dev/null +++ b/src/server/validations/albums.validation.ts @@ -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, +}; diff --git a/src/server/validations/artists.validation.ts b/src/server/validations/artists.validation.ts new file mode 100644 index 000000000..3067aa4c3 --- /dev/null +++ b/src/server/validations/artists.validation.ts @@ -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, +}; diff --git a/src/server/validations/index.ts b/src/server/validations/index.ts new file mode 100644 index 000000000..9ff392f04 --- /dev/null +++ b/src/server/validations/index.ts @@ -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, +}; diff --git a/src/server/validations/servers.validation.ts b/src/server/validations/servers.validation.ts new file mode 100644 index 000000000..259e6dce9 --- /dev/null +++ b/src/server/validations/servers.validation.ts @@ -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, +}; diff --git a/src/server/validations/shared.validation.ts b/src/server/validations/shared.validation.ts new file mode 100644 index 000000000..59b6593e5 --- /dev/null +++ b/src/server/validations/shared.validation.ts @@ -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, any, z.infer, z.infer>; + +export enum ValidationType { + BODY = 'Body', + PARAMS = 'Params', + QUERY = 'Query', +} + +type RequestValidation = { + body?: ZodSchema; + params?: ZodSchema; + query?: ZodSchema; +}; + +type ErrorListItem = { + errors: ZodError; + type: ValidationType; +}; + +export const validateRequest: ( + schemas: RequestValidation +) => RequestHandler = + ({ params, query, body }) => + (req, _res, next) => { + const errors: Array = []; + 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), +}; diff --git a/src/server/validations/songs.validation.ts b/src/server/validations/songs.validation.ts new file mode 100644 index 000000000..00b0becc3 --- /dev/null +++ b/src/server/validations/songs.validation.ts @@ -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, +}; diff --git a/src/server/validations/users.validation.ts b/src/server/validations/users.validation.ts new file mode 100644 index 000000000..d1cc433fa --- /dev/null +++ b/src/server/validations/users.validation.ts @@ -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, +};