mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 04:50:12 +02:00
Use skip/take cursors instead of page number
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<TParams, TQuery, TBody> = {
|
||||
body?: ZodSchema<TBody>;
|
||||
params?: ZodSchema<TParams>;
|
||||
query?: ZodSchema<TQuery>;
|
||||
};
|
||||
|
||||
type ErrorListItem = {
|
||||
errors: ZodError<any>;
|
||||
type: 'Query' | 'Params' | 'Body';
|
||||
};
|
||||
|
||||
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: '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);
|
||||
}
|
||||
};
|
||||
@@ -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<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 = {
|
||||
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 = {
|
||||
|
||||
Reference in New Issue
Block a user