From cba47ca346083617d967f3a891c72f588bb8fe11 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 9 Mar 2025 21:17:38 +0100 Subject: [PATCH] Add /staff/:id/roles --- api/src/controllers/staff.ts | 129 ++++++++++++++++++++++++++++++++++- api/src/models/staff.ts | 17 +++++ 2 files changed, 144 insertions(+), 2 deletions(-) diff --git a/api/src/controllers/staff.ts b/api/src/controllers/staff.ts index f7581545..fc50881f 100644 --- a/api/src/controllers/staff.ts +++ b/api/src/controllers/staff.ts @@ -1,21 +1,38 @@ import { and, eq, sql } from "drizzle-orm"; import Elysia, { t } from "elysia"; import { db } from "~/db"; -import { staff } from "~/db/schema/staff"; +import { showTranslations, shows } from "~/db/schema"; +import { roles, staff } from "~/db/schema/staff"; +import { getColumns, sqlarr } from "~/db/utils"; import { KError } from "~/models/error"; -import { Role, Staff } from "~/models/staff"; +import type { MovieStatus } from "~/models/movie"; +import { Role, RoleWShow, Staff } from "~/models/staff"; import { + Filter, + type FilterDef, + type Image, Page, Sort, createPage, isUuid, keysetPaginate, + processLanguages, sortToSql, } from "~/models/utils"; import { desc } from "~/models/utils/descriptions"; +import { showFilters, showSort } from "./shows/logic"; const staffSort = Sort(["slug", "name", "latinName"], { default: ["slug"] }); +const roleShowFilters: FilterDef = { + kind: { + column: roles.kind, + type: "enum", + values: Role.properties.kind.enum, + }, + ...showFilters, +}; + export const staffH = new Elysia({ tags: ["staff"] }) .model({ staff: Staff, @@ -87,6 +104,114 @@ export const staffH = new Elysia({ tags: ["staff"] }) }, }, ) + .get( + "/staff/:id/roles", + async ({ + params: { id }, + query: { limit, after, query, sort, filter, preferOriginal }, + headers: { "accept-language": languages }, + request: { url }, + error, + }) => { + const [member] = await db + .select({ pk: staff.pk }) + .from(staff) + .where(isUuid(id) ? eq(staff.id, id) : eq(staff.slug, id)) + .limit(1); + + if (!member) { + return error(404, { + status: 404, + message: `No staff member with the id or slug: '${id}'.`, + }); + } + + const langs = processLanguages(languages); + const transQ = db + .selectDistinctOn([showTranslations.pk]) + .from(showTranslations) + .orderBy( + showTranslations.pk, + sql`array_position(${sqlarr(langs)}, ${showTranslations.language})`, + ) + .as("t"); + const items = await db + .select({ + ...getColumns(roles), + show: { + ...getColumns(shows), + ...getColumns(transQ), + + // movie columns (status is only a typescript hint) + status: sql`${shows.status}`, + airDate: shows.startAir, + kind: sql`${shows.kind}`, + isAvailable: sql`${shows.availableCount} != 0`, + + ...(preferOriginal && { + poster: sql`coalesce(nullif(${shows.original}->'poster', 'null'::jsonb), ${transQ.poster})`, + thumbnail: sql`coalesce(nullif(${shows.original}->'thumbnail', 'null'::jsonb), ${transQ.thumbnail})`, + banner: sql`coalesce(nullif(${shows.original}->'banner', 'null'::jsonb), ${transQ.banner})`, + logo: sql`coalesce(nullif(${shows.original}->'logo', 'null'::jsonb), ${transQ.logo})`, + }), + }, + }) + .from(roles) + .innerJoin(shows, eq(roles.showPk, shows.pk)) + .where( + and( + eq(roles.staffPk, member.pk), + filter, + query ? sql`${transQ.name} %> ${query}::text` : undefined, + keysetPaginate({ table: shows, after, sort }), + ), + ) + .orderBy( + ...(query + ? [sql`word_similarity(${query}::text, ${transQ.name})`] + : sortToSql(sort, shows)), + roles.showPk, + ) + .limit(limit); + return createPage(items, { url, sort, limit }); + }, + { + detail: { + description: "Get all roles this staff member worked as/on.", + }, + params: t.Object({ + id: t.String({ + description: "The id or slug of the staff to retrieve.", + example: "hiroyuki-sawano", + }), + }), + query: t.Object({ + sort: showSort, + filter: t.Optional(Filter({ def: roleShowFilters })), + 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 })), + preferOriginal: t.Optional( + t.Boolean({ + description: desc.preferOriginal, + }), + ), + }), + response: { + 200: Page(RoleWShow), + 404: { + ...KError, + description: "No staff found with the given id or slug.", + }, + 422: KError, + }, + }, + ) .get( "/staff", async ({ query: { limit, after, sort, query }, request: { url } }) => { diff --git a/api/src/models/staff.ts b/api/src/models/staff.ts index 66d8a727..0c4be483 100644 --- a/api/src/models/staff.ts +++ b/api/src/models/staff.ts @@ -1,4 +1,5 @@ import { t } from "elysia"; +import { Show } from "./show"; import { DbMetadata, ExternalId, Image, Resource } from "./utils"; export const Character = t.Object({ @@ -32,3 +33,19 @@ export const Staff = t.Intersect([ DbMetadata, ]); export type Staff = typeof Staff.static; + +export const RoleWShow = t.Intersect([ + Role, + t.Object({ + show: Show, + }), +]); +export type RoleWShow = typeof RoleWShow.static; + +export const RoleWStaff = t.Intersect([ + Role, + t.Object({ + staff: Staff + }), +]); +export type RoleWStaff = typeof RoleWStaff.static;