Use skip/take cursors instead of page number

This commit is contained in:
jeffvli
2022-07-30 15:28:30 -07:00
parent b8cf1d8283
commit aa673ac854
15 changed files with 154 additions and 132 deletions
@@ -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,
});
+4 -3
View File
@@ -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,
});
+3 -3
View File
@@ -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,
});
+3 -3
View File
@@ -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,
});
-1
View File
@@ -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',
+5 -1
View File
@@ -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({
+5 -7
View File
@@ -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,
},
+8 -10
View File
@@ -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,
},
+5 -7
View File
@@ -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,
},
+7 -10
View File
@@ -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,
},
+4 -5
View File
@@ -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;
};
+16 -17
View File
@@ -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,
};
}
-1
View File
@@ -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';
-56
View File
@@ -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);
}
};
+91 -5
View File
@@ -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 = {