diff --git a/api/src/controllers/movies.ts b/api/src/controllers/movies.ts index 258aa7ad..9bdc8ad1 100644 --- a/api/src/controllers/movies.ts +++ b/api/src/controllers/movies.ts @@ -44,7 +44,8 @@ const getTranslationQuery = (languages: string[]) => { return [query, col] as const; }; -const { pk: _, kind, startAir, endAir, ...moviesCol } = getColumns(shows); +// we keep the pk for after handling. it will be removed by elysia's validators after. +const { kind, startAir, endAir, ...moviesCol } = getColumns(shows); const movieFilters: FilterDef = { genres: { @@ -181,7 +182,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) ) .limit(limit); - return createPage(items, { url, sort }); + return createPage(items, { url, sort, limit }); }, { detail: { description: "Get all movies" }, @@ -201,7 +202,6 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) }), after: t.Optional( t.String({ - format: "byte", description: comment` Id of the cursor in the pagination. You can ignore this and only use the prev/next field in the response. diff --git a/api/src/models/utils/keyset-paginate.ts b/api/src/models/utils/keyset-paginate.ts index 81610499..6ee14437 100644 --- a/api/src/models/utils/keyset-paginate.ts +++ b/api/src/models/utils/keyset-paginate.ts @@ -58,5 +58,8 @@ export const generateAfter = ( ...sort.map((by) => cursor[by.key]), cursor.pk, ]; - return Buffer.from(JSON.stringify(ret), "utf-8").toString("base64"); + return Buffer.from(JSON.stringify(ret), "utf-8").toString("base64url"); }; + +const reverseStart = Buffer.from("[true,", "utf-8").toString("base64url"); +export const isReverse = (x: string) => x.startsWith(reverseStart); diff --git a/api/src/models/utils/page.ts b/api/src/models/utils/page.ts index 2a8773b8..3e3df633 100644 --- a/api/src/models/utils/page.ts +++ b/api/src/models/utils/page.ts @@ -1,7 +1,7 @@ import type { ObjectOptions } from "@sinclair/typebox"; import { t, type TSchema } from "elysia"; import type { Sort } from "./sort"; -import { generateAfter } from "./keyset-paginate"; +import { generateAfter, isReverse } from "./keyset-paginate"; export const Page = (schema: T, options?: ObjectOptions) => t.Object( @@ -16,17 +16,27 @@ export const Page = (schema: T, options?: ObjectOptions) => export const createPage = ( items: T[], - { url, sort }: { url: string; sort: Sort }, + { url, sort, limit }: { url: string; sort: Sort; limit: number }, ) => { let prev: string | null = null; let next: string | null = null; - const uri = new URL(url); - if (uri.searchParams.has("after")) { + const uri = new URL(url); + const after = uri.searchParams.get("after"); + const reverse = after && isReverse(after) ? 1 : 0; + + const has = [ + // prev + items.length > 0 && after, + // next + items.length === limit && limit > 0, + ]; + + if (has[0 + reverse]) { uri.searchParams.set("after", generateAfter(items[0], sort, true)); prev = uri.toString(); } - if (items.length) { + if (has[1 - reverse]) { uri.searchParams.set("after", generateAfter(items[items.length - 1], sort)); next = uri.toString(); }