diff --git a/api/src/controllers/shows/logic.ts b/api/src/controllers/shows/logic.ts index 23eda8e7..d32740ed 100644 --- a/api/src/controllers/shows/logic.ts +++ b/api/src/controllers/shows/logic.ts @@ -89,7 +89,7 @@ const showRelations = { .from(studioTranslations) .orderBy( studioTranslations.pk, - sql`array_position(${sqlarr(languages)}, ${studioTranslations.language}`, + sql`array_position(${sqlarr(languages)}, ${studioTranslations.language})`, ) .as("t"); const { pk, language, ...studioTrans } = getColumns(studioTransQ); @@ -158,13 +158,11 @@ export async function getShows({ sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`, ) .as("t"); - const { pk, ...transCol } = getColumns(transQ); return await db .select({ ...getColumns(shows), - ...transCol, - lanugage: transQ.language, + ...getColumns(transQ), // movie columns (status is only a typescript hint) status: sql`${shows.status}`, diff --git a/api/src/controllers/studios.ts b/api/src/controllers/studios.ts index a9e9580c..68b3a3d3 100644 --- a/api/src/controllers/studios.ts +++ b/api/src/controllers/studios.ts @@ -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 { db } from "~/db"; import { @@ -7,7 +8,12 @@ import { studioTranslations, studios, } from "~/db/schema"; -import { getColumns, sqlarr } from "~/db/utils"; +import { + getColumns, + jsonbBuildObject, + jsonbObjectAgg, + sqlarr, +} from "~/db/utils"; import { KError } from "~/models/error"; import { Movie } from "~/models/movie"; import { Serie } from "~/models/serie"; @@ -18,11 +24,11 @@ import { Filter, Page, Sort, + buildRelations, createPage, isUuid, keysetPaginate, processLanguages, - selectTranslationQuery, sortToSql, } from "~/models/utils"; 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 studioRelations = { + translations: () => { + const { pk, language, ...trans } = getColumns(studioTranslations); + return db + .select({ + json: jsonbObjectAgg( + language, + jsonbBuildObject(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; + 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"] }) .model({ studio: Studio, @@ -45,21 +128,12 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] }) set, }) => { const langs = processLanguages(languages); - const ret = await db.query.studios.findFirst({ - where: isUuid(id) ? eq(studios.id, id) : eq(studios.slug, id), - with: { - selectedTranslation: selectTranslationQuery( - studioTranslations, - langs, - ), - ...(relations.includes("translations") && { - translations: { - columns: { - pk: false, - }, - }, - }), - }, + const [ret] = await getStudios({ + limit: 1, + filter: isUuid(id) ? eq(studios.id, id) : eq(studios.slug, id), + languages: langs, + fallbackLanguage: langs.includes("*"), + relations, }); if (!ret) { 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}'`, }); } - const tr = ret.selectedTranslation[0]; - set.headers["content-language"] = tr.language; - return { - ...ret, - ...tr, - ...(ret.translations && { - translations: Object.fromEntries( - ret.translations.map( - ({ language, ...translation }) => - [language, translation] as const, - ), - ), - }), - }; + if (!ret.language) { + return error(422, { + status: 422, + message: "Accept-Language header could not be satisfied.", + }); + } + set.headers["content-language"] = ret.language; + return ret; }, { detail: { @@ -150,35 +218,13 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] }) request: { url }, }) => { const langs = processLanguages(languages); - const transQ = db - .selectDistinctOn([studioTranslations.pk]) - .from(studioTranslations) - .orderBy( - studioTranslations.pk, - sql`array_position(${sqlarr(langs)}, ${studioTranslations.language}`, - ) - .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); + const items = await getStudios({ + limit, + after, + query, + sort, + languages: langs, + }); return createPage(items, { url, sort, limit }); }, { diff --git a/api/src/models/studio.ts b/api/src/models/studio.ts index 53d7ccdb..c7b8beff 100644 --- a/api/src/models/studio.ts +++ b/api/src/models/studio.ts @@ -12,6 +12,7 @@ export const StudioTranslation = t.Object({ name: t.String(), logo: t.Nullable(Image), }); +export type StudioTranslation = typeof StudioTranslation.static; export const Studio = t.Intersect([ Resource(), diff --git a/api/src/models/utils/language.ts b/api/src/models/utils/language.ts index 2f902d63..0de608b0 100644 --- a/api/src/models/utils/language.ts +++ b/api/src/models/utils/language.ts @@ -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, -}); diff --git a/api/src/models/utils/original.ts b/api/src/models/utils/original.ts index 5e51b11f..e7f18821 100644 --- a/api/src/models/utils/original.ts +++ b/api/src/models/utils/original.ts @@ -5,7 +5,7 @@ import { Language } from "./language"; export const Original = t.Object({ language: Language({ description: "The language code this was made in.", - examples: ["ja"] + examples: ["ja"], }), name: t.String({ description: "The name in the original language", diff --git a/api/src/models/utils/relations.ts b/api/src/models/utils/relations.ts index 1e5088a7..2b800024 100644 --- a/api/src/models/utils/relations.ts +++ b/api/src/models/utils/relations.ts @@ -8,12 +8,12 @@ export const buildRelations = < >( enabled: R[], relations: Rel, - params: P, + params?: P, ) => { // we wrap that in a sql`` instead of using the builder because of this issue // https://github.com/drizzle-team/drizzle-orm/pull/1674 return Object.fromEntries( - enabled.map((x) => [x, sql`${relations[x](params)}`]), + enabled.map((x) => [x, sql`${relations[x](params!)}`]), ) as { [P in R]?: SQL< ReturnType["_"]["selectedFields"] extends { diff --git a/api/tests/movies/get-movie.test.ts b/api/tests/movies/get-movie.test.ts index 687e0dd8..4b5d6252 100644 --- a/api/tests/movies/get-movie.test.ts +++ b/api/tests/movies/get-movie.test.ts @@ -11,7 +11,7 @@ beforeAll(async () => { await db.delete(shows); await db.insert(videos).values(bubbleVideo); const [ret, body] = await createMovie(bubble); - expect(ret.status).toBe(201) + expect(ret.status).toBe(201); bubbleId = body.id; });