diff --git a/api/src/controllers/shows/logic.ts b/api/src/controllers/shows/logic.ts index d8548fd8..ae783dcd 100644 --- a/api/src/controllers/shows/logic.ts +++ b/api/src/controllers/shows/logic.ts @@ -2,7 +2,12 @@ import type { StaticDecode } from "@sinclair/typebox"; import { type SQL, and, eq, sql } from "drizzle-orm"; import { db } from "~/db"; import { showTranslations, shows, studioTranslations } from "~/db/schema"; -import { getColumns, sqlarr } from "~/db/utils"; +import { + getColumns, + jsonbBuildObject, + jsonbObjectAgg, + sqlarr, +} from "~/db/utils"; import type { MovieStatus } from "~/models/movie"; import { SerieStatus } from "~/models/serie"; import { @@ -55,6 +60,16 @@ export const showSort = Sort( }, ); +const buildRelations = ( + relations: R[], + toSql: (relation: R) => SQL, +) => { + return Object.fromEntries(relations.map((x) => [x, toSql(x)])) as Record< + R, + SQL + >; +}; + export async function getShows({ after, limit, @@ -64,15 +79,17 @@ export async function getShows({ languages, fallbackLanguage = true, preferOriginal = false, + relations = [], }: { - after: string | undefined; + after?: string; limit: number; - query: string | undefined; - sort: StaticDecode; - filter: SQL | undefined; + query?: string; + sort?: StaticDecode; + filter?: SQL; languages: string[]; fallbackLanguage?: boolean; preferOriginal?: boolean; + relations?: ("translations" | "studios" | "videos")[]; }) { const transQ = db .selectDistinctOn([showTranslations.pk]) @@ -89,6 +106,22 @@ export async function getShows({ .as("t"); const { pk, ...transCol } = getColumns(transQ); + const relationsSql = buildRelations(relations, (x) => { + switch (x) { + case "studios": + case "videos": + case "translations": { + // we wrap that in a sql`` instead of using the builder because of this issue + // https://github.com/drizzle-team/drizzle-orm/pull/1674 + const { pk, language, ...trans } = getColumns(showTranslations); + return sql`${db + .select({ json: jsonbObjectAgg(language, jsonbBuildObject(trans)) }) + .from(showTranslations) + .where(eq(showTranslations.pk, shows.pk))}`; + } + } + }); + return await db .select({ ...getColumns(shows), @@ -107,6 +140,8 @@ export async function getShows({ banner: sql`coalesce(nullif(${shows.original}->'banner', 'null'::jsonb), ${transQ.banner})`, logo: sql`coalesce(nullif(${shows.original}->'logo', 'null'::jsonb), ${transQ.logo})`, }), + + ...relationsSql, }) .from(shows) [fallbackLanguage ? "innerJoin" : "leftJoin"]( diff --git a/api/src/controllers/shows/movies.ts b/api/src/controllers/shows/movies.ts index b0f4828a..e1bb197d 100644 --- a/api/src/controllers/shows/movies.ts +++ b/api/src/controllers/shows/movies.ts @@ -32,6 +32,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) }) => { const langs = processLanguages(languages); const [ret] = await getShows({ + limit: 1, filter: and( isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), eq(shows.kind, "movie"), diff --git a/api/src/controllers/shows/series.ts b/api/src/controllers/shows/series.ts index f84a4d5a..8af3a9d3 100644 --- a/api/src/controllers/shows/series.ts +++ b/api/src/controllers/shows/series.ts @@ -10,6 +10,7 @@ import { Filter, Page, createPage, + isUuid, processLanguages, } from "~/models/utils"; import { desc } from "~/models/utils/descriptions"; @@ -30,11 +31,16 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] }) set, }) => { const langs = processLanguages(languages); - const ret = await getShow(id, { + const [ret] = await getShows({ + limit: 1, + filter: and( + isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), + eq(shows.kind, "serie"), + ), languages: langs, + fallbackLanguage: langs.includes("*"), preferOriginal, relations, - filters: eq(shows.kind, "serie"), }); if (!ret) { return error(404, { @@ -49,7 +55,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] }) }); } set.headers["content-language"] = ret.language; - return ret.show; + return ret; }, { detail: { diff --git a/api/src/db/utils.ts b/api/src/db/utils.ts index f2a4947a..f2fe23df 100644 --- a/api/src/db/utils.ts +++ b/api/src/db/utils.ts @@ -1,6 +1,7 @@ import { type ColumnsSelection, type SQL, + type SQLWrapper, type Subquery, Table, View, @@ -92,3 +93,17 @@ export function values(items: Record[]) { }, }; } + +export const jsonbObjectAgg = (key: SQLWrapper, value: SQLWrapper) => { + return sql`jsonb_object_agg(${sql.join([key, value], sql.raw(","))})`; +}; + +export const jsonbBuildObject = (select: Record) => { + const query = sql.join( + Object.entries(select).flatMap(([k, v]) => { + return [sql.raw(`'${k}'`), v]; + }), + sql.raw(", "), + ); + return sql`jsonb_build_object(${query})`; +}; diff --git a/api/src/models/examples/made-in-abyss.ts b/api/src/models/examples/made-in-abyss.ts index 01991059..36e16dd8 100644 --- a/api/src/models/examples/made-in-abyss.ts +++ b/api/src/models/examples/made-in-abyss.ts @@ -60,6 +60,40 @@ export const madeInAbyss = { banner: null, trailerUrl: "https://www.youtube.com/watch?v=ePOyy6Wlk4s", }, + ja: { + name: "メイドインアビス", + tagline: "さぁ 大穴(アビス)へ――", + aliases: ["烈日の黄金郷"], + description: + "隅々まで探索されつくした世界に、唯一残された秘境の大穴『アビス』。どこまで続くとも知れない深く巨大なその縦穴には、奇妙奇怪な生物たちが生息し、今の人類では作りえない貴重な遺物が眠っている。「アビス」の不可思議に満ちた姿は人々を魅了し、冒険へと駆り立てた。そうして幾度も大穴に挑戦する冒険者たちは、次第に『探窟家』と呼ばれるようになっていった。 アビスの縁に築かれた街『オース』に暮らす孤児のリコは、いつか母のような偉大な探窟家になり、アビスの謎を解き明かすことを夢見ていた。そんなある日、リコはアビスを探窟中に、少年の姿をしたロボットを拾い…?", + tags: [ + "android", + "amnesia", + "post-apocalyptic future", + "exploration", + "friendship", + "mecha", + "survival", + "curse", + "tragedy", + "orphan", + "based on manga", + "robot", + "dark fantasy", + "seinen", + "anime", + "drastic change of life", + "fantasy", + "adventure", + ], + poster: + "https://image.tmdb.org/t/p/original/4Bh9qzB1Kau4RDaVQXVFdoJ0HcE.jpg", + thumbnail: + "https://image.tmdb.org/t/p/original/Df9XrvZFIeQfLKfu8evRmzvRsd.jpg", + logo: "https://image.tmdb.org/t/p/original/7hY3Q4GhkiYPBfn4UoVg0AO4Zgk.png", + banner: null, + trailerUrl: "https://www.youtube.com/watch?v=ePOyy6Wlk4s", + }, }, genres: [ "animation", diff --git a/api/src/models/utils/keyset-paginate.ts b/api/src/models/utils/keyset-paginate.ts index 76fd33bb..0307367f 100644 --- a/api/src/models/utils/keyset-paginate.ts +++ b/api/src/models/utils/keyset-paginate.ts @@ -26,9 +26,9 @@ export const keysetPaginate = < }: { table: Table<"pk" | Sort["sort"][number]["key"]>; after: string | undefined; - sort: Sort; + sort: Sort | undefined; }) => { - if (!after) return undefined; + if (!after || !sort) return undefined; const cursor: After = JSON.parse( Buffer.from(after, "base64").toString("utf-8"), ); diff --git a/api/tests/manual.ts b/api/tests/manual.ts index 0fcd9d2f..3835aa2c 100644 --- a/api/tests/manual.ts +++ b/api/tests/manual.ts @@ -1,7 +1,7 @@ import { db, migrate } from "~/db"; import { shows, videos } from "~/db/schema"; import { madeInAbyss, madeInAbyssVideo } from "~/models/examples"; -import { createSerie, createVideo } from "./helpers"; +import { createSerie, createVideo, getSerie } from "./helpers"; // test file used to run manually using `bun tests/manual.ts` @@ -13,3 +13,5 @@ const [_, vid] = await createVideo(madeInAbyssVideo); console.log(vid); const [__, ser] = await createSerie(madeInAbyss); console.log(ser); +const [___, got] = await getSerie(madeInAbyss.slug, { with: ["translations"] }); +console.log(got);