From 44658ce6b0b4566abc39a6850656cb911428482a Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 3 Mar 2025 10:01:41 +0100 Subject: [PATCH] Add /studios/:id --- api/src/controllers/studios.ts | 104 ++++++++++++++++++++++++++--- api/src/db/schema/studios.ts | 36 +++++++++- api/tests/helpers/index.ts | 1 + api/tests/helpers/studio-helper.ts | 18 +++++ api/tests/series/studios.test.ts | 21 +++++- 5 files changed, 170 insertions(+), 10 deletions(-) diff --git a/api/src/controllers/studios.ts b/api/src/controllers/studios.ts index 166ca754..f387117a 100644 --- a/api/src/controllers/studios.ts +++ b/api/src/controllers/studios.ts @@ -1,8 +1,16 @@ -import { and, eq, exists } from "drizzle-orm"; +import { and, eq, exists, sql } from "drizzle-orm"; import Elysia, { t } from "elysia"; import { db } from "~/db"; -import { showStudioJoin, shows, studios } from "~/db/schema"; +import { + showStudioJoin, + shows, + studioTranslations, + studios, +} from "~/db/schema"; +import { sqlarr } from "~/db/utils"; import { KError } from "~/models/error"; +import { Movie } from "~/models/movie"; +import { Serie } from "~/models/serie"; import { Show } from "~/models/show"; import { Studio, StudioTranslation } from "~/models/studio"; import { @@ -15,14 +23,94 @@ import { } from "~/models/utils"; import { desc } from "~/models/utils/descriptions"; import { getShows, showFilters, showSort } from "./shows/logic"; -import { Serie } from "~/models/serie"; -import { Movie } from "~/models/movie"; -export const studiosH = new Elysia({ tags: ["studios"] }) +export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] }) .model({ studio: Studio, "studio-translation": StudioTranslation, }) + .get( + "/:id", + async ({ + params: { id }, + headers: { "accept-language": languages }, + query: { with: relations }, + error, + 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: { + columns: { pk: false }, + where: !languages.includes("*") + ? eq(studioTranslations.language, sql`any(${sqlarr(langs)})`) + : undefined, + orderBy: [ + sql`array_position(${sqlarr(langs)}, ${studioTranslations.language})`, + ], + limit: 1, + }, + ...(relations.includes("translations") && { + translations: { + columns: { + pk: false, + }, + }, + }), + }, + }); + if (!ret) { + return error(404, { + status: 404, + 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, + ), + ), + }), + }; + }, + { + detail: { + description: "Get a studio by id or slug", + }, + params: t.Object({ + id: t.String({ + description: "The id or slug of the collection to retrieve.", + example: "mappa", + }), + }), + query: t.Object({ + with: t.Array(t.UnionEnum(["translations"]), { + default: [], + description: "Include related resources in the response.", + }), + }), + headers: t.Object({ + "accept-language": AcceptLanguage(), + }), + response: { + 200: { ...Studio, description: "Found" }, + 404: { + ...KError, + description: "No collection found with the given id or slug.", + }, + 422: KError, + }, + }, + ) .guard({ params: t.Object({ id: t.String({ @@ -52,7 +140,7 @@ export const studiosH = new Elysia({ tags: ["studios"] }) }), }) .get( - "/studios/:id/shows", + "/:id/shows", async ({ params: { id }, query: { limit, after, query, sort, filter, preferOriginal }, @@ -111,7 +199,7 @@ export const studiosH = new Elysia({ tags: ["studios"] }) }, ) .get( - "/studios/:id/movies", + "/:id/movies", async ({ params: { id }, query: { limit, after, query, sort, filter, preferOriginal }, @@ -171,7 +259,7 @@ export const studiosH = new Elysia({ tags: ["studios"] }) }, ) .get( - "/studios/:id/series", + "/:id/series", async ({ params: { id }, query: { limit, after, query, sort, filter, preferOriginal }, diff --git a/api/src/db/schema/studios.ts b/api/src/db/schema/studios.ts index 7c0dd7d7..70549b8d 100644 --- a/api/src/db/schema/studios.ts +++ b/api/src/db/schema/studios.ts @@ -1,4 +1,4 @@ -import { sql } from "drizzle-orm"; +import { relations, sql } from "drizzle-orm"; import { index, integer, @@ -53,3 +53,37 @@ export const showStudioJoin = schema.table( }, (t) => [primaryKey({ columns: [t.show, t.studio] })], ); + +export const studioRelations = relations(studios, ({ many }) => ({ + translations: many(studioTranslations, { + relationName: "studio_translations", + }), + selectedTranslation: many(studioTranslations, { + relationName: "studio_selected_translation", + }), + showsJoin: many(showStudioJoin, { relationName: "show_studios" }), +})); +export const studioTrRelations = relations(studioTranslations, ({ one }) => ({ + studio: one(studios, { + relationName: "studio_translations", + fields: [studioTranslations.pk], + references: [studios.pk], + }), + selectedTranslation: one(studios, { + relationName: "studio_selected_translation", + fields: [studioTranslations.pk], + references: [studios.pk], + }), +})); +export const ssjRelations = relations(showStudioJoin, ({ one }) => ({ + show: one(shows, { + relationName: "ssj_show", + fields: [showStudioJoin.show], + references: [shows.pk], + }), + studio: one(studios, { + relationName: "ssj_studio", + fields: [showStudioJoin.studio], + references: [studios.pk], + }), +})); diff --git a/api/tests/helpers/index.ts b/api/tests/helpers/index.ts index 62e4bb29..e051fdff 100644 --- a/api/tests/helpers/index.ts +++ b/api/tests/helpers/index.ts @@ -1,5 +1,6 @@ export * from "./movies-helper"; export * from "./series-helper"; +export * from "./studio-helper"; export * from "./videos-helper"; export * from "~/elysia"; diff --git a/api/tests/helpers/studio-helper.ts b/api/tests/helpers/studio-helper.ts index 42f80160..fb9fe255 100644 --- a/api/tests/helpers/studio-helper.ts +++ b/api/tests/helpers/studio-helper.ts @@ -1,6 +1,24 @@ import { buildUrl } from "tests/utils"; import { app } from "~/elysia"; +export const getStudio = async ( + id: string, + { langs, ...query }: { langs?: string; preferOriginal?: boolean }, +) => { + const resp = await app.handle( + new Request(buildUrl(`studios/${id}`, query), { + method: "GET", + headers: langs + ? { + "Accept-Language": langs, + } + : {}, + }), + ); + const body = await resp.json(); + return [resp, body] as const; +}; + export const getShowsByStudio = async ( studio: string, { diff --git a/api/tests/series/studios.test.ts b/api/tests/series/studios.test.ts index 4d2b6c21..6bbf5d8e 100644 --- a/api/tests/series/studios.test.ts +++ b/api/tests/series/studios.test.ts @@ -1,5 +1,5 @@ import { beforeAll, describe, expect, it } from "bun:test"; -import { getShowsByStudio } from "tests/helpers/studio-helper"; +import { getShowsByStudio, getStudio } from "tests/helpers"; import { expectStatus } from "tests/utils"; import { seedSerie } from "~/controllers/seed/series"; import { madeInAbyss } from "~/models/examples"; @@ -28,3 +28,22 @@ describe("Get by studio", () => { expect(body.items[0].slug).toBe(madeInAbyss.slug); }); }); + +describe("Get a studio", () => { + it("Invalid slug", async () => { + const [resp, body] = await getStudio("sotneuhn", { langs: "en" }); + + expectStatus(resp, body).toBe(404); + expect(body).toMatchObject({ + status: 404, + message: expect.any(String), + }); + }); + it("Get by id", async () => { + const slug = madeInAbyss.studios[0].slug; + const [resp, body] = await getStudio(slug, { langs: "en" }); + + expectStatus(resp, body).toBe(200); + expect(body.slug).toBe(slug); + }); +});