From 5ae04c6dac114f08f809992b6bb36f82d29e4f36 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 10 Mar 2025 19:32:09 +0100 Subject: [PATCH] Add `with=firstEntry` --- api/src/controllers/shows/logic.ts | 50 ++++++++++++++++++++++++++++- api/src/controllers/shows/series.ts | 2 +- api/src/models/serie.ts | 3 +- api/tests/series/get-series.test.ts | 13 ++++++++ 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/api/src/controllers/shows/logic.ts b/api/src/controllers/shows/logic.ts index 6b9483c2..4d743df2 100644 --- a/api/src/controllers/shows/logic.ts +++ b/api/src/controllers/shows/logic.ts @@ -1,7 +1,8 @@ -import { type SQL, and, eq, exists, sql } from "drizzle-orm"; +import { type SQL, and, eq, exists, ne, sql } from "drizzle-orm"; import { db } from "~/db"; import { entries, + entryTranslations, entryVideoJoin, showStudioJoin, showTranslations, @@ -18,6 +19,7 @@ import { jsonbObjectAgg, sqlarr, } from "~/db/utils"; +import type { Entry } from "~/models/entry"; import type { MovieStatus } from "~/models/movie"; import { SerieStatus, type SerieTranslation } from "~/models/serie"; import type { Studio } from "~/models/studio"; @@ -142,6 +144,52 @@ const showRelations = { .leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk)) .as("videos"); }, + firstEntry: ({ languages }: { languages: string[] }) => { + const transQ = db + .selectDistinctOn([entryTranslations.pk]) + .from(entryTranslations) + .orderBy( + entryTranslations.pk, + sql`array_position(${sqlarr(languages)}, ${entryTranslations.language})`, + ) + .as("t"); + const { pk, ...transCol } = getColumns(transQ); + + const { guess, createdAt, updatedAt, ...videosCol } = getColumns(videos); + const videosQ = db + .select({ + videos: coalesce( + jsonbAgg( + jsonbBuildObject({ + slug: entryVideoJoin.slug, + ...videosCol, + }), + ), + sql`'[]'::jsonb`, + ).as("videos"), + }) + .from(entryVideoJoin) + .where(eq(entryVideoJoin.entryPk, entries.pk)) + .leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk)) + .as("videos"); + + return db + .select({ + firstEntry: jsonbBuildObject({ + ...getColumns(entries), + ...transCol, + number: entries.episodeNumber, + videos: videosQ.videos, + }).as("firstEntry"), + }) + .from(entries) + .innerJoin(transQ, eq(entries.pk, transQ.pk)) + .leftJoinLateral(videosQ, sql`true`) + .where(and(eq(entries.showPk, shows.pk), ne(entries.kind, "extra"))) + .orderBy(entries.order) + .limit(1) + .as("firstEntry"); + }, }; export async function getShows({ diff --git a/api/src/controllers/shows/series.ts b/api/src/controllers/shows/series.ts index 779ae39a..5aad0163 100644 --- a/api/src/controllers/shows/series.ts +++ b/api/src/controllers/shows/series.ts @@ -71,7 +71,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] }) preferOriginal: t.Optional( t.Boolean({ description: desc.preferOriginal }), ), - with: t.Array(t.UnionEnum(["translations", "studios"]), { + with: t.Array(t.UnionEnum(["translations", "studios", "firstEntry"]), { default: [], description: "Include related resources in the response.", }), diff --git a/api/src/models/serie.ts b/api/src/models/serie.ts index 3bafdb67..1b664bbc 100644 --- a/api/src/models/serie.ts +++ b/api/src/models/serie.ts @@ -1,7 +1,7 @@ import { t } from "elysia"; import type { Prettify } from "~/utils"; import { SeedCollection } from "./collections"; -import { SeedEntry, SeedExtra } from "./entry"; +import { Entry, SeedEntry, SeedExtra } from "./entry"; import { bubbleImages, madeInAbyss, registerExamples } from "./examples"; import { SeedSeason } from "./season"; import { SeedStaff } from "./staff"; @@ -79,6 +79,7 @@ export const FullSerie = t.Intersect([ t.Object({ translations: t.Optional(TranslationRecord(SerieTranslation)), studios: t.Optional(t.Array(Studio)), + firstEntry: t.Optional(Entry), }), ]); export type FullMovie = Prettify; diff --git a/api/tests/series/get-series.test.ts b/api/tests/series/get-series.test.ts index 2cdfb3c7..426ce77a 100644 --- a/api/tests/series/get-series.test.ts +++ b/api/tests/series/get-series.test.ts @@ -29,4 +29,17 @@ describe("Get series", () => { expect(body.entriesCount).toBe(madeInAbyss.entries.length); expect(body.availableCount).toBe(1); }); + it("With firstEntry", async () => { + const [resp, body] = await getSerie(madeInAbyss.slug, { + langs: "en", + with: ["firstEntry"], + }); + + expectStatus(resp, body).toBe(200); + expect(body.firstEntry.slug).toBe("made-in-abyss-s1e13"); + expect(body.firstEntry.name).toBe( + madeInAbyss.entries[0].translations.en.name, + ); + expect(body.firstEntry.videos).toBeArrayOfSize(1); + }); });