From 879d2959d5bafddd5429140a484fd4dfba2d2767 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 6 Jan 2025 22:05:13 +0100 Subject: [PATCH] Create page with next/prev url --- api/src/controllers/movies.ts | 34 ++++++++++++------------- api/src/models/utils/keyset-paginate.ts | 20 ++++++++++++--- api/src/models/utils/page.ts | 25 ++++++++++++++++-- 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/api/src/controllers/movies.ts b/api/src/controllers/movies.ts index 3633ea85..258aa7ad 100644 --- a/api/src/controllers/movies.ts +++ b/api/src/controllers/movies.ts @@ -14,8 +14,8 @@ import { Genre, isUuid, keysetPaginate, + Page, processLanguages, - type Page, createPage, } from "~/models/utils"; @@ -147,7 +147,6 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) { status: 422, message: "Accept-Language header could not be satisfied.", - details: undefined, }, ], }, @@ -222,21 +221,20 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) `, }), }), - // response: { - // 200: Page(Movie, { - // description: "Paginated list of movies that match filters.", - // }), - // 422: { - // ...KError, - // description: "Invalid query parameters.", - // examples: [ - // { - // status: 422, - // message: "Accept-Language header could not be satisfied.", - // details: undefined, - // }, - // ], - // }, - // }, + response: { + 200: Page(Movie, { + description: "Paginated list of movies that match filters.", + }), + 422: { + ...KError, + description: "Invalid query parameters.", + examples: [ + { + status: 422, + message: "Accept-Language header could not be satisfied.", + }, + ], + }, + }, }, ); diff --git a/api/src/models/utils/keyset-paginate.ts b/api/src/models/utils/keyset-paginate.ts index f083d92a..d6710af3 100644 --- a/api/src/models/utils/keyset-paginate.ts +++ b/api/src/models/utils/keyset-paginate.ts @@ -3,6 +3,10 @@ import { eq, or, type Column, and, gt, lt } from "drizzle-orm"; type Table = Record; +type After = Record & { + reverse?: boolean; +}; + // Create a filter (where) expression on the query to skip everything before/after the referenceID. // The generalized expression for this in pseudocode is: // (x > a) OR @@ -27,7 +31,7 @@ export const keysetPaginate = < sort: Sort; }) => { if (!after) return undefined; - const cursor: Record = JSON.parse( + const { reverse, ...cursor }: After = JSON.parse( Buffer.from(after, "base64").toString("utf-8"), ); @@ -36,7 +40,7 @@ export const keysetPaginate = < let where = undefined; let previous = undefined; for (const by of [...sort, { key: "pk" as const, desc: false }]) { - const cmp = by.desc ? lt : gt; + const cmp = by.desc !== reverse ? lt : gt; where = or(where, and(previous, cmp(table[by.key], cursor[by.key]))); previous = and(previous, eq(table[by.key], cursor[by.key])); } @@ -44,4 +48,14 @@ export const keysetPaginate = < return where; }; - +export const generateAfter = ( + cursor: any, + sort: Sort, + reverse?: boolean, +) => { + const ret: After = { reverse }; + for (const by of sort) { + ret[by.key] = cursor[by.key]; + } + return Buffer.from(JSON.stringify(ret), "utf-8").toString("base64"); +}; diff --git a/api/src/models/utils/page.ts b/api/src/models/utils/page.ts index a41b8504..2a8773b8 100644 --- a/api/src/models/utils/page.ts +++ b/api/src/models/utils/page.ts @@ -1,13 +1,34 @@ import type { ObjectOptions } from "@sinclair/typebox"; import { t, type TSchema } from "elysia"; +import type { Sort } from "./sort"; +import { generateAfter } from "./keyset-paginate"; export const Page = (schema: T, options?: ObjectOptions) => t.Object( { items: t.Array(schema), this: t.String({ format: "uri" }), - prev: t.String({ format: "uri" }), - next: t.String({ format: "uri" }), + prev: t.Nullable(t.String({ format: "uri" })), + next: t.Nullable(t.String({ format: "uri" })), }, options, ); + +export const createPage = ( + items: T[], + { url, sort }: { url: string; sort: Sort }, +) => { + let prev: string | null = null; + let next: string | null = null; + const uri = new URL(url); + + if (uri.searchParams.has("after")) { + uri.searchParams.set("after", generateAfter(items[0], sort, true)); + prev = uri.toString(); + } + if (items.length) { + uri.searchParams.set("after", generateAfter(items[items.length - 1], sort)); + next = uri.toString(); + } + return { items, this: url, prev, next }; +};