From aa673ac854cfc276bdaf5c55c175eaae1c9d874d Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 30 Jul 2022 15:28:30 -0700 Subject: [PATCH] Use skip/take cursors instead of page number --- .../controllers/album-artists.controller.ts | 6 +- src/server/controllers/albums.controller.ts | 7 +- src/server/controllers/artists.controller.ts | 6 +- src/server/controllers/songs.controller.ts | 6 +- src/server/helpers/albums.helpers.ts | 1 - src/server/middleware/error-handler.ts | 6 +- src/server/services/album-artists.service.ts | 12 +-- src/server/services/albums.service.ts | 18 ++-- src/server/services/artists.service.ts | 12 +-- src/server/services/songs.service.ts | 17 ++-- src/server/types/types.ts | 9 +- src/server/utils/get-success-response.ts | 33 ++++--- src/server/utils/index.ts | 1 - src/server/utils/validate-request.ts | 56 ----------- src/server/utils/zod-validation.ts | 96 ++++++++++++++++++- 15 files changed, 154 insertions(+), 132 deletions(-) delete mode 100644 src/server/utils/validate-request.ts diff --git a/src/server/controllers/album-artists.controller.ts b/src/server/controllers/album-artists.controller.ts index 3224ae71c..b66ceff01 100644 --- a/src/server/controllers/album-artists.controller.ts +++ b/src/server/controllers/album-artists.controller.ts @@ -16,11 +16,11 @@ const getAlbumArtists = async (req: Request, res: Response) => { }), }); - const { limit, page, serverFolderIds } = req.query; + const { take, skip, serverFolderIds } = req.query; const data = await albumArtistsService.findMany(req, { - limit: Number(limit), - page: Number(page), serverFolderIds: String(serverFolderIds), + skip: Number(skip), + take: Number(take), user: req.auth, }); diff --git a/src/server/controllers/albums.controller.ts b/src/server/controllers/albums.controller.ts index 1b5c47e6e..557c2fe43 100644 --- a/src/server/controllers/albums.controller.ts +++ b/src/server/controllers/albums.controller.ts @@ -38,15 +38,16 @@ const getAlbums = async (req: Request, res: Response) => { }), }); - const { limit, page, serverFolderIds, serverUrls, sortBy, orderBy } = + const { take, serverFolderIds, serverUrls, sortBy, orderBy, skip } = req.query; + const data = await albumsService.findMany(req, { - limit: Number(limit), orderBy: orderBy as SortOrder, - page: Number(page), serverFolderIds: serverFolderIds && String(serverFolderIds), serverUrls: serverUrls && String(serverUrls), + skip: Number(skip), sortBy: sortBy as AlbumSort, + take: Number(take), user: req.auth, }); diff --git a/src/server/controllers/artists.controller.ts b/src/server/controllers/artists.controller.ts index 7ad478ae2..36acc955c 100644 --- a/src/server/controllers/artists.controller.ts +++ b/src/server/controllers/artists.controller.ts @@ -27,11 +27,11 @@ const getArtists = async (req: Request, res: Response) => { }), }); - const { limit, page, serverFolderIds } = req.query; + const { take, skip, serverFolderIds } = req.query; const data = await artistsService.findMany(req, { - limit: Number(limit), - page: Number(page), serverFolderIds: String(serverFolderIds), + skip: Number(skip), + take: Number(take), user: req.auth, }); diff --git a/src/server/controllers/songs.controller.ts b/src/server/controllers/songs.controller.ts index fd85ad9f5..939cd173d 100644 --- a/src/server/controllers/songs.controller.ts +++ b/src/server/controllers/songs.controller.ts @@ -18,12 +18,12 @@ const getSongs = async (req: Request, res: Response) => { }), }); - const { limit, page, serverFolderIds } = req.query; + const { take, skip, serverFolderIds } = req.query; const data = await songsService.findMany(req, { - limit: Number(limit), - page: Number(page), serverFolderIds: String(serverFolderIds), + skip: Number(skip), + take: Number(take), user: req.auth, }); diff --git a/src/server/helpers/albums.helpers.ts b/src/server/helpers/albums.helpers.ts index e8d6f99b3..d55ead09f 100644 --- a/src/server/helpers/albums.helpers.ts +++ b/src/server/helpers/albums.helpers.ts @@ -6,7 +6,6 @@ import { songHelpers } from './songs.helpers'; export enum AlbumSort { DATE_ADDED = 'date_added', DATE_ADDED_REMOTE = 'date_added_remote', - DATE_PLAYED = 'date_played', DATE_RELEASED = 'date_released', RANDOM = 'random', RATING = 'rating', diff --git a/src/server/middleware/error-handler.ts b/src/server/middleware/error-handler.ts index 8c28c49be..9089f328f 100644 --- a/src/server/middleware/error-handler.ts +++ b/src/server/middleware/error-handler.ts @@ -14,7 +14,11 @@ export const errorHandler = ( }); if (err.message) { - message = isJsonString(err.message) ? JSON.parse(err.message) : err.message; + message = isJsonString(err.message) + ? Array.isArray(JSON.parse(err.message)) + ? JSON.parse(err.message)[0].message // Handles errors sent from zod preprocess + : JSON.parse(err.message) + : err.message; } res.status(err.statusCode || 500).json({ diff --git a/src/server/services/album-artists.service.ts b/src/server/services/album-artists.service.ts index 32f425cd8..7246949ba 100644 --- a/src/server/services/album-artists.service.ts +++ b/src/server/services/album-artists.service.ts @@ -39,7 +39,7 @@ const findMany = async ( req: Request, options: { serverFolderIds: string; user: User } & OffsetPagination ) => { - const { user, limit, page, serverFolderIds: rServerFolderIds } = options; + const { user, take, serverFolderIds: rServerFolderIds, skip } = options; const serverFolderIds = splitNumberString(rServerFolderIds); if (!(await folderPermissions(serverFolderIds!, user))) { @@ -52,23 +52,21 @@ const findMany = async ( }; }); - const startIndex = limit * page; const totalEntries = await prisma.albumArtist.count({ where: { OR: serverFoldersFilter }, }); const albumArtists = await prisma.albumArtist.findMany({ include: { genres: true }, - skip: startIndex, - take: limit, + skip, + take, where: { OR: serverFoldersFilter }, }); return ApiSuccess.ok({ data: albumArtists, paginationItems: { - limit, - page, - startIndex, + skip, + take, totalEntries, url: req.originalUrl, }, diff --git a/src/server/services/albums.service.ts b/src/server/services/albums.service.ts index 3da61fa88..fbe3f9dc2 100644 --- a/src/server/services/albums.service.ts +++ b/src/server/services/albums.service.ts @@ -55,10 +55,10 @@ const findMany = async ( ) => { const { user, - limit, - page, + take, serverFolderIds: rServerFolderIds, serverUrls, + skip, sortBy, orderBy, } = options; @@ -75,7 +75,6 @@ const findMany = async ( serverFolderIds! ); - const startIndex = limit * page; let totalEntries = 0; let albums: Album[]; @@ -94,8 +93,8 @@ const findMany = async ( }, }, orderBy: { value: orderBy }, - skip: startIndex, - take: limit, + skip, + take, where: { album: { OR: serverFoldersFilter }, user: { id: user.id }, @@ -113,8 +112,8 @@ const findMany = async ( prisma.album.findMany({ include: { ...albumHelpers.include({ serverUrls, songs: false }) }, orderBy: [{ ...albumHelpers.sort(sortBy, orderBy) }], - skip: startIndex, - take: limit, + skip, + take, where: { OR: serverFoldersFilter }, }), ]); @@ -126,9 +125,8 @@ const findMany = async ( return ApiSuccess.ok({ data: toRes.albums(albums, user), paginationItems: { - limit, - page, - startIndex, + skip, + take, totalEntries, url: req.originalUrl, }, diff --git a/src/server/services/artists.service.ts b/src/server/services/artists.service.ts index 1d350a28d..74ba1d834 100644 --- a/src/server/services/artists.service.ts +++ b/src/server/services/artists.service.ts @@ -35,7 +35,7 @@ const findMany = async ( req: Request, options: { serverFolderIds: string; user: User } & OffsetPagination ) => { - const { user, limit, page, serverFolderIds: rServerFolderIds } = options; + const { user, skip, take, serverFolderIds: rServerFolderIds } = options; const serverFolderIds = splitNumberString(rServerFolderIds); if (!(await folderPermissions(serverFolderIds!, user))) { @@ -52,23 +52,21 @@ const findMany = async ( }; }); - const startIndex = limit * page; const totalEntries = await prisma.artist.count({ where: { OR: serverFoldersFilter }, }); const artists = await prisma.artist.findMany({ include: { genres: true }, - skip: startIndex, - take: limit, + skip, + take, where: { OR: serverFoldersFilter }, }); return ApiSuccess.ok({ data: artists, paginationItems: { - limit, - page, - startIndex, + skip, + take, totalEntries, url: req.originalUrl, }, diff --git a/src/server/services/songs.service.ts b/src/server/services/songs.service.ts index 4c2b01455..b67db4e15 100644 --- a/src/server/services/songs.service.ts +++ b/src/server/services/songs.service.ts @@ -1,6 +1,6 @@ import { Request } from 'express'; import { prisma } from '../lib'; -import { OffsetPagination, User } from '../types/types'; +import { User } from '../types/types'; import { ApiError, ApiSuccess, @@ -52,8 +52,8 @@ const findMany = async ( artistIds: rawArtistIds, songIds: rawSongIds, user, - limit, - page, + skip, + take, serverFolderIds: rServerFolderIds, } = options; const serverFolderIds = splitNumberString(rServerFolderIds); @@ -75,8 +75,6 @@ const findMany = async ( serverFolders: { some: { id: { in: serverFolderIds } } }, }; - const startIndex = limit * page; - const [totalEntries, songs] = await prisma.$transaction([ prisma.song.count({ where: { @@ -96,8 +94,8 @@ const findMany = async ( images: true, serverFolders: { include: { server: true } }, }, - skip: startIndex, - take: limit, + skip, + take, where: { OR: serverFoldersFilter }, }), ]); @@ -105,9 +103,8 @@ const findMany = async ( return ApiSuccess.ok({ data: songs, paginationItems: { - limit, - page, - startIndex, + skip, + take, totalEntries, url: req.originalUrl, }, diff --git a/src/server/types/types.ts b/src/server/types/types.ts index 9fa47919e..7e6a8e68c 100644 --- a/src/server/types/types.ts +++ b/src/server/types/types.ts @@ -113,8 +113,8 @@ export type Task = { }; export type OffsetPagination = { - limit: number; - page: number; + skip: number; + take: number; }; export type PaginationResponse = { @@ -131,9 +131,8 @@ export type SuccessResponse = { }; export type PaginationItems = { - limit: number; - page: number; - startIndex: number; + skip: number; + take: number; totalEntries: number; url: string; }; diff --git a/src/server/utils/get-success-response.ts b/src/server/utils/get-success-response.ts index 55e4c4fa0..dfeaf4d0a 100644 --- a/src/server/utils/get-success-response.ts +++ b/src/server/utils/get-success-response.ts @@ -1,17 +1,16 @@ import { PaginationItems } from '../types/types'; -const getPaginationUrl = (url: string, action: 'next' | 'prev') => { - const currentPageRegex = url.match(/page=(\d+)/gm); - - if (currentPageRegex) { - const currentPage = Number(currentPageRegex[0].split('=')[1]); - const newPage = action === 'next' ? currentPage + 1 : currentPage - 1; - const normalizedUrl = process.env.APP_BASE_URL?.replace(/\/$/, ''); - - return `${normalizedUrl}${url.replace(/page=\d+/gm, `page=${newPage}`)}`; +const getPaginationUrl = ( + url: string, + skip: number, + take: number, + action: 'next' | 'prev' +) => { + if (action === 'next') { + return url.replace(/skip=(\d+)/gm, `skip=${skip + take}`); } - return null; + return url.replace(/skip=(\d+)/gm, `skip=${skip - take}`); }; export const getSuccessResponse = (options: { @@ -24,15 +23,15 @@ export const getSuccessResponse = (options: { let pagination; if (paginationItems) { - const { startIndex, totalEntries, limit, url, page } = paginationItems; - const hasPrevPage = startIndex - limit >= 0; - const hasNextPage = startIndex + limit <= totalEntries; + const { skip, totalEntries, take, url } = paginationItems; + + const hasPrevPage = skip - take >= 0; + const hasNextPage = skip + take <= totalEntries; pagination = { - currentPage: page, - nextPage: hasNextPage ? getPaginationUrl(url, 'next') : null, - prevPage: hasPrevPage ? getPaginationUrl(url, 'prev') : null, - startIndex, + nextPage: hasNextPage ? getPaginationUrl(url, skip, take, 'next') : null, + prevPage: hasPrevPage ? getPaginationUrl(url, skip, take, 'prev') : null, + skip, totalEntries, }; } diff --git a/src/server/utils/index.ts b/src/server/utils/index.ts index bf6af1dd6..30c0beb6e 100644 --- a/src/server/utils/index.ts +++ b/src/server/utils/index.ts @@ -7,7 +7,6 @@ export * from './split-text-string'; export * from './folder-permissions'; export * from './is-array-equal'; export * from './is-json-string'; -export * from './validate-request'; export * from './unique-array'; export * from './zod-validation'; export * from './get-image-url'; diff --git a/src/server/utils/validate-request.ts b/src/server/utils/validate-request.ts deleted file mode 100644 index eb43cbc0e..000000000 --- a/src/server/utils/validate-request.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Taken from zod-express-middleware: https://github.com/Aquila169/zod-express-middleware - -import { ZodError, ZodSchema } from 'zod'; -import { ApiError } from './api-error'; - -type RequestValidation = { - body?: ZodSchema; - params?: ZodSchema; - query?: ZodSchema; -}; - -type ErrorListItem = { - errors: ZodError; - type: 'Query' | 'Params' | 'Body'; -}; - -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: 'Params' }); - } - } - - if (query) { - const parsed = query.safeParse(req.query); - if (!parsed.success) { - errors.push({ errors: parsed.error, type: 'Query' }); - } - } - - if (body) { - const parsed = body.safeParse(req.body); - if (!parsed.success) { - errors.push({ errors: parsed.error, type: '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); - } -}; diff --git a/src/server/utils/zod-validation.ts b/src/server/utils/zod-validation.ts index 1950b5891..5b8e885a2 100644 --- a/src/server/utils/zod-validation.ts +++ b/src/server/utils/zod-validation.ts @@ -1,11 +1,97 @@ -import { z } from 'zod'; +// 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 = { - limit: z.preprocess( - (a) => parseInt(z.string().parse(a), 10), - z.number().min(0).max(1000) + 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) ), - page: z.preprocess((a) => parseInt(z.string().parse(a), 10), z.number()), }; export const idValidation = {