v5 api: Random query parameter becomes sort value

This commit is contained in:
Arthur Jamet 2025-01-11 17:52:56 +00:00 committed by Zoe Roux
parent 0e230114a7
commit 2afccaa813
No known key found for this signature in database
6 changed files with 93 additions and 35 deletions

View File

@ -20,6 +20,5 @@
"@types/pg": "^8.11.10",
"bun-types": "^1.1.42"
},
"module": "src/index.js",
"packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
"module": "src/index.js"
}

View File

@ -157,13 +157,12 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
.get(
"",
async ({
query: { limit, after, sort, filter, random },
query: { limit, after, sort, filter },
headers: { "accept-language": languages },
request: { url },
}) => {
const langs = processLanguages(languages);
const [transQ, transCol] = getTranslationQuery(langs, true);
// TODO: Add sql indexes on sort keys
const items = await db
@ -177,10 +176,14 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
.innerJoin(transQ, eq(shows.pk, transQ.pk))
.where(and(filter, keysetPaginate({ table: shows, after, sort })))
.orderBy(
...(random !== undefined
? [sql`md5(${random} || ${shows.pk} )`]
...(sort.random !== undefined
? [
sort.random.desc
? sql`md5(${sort.random.seed} || ${shows.pk}) desc`
: sql`md5(${sort.random.seed} || ${shows.pk})`,
]
: []),
...sort.map((x) =>
...sort.sort.map((x) =>
x.desc ? sql`${shows[x.key]} desc nulls last` : shows[x.key],
),
shows.pk,
@ -193,17 +196,10 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
detail: { description: "Get all movies" },
query: t.Object({
sort: Sort(["slug", "rating", "airDate", "createdAt", "nextRefresh"], {
// TODO: Add random
remap: { airDate: "startAir" },
default: ["slug"],
description: "How to sort the query",
}),
random: t.Optional(
t.Integer({
minimum: 0,
description: "Seed to shuffle items",
}),
),
filter: t.Optional(Filter({ def: movieFilters })),
limit: t.Integer({
minimum: 1,

View File

@ -24,7 +24,7 @@ export const keysetPaginate = <
sort,
after,
}: {
table: Table<"pk" | Sort<T, Remap>[number]["key"]>;
table: Table<"pk" | Sort<T, Remap>["sort"][number]["key"]>;
after: string | undefined;
sort: Sort<T, Remap>;
}) => {
@ -39,7 +39,7 @@ export const keysetPaginate = <
// PERF: See https://use-the-index-luke.com/sql/partial-results/fetch-next-page#sb-equivalent-logic
let where = undefined;
let previous = undefined;
for (const [i, by] of [...sort, pkSort].entries()) {
for (const [i, by] of [...sort.sort, pkSort].entries()) {
const cmp = by.desc ? lt : gt;
where = or(
where,
@ -62,7 +62,7 @@ export const keysetPaginate = <
export const generateAfter = (cursor: any, sort: Sort<any, any>) => {
const ret = [
...sort.map((by) => cursor[by.remmapedKey ?? by.key]),
...sort.sort.map((by) => cursor[by.remmapedKey ?? by.key]),
cursor.pk,
];
return Buffer.from(JSON.stringify(ret), "utf-8").toString("base64url");

View File

@ -1,13 +1,16 @@
import { t, TSchema } from "elysia";
import { t } from "elysia";
export type Sort<
T extends string[],
Remap extends Partial<Record<T[number], string>>,
> = {
key: Exclude<T[number], keyof Remap> | NonNullable<Remap[keyof Remap]>;
remmapedKey?: keyof Remap;
desc: boolean;
}[];
sort: {
key: Exclude<T[number], keyof Remap> | NonNullable<Remap[keyof Remap]>;
remmapedKey?: keyof Remap;
desc: boolean;
}[];
random?: { desc: boolean; seed: number };
};
export type NonEmptyArray<T> = [T, ...T[]];
@ -29,9 +32,15 @@ export const Sort = <
t
.Transform(
t.Array(
t.UnionEnum([
...values,
...values.map((x: T[number]) => `-${x}` as const),
t.Union([
t.UnionEnum([
...values,
...values.map((x: T[number]) => `-${x}` as const),
]),
t.Union([
t.TemplateLiteral("random:${number}"),
t.TemplateLiteral("-random:${number}"),
]),
]),
{
// TODO: support explode: true (allow sort=slug,-createdAt). needs a pr to elysia
@ -42,12 +51,39 @@ export const Sort = <
),
)
.Decode((sort): Sort<T, Remap> => {
return sort.map((x) => {
const sortItems: Sort<T, Remap>["sort"] = [];
let random: Sort<T, Remap>["random"] = undefined;
for (const x of sort) {
const desc = x[0] === "-";
const key = (desc ? x.substring(1) : x) as T[number];
if (key in remap) return { key: remap[key]!, remmapedKey: key, desc };
return { key: key as Exclude<typeof key, keyof Remap>, desc };
});
if (key == "random") {
random = {
seed: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER),
desc,
};
continue;
} else if (key.startsWith("random:")) {
const strSeed = key.replace("random:", "");
random = {
seed: parseInt(strSeed),
desc,
};
continue;
}
if (key in remap) {
sortItems.push({ key: remap[key]!, remmapedKey: key, desc });
} else {
sortItems.push({
key: key as Exclude<typeof key, keyof Remap>,
desc,
});
}
}
return {
sort: sortItems,
random,
};
})
.Encode(() => {
throw new Error("Encode not supported for sort");

View File

@ -126,7 +126,7 @@ describe("Get all movies", () => {
it("No limit, compare order with same seeds", async () => {
// First query
let [resp1, body1] = await getMovies({
random: 100,
sort: "random:100",
});
expectStatus(resp1, body1).toBe(200);
const items1: Movie[] = body1.items;
@ -134,7 +134,7 @@ describe("Get all movies", () => {
// Second query
let [resp2, body2] = await getMovies({
random: 100,
sort: "random:100",
});
expectStatus(resp2, body2).toBe(200);
const items2: Movie[] = body2.items;
@ -145,7 +145,7 @@ describe("Get all movies", () => {
it("No limit, compare order with different seeds", async () => {
// First query
let [resp1, body1] = await getMovies({
random: 100,
sort: "random:100",
});
expectStatus(resp1, body1).toBe(200);
const items1: Movie[] = body1.items;
@ -153,14 +153,42 @@ describe("Get all movies", () => {
// Second query
let [resp2, body2] = await getMovies({
random: 1,
sort: "random:5",
});
expectStatus(resp2, body2).toBe(200);
const items2: Movie[] = body2.items;
const items2Ids = items2.map(({ id }) => id);
console.log(items1Ids, items2Ids);
expect(items1Ids).not.toEqual(items2Ids);
});
it("Limit 1, pages 1 and 2 ", async () => {
// First query fetches all
// use the result to know what is expected
let [resp, body] = await getMovies({
sort: "random:1234",
});
expectStatus(resp, body).toBe(200);
let items: Movie[] = body.items;
const expectedIds = items.map(({ id }) => id);
// Get First Page
[resp, body] = await getMovies({
sort: "random:1234",
limit: 1,
});
expectStatus(resp, body).toBe(200);
items = body.items;
expect(items.length).toBe(1);
expect(items[0].id).toBe(expectedIds[0]);
// Get Second Page
resp = await movieApp.handle(new Request(body.next));
body = await resp.json();
expectStatus(resp, body).toBe(200);
items = body.items;
expect(items.length).toBe(1);
expect(items[0].id).toBe(expectedIds[1]);
});
});
});

View File

@ -30,7 +30,6 @@ export const getMovies = async ({
limit?: number;
after?: string;
sort?: string | string[];
random?: number;
langs?: string;
}) => {
const resp = await movieApp.handle(