mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 12:14:46 -04:00
v5 api: Random query parameter becomes sort value
This commit is contained in:
parent
0e230114a7
commit
2afccaa813
@ -20,6 +20,5 @@
|
|||||||
"@types/pg": "^8.11.10",
|
"@types/pg": "^8.11.10",
|
||||||
"bun-types": "^1.1.42"
|
"bun-types": "^1.1.42"
|
||||||
},
|
},
|
||||||
"module": "src/index.js",
|
"module": "src/index.js"
|
||||||
"packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
|
|
||||||
}
|
}
|
||||||
|
@ -157,13 +157,12 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
|||||||
.get(
|
.get(
|
||||||
"",
|
"",
|
||||||
async ({
|
async ({
|
||||||
query: { limit, after, sort, filter, random },
|
query: { limit, after, sort, filter },
|
||||||
headers: { "accept-language": languages },
|
headers: { "accept-language": languages },
|
||||||
request: { url },
|
request: { url },
|
||||||
}) => {
|
}) => {
|
||||||
const langs = processLanguages(languages);
|
const langs = processLanguages(languages);
|
||||||
const [transQ, transCol] = getTranslationQuery(langs, true);
|
const [transQ, transCol] = getTranslationQuery(langs, true);
|
||||||
|
|
||||||
// TODO: Add sql indexes on sort keys
|
// TODO: Add sql indexes on sort keys
|
||||||
|
|
||||||
const items = await db
|
const items = await db
|
||||||
@ -177,10 +176,14 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
|||||||
.innerJoin(transQ, eq(shows.pk, transQ.pk))
|
.innerJoin(transQ, eq(shows.pk, transQ.pk))
|
||||||
.where(and(filter, keysetPaginate({ table: shows, after, sort })))
|
.where(and(filter, keysetPaginate({ table: shows, after, sort })))
|
||||||
.orderBy(
|
.orderBy(
|
||||||
...(random !== undefined
|
...(sort.random !== undefined
|
||||||
? [sql`md5(${random} || ${shows.pk} )`]
|
? [
|
||||||
|
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],
|
x.desc ? sql`${shows[x.key]} desc nulls last` : shows[x.key],
|
||||||
),
|
),
|
||||||
shows.pk,
|
shows.pk,
|
||||||
@ -193,17 +196,10 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
|||||||
detail: { description: "Get all movies" },
|
detail: { description: "Get all movies" },
|
||||||
query: t.Object({
|
query: t.Object({
|
||||||
sort: Sort(["slug", "rating", "airDate", "createdAt", "nextRefresh"], {
|
sort: Sort(["slug", "rating", "airDate", "createdAt", "nextRefresh"], {
|
||||||
// TODO: Add random
|
|
||||||
remap: { airDate: "startAir" },
|
remap: { airDate: "startAir" },
|
||||||
default: ["slug"],
|
default: ["slug"],
|
||||||
description: "How to sort the query",
|
description: "How to sort the query",
|
||||||
}),
|
}),
|
||||||
random: t.Optional(
|
|
||||||
t.Integer({
|
|
||||||
minimum: 0,
|
|
||||||
description: "Seed to shuffle items",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
filter: t.Optional(Filter({ def: movieFilters })),
|
filter: t.Optional(Filter({ def: movieFilters })),
|
||||||
limit: t.Integer({
|
limit: t.Integer({
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
|
@ -24,7 +24,7 @@ export const keysetPaginate = <
|
|||||||
sort,
|
sort,
|
||||||
after,
|
after,
|
||||||
}: {
|
}: {
|
||||||
table: Table<"pk" | Sort<T, Remap>[number]["key"]>;
|
table: Table<"pk" | Sort<T, Remap>["sort"][number]["key"]>;
|
||||||
after: string | undefined;
|
after: string | undefined;
|
||||||
sort: Sort<T, Remap>;
|
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
|
// PERF: See https://use-the-index-luke.com/sql/partial-results/fetch-next-page#sb-equivalent-logic
|
||||||
let where = undefined;
|
let where = undefined;
|
||||||
let previous = 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;
|
const cmp = by.desc ? lt : gt;
|
||||||
where = or(
|
where = or(
|
||||||
where,
|
where,
|
||||||
@ -62,7 +62,7 @@ export const keysetPaginate = <
|
|||||||
|
|
||||||
export const generateAfter = (cursor: any, sort: Sort<any, any>) => {
|
export const generateAfter = (cursor: any, sort: Sort<any, any>) => {
|
||||||
const ret = [
|
const ret = [
|
||||||
...sort.map((by) => cursor[by.remmapedKey ?? by.key]),
|
...sort.sort.map((by) => cursor[by.remmapedKey ?? by.key]),
|
||||||
cursor.pk,
|
cursor.pk,
|
||||||
];
|
];
|
||||||
return Buffer.from(JSON.stringify(ret), "utf-8").toString("base64url");
|
return Buffer.from(JSON.stringify(ret), "utf-8").toString("base64url");
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { t, TSchema } from "elysia";
|
import { t } from "elysia";
|
||||||
|
|
||||||
export type Sort<
|
export type Sort<
|
||||||
T extends string[],
|
T extends string[],
|
||||||
Remap extends Partial<Record<T[number], string>>,
|
Remap extends Partial<Record<T[number], string>>,
|
||||||
> = {
|
> = {
|
||||||
key: Exclude<T[number], keyof Remap> | NonNullable<Remap[keyof Remap]>;
|
sort: {
|
||||||
remmapedKey?: keyof Remap;
|
key: Exclude<T[number], keyof Remap> | NonNullable<Remap[keyof Remap]>;
|
||||||
desc: boolean;
|
remmapedKey?: keyof Remap;
|
||||||
}[];
|
desc: boolean;
|
||||||
|
}[];
|
||||||
|
random?: { desc: boolean; seed: number };
|
||||||
|
};
|
||||||
|
|
||||||
export type NonEmptyArray<T> = [T, ...T[]];
|
export type NonEmptyArray<T> = [T, ...T[]];
|
||||||
|
|
||||||
@ -29,9 +32,15 @@ export const Sort = <
|
|||||||
t
|
t
|
||||||
.Transform(
|
.Transform(
|
||||||
t.Array(
|
t.Array(
|
||||||
t.UnionEnum([
|
t.Union([
|
||||||
...values,
|
t.UnionEnum([
|
||||||
...values.map((x: T[number]) => `-${x}` as const),
|
...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
|
// 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> => {
|
.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 desc = x[0] === "-";
|
||||||
const key = (desc ? x.substring(1) : x) as T[number];
|
const key = (desc ? x.substring(1) : x) as T[number];
|
||||||
if (key in remap) return { key: remap[key]!, remmapedKey: key, desc };
|
if (key == "random") {
|
||||||
return { key: key as Exclude<typeof key, keyof Remap>, desc };
|
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(() => {
|
.Encode(() => {
|
||||||
throw new Error("Encode not supported for sort");
|
throw new Error("Encode not supported for sort");
|
||||||
|
@ -126,7 +126,7 @@ describe("Get all movies", () => {
|
|||||||
it("No limit, compare order with same seeds", async () => {
|
it("No limit, compare order with same seeds", async () => {
|
||||||
// First query
|
// First query
|
||||||
let [resp1, body1] = await getMovies({
|
let [resp1, body1] = await getMovies({
|
||||||
random: 100,
|
sort: "random:100",
|
||||||
});
|
});
|
||||||
expectStatus(resp1, body1).toBe(200);
|
expectStatus(resp1, body1).toBe(200);
|
||||||
const items1: Movie[] = body1.items;
|
const items1: Movie[] = body1.items;
|
||||||
@ -134,7 +134,7 @@ describe("Get all movies", () => {
|
|||||||
|
|
||||||
// Second query
|
// Second query
|
||||||
let [resp2, body2] = await getMovies({
|
let [resp2, body2] = await getMovies({
|
||||||
random: 100,
|
sort: "random:100",
|
||||||
});
|
});
|
||||||
expectStatus(resp2, body2).toBe(200);
|
expectStatus(resp2, body2).toBe(200);
|
||||||
const items2: Movie[] = body2.items;
|
const items2: Movie[] = body2.items;
|
||||||
@ -145,7 +145,7 @@ describe("Get all movies", () => {
|
|||||||
it("No limit, compare order with different seeds", async () => {
|
it("No limit, compare order with different seeds", async () => {
|
||||||
// First query
|
// First query
|
||||||
let [resp1, body1] = await getMovies({
|
let [resp1, body1] = await getMovies({
|
||||||
random: 100,
|
sort: "random:100",
|
||||||
});
|
});
|
||||||
expectStatus(resp1, body1).toBe(200);
|
expectStatus(resp1, body1).toBe(200);
|
||||||
const items1: Movie[] = body1.items;
|
const items1: Movie[] = body1.items;
|
||||||
@ -153,14 +153,42 @@ describe("Get all movies", () => {
|
|||||||
|
|
||||||
// Second query
|
// Second query
|
||||||
let [resp2, body2] = await getMovies({
|
let [resp2, body2] = await getMovies({
|
||||||
random: 1,
|
sort: "random:5",
|
||||||
});
|
});
|
||||||
expectStatus(resp2, body2).toBe(200);
|
expectStatus(resp2, body2).toBe(200);
|
||||||
const items2: Movie[] = body2.items;
|
const items2: Movie[] = body2.items;
|
||||||
const items2Ids = items2.map(({ id }) => id);
|
const items2Ids = items2.map(({ id }) => id);
|
||||||
|
|
||||||
console.log(items1Ids, items2Ids);
|
|
||||||
expect(items1Ids).not.toEqual(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]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -30,7 +30,6 @@ export const getMovies = async ({
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
after?: string;
|
after?: string;
|
||||||
sort?: string | string[];
|
sort?: string | string[];
|
||||||
random?: number;
|
|
||||||
langs?: string;
|
langs?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const resp = await movieApp.handle(
|
const resp = await movieApp.handle(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user