diff --git a/api/src/models/utils/keyset-paginate.ts b/api/src/models/utils/keyset-paginate.ts index 6ee14437..0c08db34 100644 --- a/api/src/models/utils/keyset-paginate.ts +++ b/api/src/models/utils/keyset-paginate.ts @@ -3,7 +3,7 @@ import { eq, or, type Column, and, gt, lt } from "drizzle-orm"; type Table = Record; -type After = [boolean, ...[string | number | boolean | undefined]]; +type After = (string | number | boolean | undefined)[]; // Create a filter (where) expression on the query to skip everything before/after the referenceID. // The generalized expression for this in pseudocode is: @@ -29,7 +29,7 @@ export const keysetPaginate = < sort: Sort; }) => { if (!after) return undefined; - const [reverse, ...cursor]: After = JSON.parse( + const cursor: After = JSON.parse( Buffer.from(after, "base64").toString("utf-8"), ); @@ -40,7 +40,7 @@ export const keysetPaginate = < let where = undefined; let previous = undefined; for (const [i, by] of [...sort, pkSort].entries()) { - const cmp = by.desc !== reverse ? lt : gt; + const cmp = by.desc ? lt : gt; where = or(where, and(previous, cmp(table[by.key], cursor[i]))); previous = and(previous, eq(table[by.key], cursor[i])); } @@ -48,18 +48,7 @@ export const keysetPaginate = < return where; }; -export const generateAfter = ( - cursor: any, - sort: Sort, - reverse?: boolean, -) => { - const ret = [ - reverse ?? false, - ...sort.map((by) => cursor[by.key]), - cursor.pk, - ]; +export const generateAfter = (cursor: any, sort: Sort) => { + const ret = [...sort.map((by) => cursor[by.key]), cursor.pk]; 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 3e3df633..a533dcfa 100644 --- a/api/src/models/utils/page.ts +++ b/api/src/models/utils/page.ts @@ -1,14 +1,13 @@ import type { ObjectOptions } from "@sinclair/typebox"; import { t, type TSchema } from "elysia"; import type { Sort } from "./sort"; -import { generateAfter, isReverse } from "./keyset-paginate"; +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.Nullable(t.String({ format: "uri" })), next: t.Nullable(t.String({ format: "uri" })), }, options, @@ -18,27 +17,12 @@ export const createPage = ( items: T[], { url, sort, limit }: { url: string; sort: Sort; limit: number }, ) => { - let prev: string | null = null; let next: string | null = null; - 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 (has[1 - reverse]) { + if (items.length === limit && limit > 0) { + const uri = new URL(url); uri.searchParams.set("after", generateAfter(items[items.length - 1], sort)); next = uri.toString(); } - return { items, this: url, prev, next }; + return { items, this: url, next }; }; diff --git a/api/tests/movies/get-all-movies.test.ts b/api/tests/movies/get-all-movies.test.ts index 1b76eea1..c3ee89a6 100644 --- a/api/tests/movies/get-all-movies.test.ts +++ b/api/tests/movies/get-all-movies.test.ts @@ -78,23 +78,47 @@ describe("Get all movies", () => { expectStatus(resp, body).toBe(422); expect(body).toMatchObject({ - details: expect.objectContaining( { - in: "slug eq gt bubble", - }), + details: expect.anything(), message: "Invalid filter: slug eq gt bubble.", status: 422, }); }); - it("Limit 1, default sort", async () => { + it("Limit 2, default sort", async () => { const [resp, body] = await getMovies({ - limit: 1, + limit: 2, langs: "en", }); expectStatus(resp, body).toBe(200); expect(body).toMatchObject({ - items: [bubble], - this: "", + items: [ + expect.objectContaining({ slug: bubble.slug }), + expect.objectContaining({ slug: dune.slug }), + ], + this: "http://localhost/movies?limit=2", + // we can't have the exact after since it contains the pk that changes with every tests. + next: expect.stringContaining( + "http://localhost/movies?limit=2&after=WyJkdW5lIiw0", + ), + }); + }); + it("Limit 2, default sort, page 2", async () => { + let [resp, body] = await getMovies({ + limit: 2, + langs: "en", + }); + expectStatus(resp, body).toBe(200); + + resp = await app.handle(new Request(body.next)); + body = await resp.json(); + + expectStatus(resp, body).toBe(200); + expect(body).toMatchObject({ + items: [expect.objectContaining({ slug: dune1984.slug })], + this: expect.stringContaining( + "http://localhost/movies?limit=2&after=WyJkdW5lIiw0", + ), + next: null, }); }); }); diff --git a/api/tests/movies/seed-movies.test.ts b/api/tests/movies/seed-movies.test.ts index b3e1aba4..8e950fe8 100644 --- a/api/tests/movies/seed-movies.test.ts +++ b/api/tests/movies/seed-movies.test.ts @@ -110,7 +110,7 @@ describe("Movie seeding", () => { expect(existing).toMatchObject({ slug: dune.slug, startAir: dune.airDate }); const [resp, body] = await createMovie({ ...dune, airDate: "2158-12-13" }); - expectStatus(resp, body).toBe(200); + expectStatus(resp, body).toBe(201); expect(body.id).toBeString(); expect(body.slug).toBe("dune-2158"); }); @@ -224,7 +224,7 @@ describe("Movie seeding", () => { }); const cleanup = async () => { - await db.delete(shows).where(inArray(shows.slug, [dune.slug])); + await db.delete(shows).where(inArray(shows.slug, [dune.slug, "dune-2158"])); await db.delete(videos).where(inArray(videos.id, [duneVideo.id])); }; // cleanup db beforehand to unsure tests are consistent