From c730d47cd993e765638facbad6aa897eed1f9b25 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 1 Feb 2025 19:12:13 +0100 Subject: [PATCH] Add entries controller --- api/src/controllers/entries.ts | 184 +++++++++++++++++++++++++++++++-- api/src/elysia.ts | 4 +- 2 files changed, 180 insertions(+), 8 deletions(-) diff --git a/api/src/controllers/entries.ts b/api/src/controllers/entries.ts index 90ca27cf..b828cf23 100644 --- a/api/src/controllers/entries.ts +++ b/api/src/controllers/entries.ts @@ -1,14 +1,137 @@ +import type { StaticDecode } from "@sinclair/typebox"; +import { type SQL, and, eq, ne, sql } from "drizzle-orm"; import { Elysia, t } from "elysia"; +import { db } from "~/db"; +import { entries, entryTranslations, shows } from "~/db/schema"; +import { getColumns, sqlarr } from "~/db/utils"; import { - type Entry, + Entry, Episode, Extra, + ExtraType, MovieEntry, Special, UnknownEntry, -} from "../models/entry"; +} from "~/models/entry"; +import { KError } from "~/models/error"; +import { madeInAbyss } from "~/models/examples"; +import { + AcceptLanguage, + Filter, + type FilterDef, + Page, + Sort, + createPage, + isUuid, + keysetPaginate, + processLanguages, + sortToSql, +} from "~/models/utils"; +import { desc } from "~/models/utils/descriptions"; -export const entries = new Elysia() +const entryFilters: FilterDef = { + kind: { + column: entries.kind, + type: "enum", + values: ["episode", "movie", "special"], + }, + seasonNumber: { column: entries.seasonNumber, type: "int" }, + episodeNumber: { column: entries.episodeNumber, type: "int" }, + number: { column: entries.episodeNumber, type: "int" }, + order: { column: entries.order, type: "float" }, + runtime: { column: entries.runtime, type: "float" }, + airDate: { column: entries.airDate, type: "date" }, +}; + +const extraFilters: FilterDef = { + kind: { column: entries.extraKind, type: "enum", values: ExtraType.enum }, + runtime: { column: entries.runtime, type: "float" }, +}; + +const entrySort = Sort( + [ + "order", + "seasonNumber", + "episodeNumber", + "number", + "airDate", + "nextRefresh", + ], + { + default: ["order"], + remap: { + number: "episodeNumber", + }, + }, +); + +async function getEntries( + serie: string, + { + after, + limit, + query, + sort, + filter, + languages, + }: { + after: string | undefined; + limit: number; + query: string | undefined; + sort: StaticDecode; + filter: SQL | undefined; + languages: string[]; + }, +) { + const show = db.$with("serie").as( + db + .select({ pk: shows.pk }) + .from(shows) + .where( + and( + eq(shows.kind, "serie"), + isUuid(serie) ? eq(shows.id, serie) : eq(shows.slug, serie), + ), + ) + .limit(1), + ); + + 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); + + return await db + .with(show) + .select({ + ...getColumns(entries), + ...transCol, + }) + .from(entries) + .innerJoin(transQ, eq(entries.pk, transQ.pk)) + .where( + and( + eq(entries.showPk, show.pk), + filter, + query ? sql`${transQ.name} %> ${query}::text` : undefined, + keysetPaginate({ table: entries, after, sort }), + ), + ) + .orderBy( + ...(query + ? [sql`word_similarity(${query}::text, ${transQ.name})`] + : sortToSql(sort, entries)), + entries.pk, + ) + .limit(limit); +} + +export const entriesH = new Elysia() .model({ episode: Episode, movie_entry: MovieEntry, @@ -21,9 +144,58 @@ export const entries = new Elysia() ...models, entry: t.Union([models.episode, models.movie_entry, models.special]), })) - .get("/entries/:id", () => "hello" as unknown as Entry, { - response: { 200: "entry" }, - }) + .get( + "/series/:id/entries", + async ({ + params: { id }, + query: { limit, after, query, sort, filter }, + headers: { "accept-language": languages }, + request: { url }, + }) => { + const langs = processLanguages(languages); + const items = await getEntries(id, { + limit, + after, + query, + sort, + filter: and( + ne(entries.kind, "extra"), + ne(entries.kind, "unknown"), + filter, + ), + languages: langs, + }); + return createPage(items, { url, sort, limit }); + }, + { + detail: { description: "Get entries of a serie" }, + path: t.Object({ + id: t.String({ + description: "The id or slug of the serie.", + examples: [madeInAbyss.slug], + }), + }), + query: t.Object({ + sort: entrySort, + filter: t.Optional(Filter({ def: entryFilters })), + query: t.Optional(t.String({ description: desc.query })), + limit: t.Integer({ + minimum: 1, + maximum: 250, + default: 50, + description: "Max page size.", + }), + after: t.Optional(t.String({ description: desc.after })), + }), + headers: t.Object({ + "accept-language": AcceptLanguage({ autoFallback: true }), + }), + response: { + 200: Page(Entry), + 422: KError, + }, + }, + ) .get("/extras/:id", () => "hello" as unknown as Extra, { response: { 200: "extra" }, }) diff --git a/api/src/elysia.ts b/api/src/elysia.ts index 68eb2cd3..07152bcc 100644 --- a/api/src/elysia.ts +++ b/api/src/elysia.ts @@ -1,5 +1,5 @@ import { Elysia } from "elysia"; -import { entries } from "./controllers/entries"; +import { entriesH } from "./controllers/entries"; import { movies } from "./controllers/movies"; import { seasonsH } from "./controllers/seasons"; import { seed } from "./controllers/seed"; @@ -42,7 +42,7 @@ export const app = new Elysia() .use(base) .use(movies) .use(series) - .use(entries) + .use(entriesH) .use(seasonsH) .use(videosH) .use(seed);