mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-03 19:17:16 -05:00 
			
		
		
		
	Rework /studios/:id to use relational query
This commit is contained in:
		
							parent
							
								
									34926dab51
								
							
						
					
					
						commit
						4b46963eff
					
				@ -89,7 +89,7 @@ const showRelations = {
 | 
				
			|||||||
			.from(studioTranslations)
 | 
								.from(studioTranslations)
 | 
				
			||||||
			.orderBy(
 | 
								.orderBy(
 | 
				
			||||||
				studioTranslations.pk,
 | 
									studioTranslations.pk,
 | 
				
			||||||
				sql`array_position(${sqlarr(languages)}, ${studioTranslations.language}`,
 | 
									sql`array_position(${sqlarr(languages)}, ${studioTranslations.language})`,
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
			.as("t");
 | 
								.as("t");
 | 
				
			||||||
		const { pk, language, ...studioTrans } = getColumns(studioTransQ);
 | 
							const { pk, language, ...studioTrans } = getColumns(studioTransQ);
 | 
				
			||||||
@ -158,13 +158,11 @@ export async function getShows({
 | 
				
			|||||||
			sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`,
 | 
								sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`,
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
		.as("t");
 | 
							.as("t");
 | 
				
			||||||
	const { pk, ...transCol } = getColumns(transQ);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return await db
 | 
						return await db
 | 
				
			||||||
		.select({
 | 
							.select({
 | 
				
			||||||
			...getColumns(shows),
 | 
								...getColumns(shows),
 | 
				
			||||||
			...transCol,
 | 
								...getColumns(transQ),
 | 
				
			||||||
			lanugage: transQ.language,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// movie columns (status is only a typescript hint)
 | 
								// movie columns (status is only a typescript hint)
 | 
				
			||||||
			status: sql<MovieStatus>`${shows.status}`,
 | 
								status: sql<MovieStatus>`${shows.status}`,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import { and, eq, exists, sql } from "drizzle-orm";
 | 
					import type { StaticDecode } from "@sinclair/typebox";
 | 
				
			||||||
 | 
					import { type SQL, and, eq, exists, sql } from "drizzle-orm";
 | 
				
			||||||
import Elysia, { t } from "elysia";
 | 
					import Elysia, { t } from "elysia";
 | 
				
			||||||
import { db } from "~/db";
 | 
					import { db } from "~/db";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@ -7,7 +8,12 @@ import {
 | 
				
			|||||||
	studioTranslations,
 | 
						studioTranslations,
 | 
				
			||||||
	studios,
 | 
						studios,
 | 
				
			||||||
} from "~/db/schema";
 | 
					} from "~/db/schema";
 | 
				
			||||||
import { getColumns, sqlarr } from "~/db/utils";
 | 
					import {
 | 
				
			||||||
 | 
						getColumns,
 | 
				
			||||||
 | 
						jsonbBuildObject,
 | 
				
			||||||
 | 
						jsonbObjectAgg,
 | 
				
			||||||
 | 
						sqlarr,
 | 
				
			||||||
 | 
					} from "~/db/utils";
 | 
				
			||||||
import { KError } from "~/models/error";
 | 
					import { KError } from "~/models/error";
 | 
				
			||||||
import { Movie } from "~/models/movie";
 | 
					import { Movie } from "~/models/movie";
 | 
				
			||||||
import { Serie } from "~/models/serie";
 | 
					import { Serie } from "~/models/serie";
 | 
				
			||||||
@ -18,11 +24,11 @@ import {
 | 
				
			|||||||
	Filter,
 | 
						Filter,
 | 
				
			||||||
	Page,
 | 
						Page,
 | 
				
			||||||
	Sort,
 | 
						Sort,
 | 
				
			||||||
 | 
						buildRelations,
 | 
				
			||||||
	createPage,
 | 
						createPage,
 | 
				
			||||||
	isUuid,
 | 
						isUuid,
 | 
				
			||||||
	keysetPaginate,
 | 
						keysetPaginate,
 | 
				
			||||||
	processLanguages,
 | 
						processLanguages,
 | 
				
			||||||
	selectTranslationQuery,
 | 
					 | 
				
			||||||
	sortToSql,
 | 
						sortToSql,
 | 
				
			||||||
} from "~/models/utils";
 | 
					} from "~/models/utils";
 | 
				
			||||||
import { desc } from "~/models/utils/descriptions";
 | 
					import { desc } from "~/models/utils/descriptions";
 | 
				
			||||||
@ -30,6 +36,83 @@ import { getShows, showFilters, showSort } from "./shows/logic";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const studioSort = Sort(["slug", "createdAt"], { default: ["slug"] });
 | 
					const studioSort = Sort(["slug", "createdAt"], { default: ["slug"] });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const studioRelations = {
 | 
				
			||||||
 | 
						translations: () => {
 | 
				
			||||||
 | 
							const { pk, language, ...trans } = getColumns(studioTranslations);
 | 
				
			||||||
 | 
							return db
 | 
				
			||||||
 | 
								.select({
 | 
				
			||||||
 | 
									json: jsonbObjectAgg(
 | 
				
			||||||
 | 
										language,
 | 
				
			||||||
 | 
										jsonbBuildObject<StudioTranslation>(trans),
 | 
				
			||||||
 | 
									).as("json"),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								.from(studioTranslations)
 | 
				
			||||||
 | 
								.where(eq(studioTranslations.pk, shows.pk))
 | 
				
			||||||
 | 
								.as("translations");
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getStudios({
 | 
				
			||||||
 | 
						after,
 | 
				
			||||||
 | 
						limit,
 | 
				
			||||||
 | 
						query,
 | 
				
			||||||
 | 
						sort,
 | 
				
			||||||
 | 
						filter,
 | 
				
			||||||
 | 
						languages,
 | 
				
			||||||
 | 
						fallbackLanguage = true,
 | 
				
			||||||
 | 
						relations = [],
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
						after?: string;
 | 
				
			||||||
 | 
						limit: number;
 | 
				
			||||||
 | 
						query?: string;
 | 
				
			||||||
 | 
						sort?: StaticDecode<typeof studioSort>;
 | 
				
			||||||
 | 
						filter?: SQL;
 | 
				
			||||||
 | 
						languages: string[];
 | 
				
			||||||
 | 
						fallbackLanguage?: boolean;
 | 
				
			||||||
 | 
						preferOriginal?: boolean;
 | 
				
			||||||
 | 
						relations?: (keyof typeof studioRelations)[];
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
						const transQ = db
 | 
				
			||||||
 | 
							.selectDistinctOn([studioTranslations.pk])
 | 
				
			||||||
 | 
							.from(studioTranslations)
 | 
				
			||||||
 | 
							.where(
 | 
				
			||||||
 | 
								!fallbackLanguage
 | 
				
			||||||
 | 
									? eq(studioTranslations.language, sql`any(${sqlarr(languages)})`)
 | 
				
			||||||
 | 
									: undefined,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.orderBy(
 | 
				
			||||||
 | 
								studioTranslations.pk,
 | 
				
			||||||
 | 
								sql`array_position(${sqlarr(languages)}, ${studioTranslations.language})`,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.as("t");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return await db
 | 
				
			||||||
 | 
							.select({
 | 
				
			||||||
 | 
								...getColumns(studios),
 | 
				
			||||||
 | 
								...getColumns(transQ),
 | 
				
			||||||
 | 
								...buildRelations(relations, studioRelations),
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							.from(studios)
 | 
				
			||||||
 | 
							[fallbackLanguage ? "innerJoin" : ("leftJoin" as "innerJoin")](
 | 
				
			||||||
 | 
								transQ,
 | 
				
			||||||
 | 
								eq(studios.pk, transQ.pk),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.where(
 | 
				
			||||||
 | 
								and(
 | 
				
			||||||
 | 
									filter,
 | 
				
			||||||
 | 
									query ? sql`${transQ.name} %> ${query}::text` : undefined,
 | 
				
			||||||
 | 
									keysetPaginate({ table: studios, after, sort }),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.orderBy(
 | 
				
			||||||
 | 
								...(query
 | 
				
			||||||
 | 
									? [sql`word_similarity(${query}::text, ${transQ.name})`]
 | 
				
			||||||
 | 
									: sortToSql(sort, studios)),
 | 
				
			||||||
 | 
								studios.pk,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.limit(limit);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
 | 
					export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
 | 
				
			||||||
	.model({
 | 
						.model({
 | 
				
			||||||
		studio: Studio,
 | 
							studio: Studio,
 | 
				
			||||||
@ -45,21 +128,12 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
 | 
				
			|||||||
			set,
 | 
								set,
 | 
				
			||||||
		}) => {
 | 
							}) => {
 | 
				
			||||||
			const langs = processLanguages(languages);
 | 
								const langs = processLanguages(languages);
 | 
				
			||||||
			const ret = await db.query.studios.findFirst({
 | 
								const [ret] = await getStudios({
 | 
				
			||||||
				where: isUuid(id) ? eq(studios.id, id) : eq(studios.slug, id),
 | 
									limit: 1,
 | 
				
			||||||
				with: {
 | 
									filter: isUuid(id) ? eq(studios.id, id) : eq(studios.slug, id),
 | 
				
			||||||
					selectedTranslation: selectTranslationQuery(
 | 
									languages: langs,
 | 
				
			||||||
						studioTranslations,
 | 
									fallbackLanguage: langs.includes("*"),
 | 
				
			||||||
						langs,
 | 
									relations,
 | 
				
			||||||
					),
 | 
					 | 
				
			||||||
					...(relations.includes("translations") && {
 | 
					 | 
				
			||||||
						translations: {
 | 
					 | 
				
			||||||
							columns: {
 | 
					 | 
				
			||||||
								pk: false,
 | 
					 | 
				
			||||||
							},
 | 
					 | 
				
			||||||
						},
 | 
					 | 
				
			||||||
					}),
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
			if (!ret) {
 | 
								if (!ret) {
 | 
				
			||||||
				return error(404, {
 | 
									return error(404, {
 | 
				
			||||||
@ -67,20 +141,14 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
 | 
				
			|||||||
					message: `No studio with the id or slug: '${id}'`,
 | 
										message: `No studio with the id or slug: '${id}'`,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			const tr = ret.selectedTranslation[0];
 | 
								if (!ret.language) {
 | 
				
			||||||
			set.headers["content-language"] = tr.language;
 | 
									return error(422, {
 | 
				
			||||||
			return {
 | 
										status: 422,
 | 
				
			||||||
				...ret,
 | 
										message: "Accept-Language header could not be satisfied.",
 | 
				
			||||||
				...tr,
 | 
									});
 | 
				
			||||||
				...(ret.translations && {
 | 
								}
 | 
				
			||||||
					translations: Object.fromEntries(
 | 
								set.headers["content-language"] = ret.language;
 | 
				
			||||||
						ret.translations.map(
 | 
								return ret;
 | 
				
			||||||
							({ language, ...translation }) =>
 | 
					 | 
				
			||||||
								[language, translation] as const,
 | 
					 | 
				
			||||||
						),
 | 
					 | 
				
			||||||
					),
 | 
					 | 
				
			||||||
				}),
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			detail: {
 | 
								detail: {
 | 
				
			||||||
@ -150,35 +218,13 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
 | 
				
			|||||||
			request: { url },
 | 
								request: { url },
 | 
				
			||||||
		}) => {
 | 
							}) => {
 | 
				
			||||||
			const langs = processLanguages(languages);
 | 
								const langs = processLanguages(languages);
 | 
				
			||||||
			const transQ = db
 | 
								const items = await getStudios({
 | 
				
			||||||
				.selectDistinctOn([studioTranslations.pk])
 | 
									limit,
 | 
				
			||||||
				.from(studioTranslations)
 | 
									after,
 | 
				
			||||||
				.orderBy(
 | 
									query,
 | 
				
			||||||
					studioTranslations.pk,
 | 
									sort,
 | 
				
			||||||
					sql`array_position(${sqlarr(langs)}, ${studioTranslations.language}`,
 | 
									languages: langs,
 | 
				
			||||||
				)
 | 
								});
 | 
				
			||||||
				.as("t");
 | 
					 | 
				
			||||||
			const { pk, ...transCol } = getColumns(transQ);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const items = await db
 | 
					 | 
				
			||||||
				.select({
 | 
					 | 
				
			||||||
					...getColumns(studios),
 | 
					 | 
				
			||||||
					...transCol,
 | 
					 | 
				
			||||||
				})
 | 
					 | 
				
			||||||
				.from(studios)
 | 
					 | 
				
			||||||
				.where(
 | 
					 | 
				
			||||||
					and(
 | 
					 | 
				
			||||||
						query ? sql`${transQ.name} %> ${query}::text` : undefined,
 | 
					 | 
				
			||||||
						keysetPaginate({ table: studios, after, sort }),
 | 
					 | 
				
			||||||
					),
 | 
					 | 
				
			||||||
				)
 | 
					 | 
				
			||||||
				.orderBy(
 | 
					 | 
				
			||||||
					...(query
 | 
					 | 
				
			||||||
						? [sql`word_similarity(${query}::text, ${transQ.name})`]
 | 
					 | 
				
			||||||
						: sortToSql(sort, studios)),
 | 
					 | 
				
			||||||
					studios.pk,
 | 
					 | 
				
			||||||
				)
 | 
					 | 
				
			||||||
				.limit(limit);
 | 
					 | 
				
			||||||
			return createPage(items, { url, sort, limit });
 | 
								return createPage(items, { url, sort, limit });
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,7 @@ export const StudioTranslation = t.Object({
 | 
				
			|||||||
	name: t.String(),
 | 
						name: t.String(),
 | 
				
			||||||
	logo: t.Nullable(Image),
 | 
						logo: t.Nullable(Image),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					export type StudioTranslation = typeof StudioTranslation.static;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Studio = t.Intersect([
 | 
					export const Studio = t.Intersect([
 | 
				
			||||||
	Resource(),
 | 
						Resource(),
 | 
				
			||||||
 | 
				
			|||||||
@ -107,19 +107,3 @@ export const AcceptLanguage = ({
 | 
				
			|||||||
		`
 | 
							`
 | 
				
			||||||
				: ""),
 | 
									: ""),
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const selectTranslationQuery = (
 | 
					 | 
				
			||||||
	translationTable: Table & { language: Column },
 | 
					 | 
				
			||||||
	languages: string[],
 | 
					 | 
				
			||||||
) => ({
 | 
					 | 
				
			||||||
	columns: {
 | 
					 | 
				
			||||||
		pk: false,
 | 
					 | 
				
			||||||
	} as const,
 | 
					 | 
				
			||||||
	where: !languages.includes("*")
 | 
					 | 
				
			||||||
		? eq(translationTable.language, sql`any(${sqlarr(languages)})`)
 | 
					 | 
				
			||||||
		: undefined,
 | 
					 | 
				
			||||||
	orderBy: [
 | 
					 | 
				
			||||||
		sql`array_position(${sqlarr(languages)}, ${translationTable.language})`,
 | 
					 | 
				
			||||||
	],
 | 
					 | 
				
			||||||
	limit: 1,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,7 @@ import { Language } from "./language";
 | 
				
			|||||||
export const Original = t.Object({
 | 
					export const Original = t.Object({
 | 
				
			||||||
	language: Language({
 | 
						language: Language({
 | 
				
			||||||
		description: "The language code this was made in.",
 | 
							description: "The language code this was made in.",
 | 
				
			||||||
		examples: ["ja"]
 | 
							examples: ["ja"],
 | 
				
			||||||
	}),
 | 
						}),
 | 
				
			||||||
	name: t.String({
 | 
						name: t.String({
 | 
				
			||||||
		description: "The name in the original language",
 | 
							description: "The name in the original language",
 | 
				
			||||||
 | 
				
			|||||||
@ -8,12 +8,12 @@ export const buildRelations = <
 | 
				
			|||||||
>(
 | 
					>(
 | 
				
			||||||
	enabled: R[],
 | 
						enabled: R[],
 | 
				
			||||||
	relations: Rel,
 | 
						relations: Rel,
 | 
				
			||||||
	params: P,
 | 
						params?: P,
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
	// we wrap that in a sql`` instead of using the builder because of this issue
 | 
						// we wrap that in a sql`` instead of using the builder because of this issue
 | 
				
			||||||
	// https://github.com/drizzle-team/drizzle-orm/pull/1674
 | 
						// https://github.com/drizzle-team/drizzle-orm/pull/1674
 | 
				
			||||||
	return Object.fromEntries(
 | 
						return Object.fromEntries(
 | 
				
			||||||
		enabled.map((x) => [x, sql`${relations[x](params)}`]),
 | 
							enabled.map((x) => [x, sql`${relations[x](params!)}`]),
 | 
				
			||||||
	) as {
 | 
						) as {
 | 
				
			||||||
		[P in R]?: SQL<
 | 
							[P in R]?: SQL<
 | 
				
			||||||
			ReturnType<Rel[P]>["_"]["selectedFields"] extends {
 | 
								ReturnType<Rel[P]>["_"]["selectedFields"] extends {
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,7 @@ beforeAll(async () => {
 | 
				
			|||||||
	await db.delete(shows);
 | 
						await db.delete(shows);
 | 
				
			||||||
	await db.insert(videos).values(bubbleVideo);
 | 
						await db.insert(videos).values(bubbleVideo);
 | 
				
			||||||
	const [ret, body] = await createMovie(bubble);
 | 
						const [ret, body] = await createMovie(bubble);
 | 
				
			||||||
	expect(ret.status).toBe(201)
 | 
						expect(ret.status).toBe(201);
 | 
				
			||||||
	bubbleId = body.id;
 | 
						bubbleId = body.id;
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user