mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-03 19:17:16 -05: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