From aab38f6a89433485d4c10c0912abd486e080f3ea Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 9 Mar 2025 16:26:28 +0100 Subject: [PATCH] Rework & type relations --- api/src/controllers/shows/logic.ts | 126 +++++++++++++---------------- api/src/db/utils.ts | 19 +++-- api/src/models/utils/index.ts | 1 + api/src/models/utils/relations.ts | 26 ++++++ 4 files changed, 96 insertions(+), 76 deletions(-) create mode 100644 api/src/models/utils/relations.ts diff --git a/api/src/controllers/shows/logic.ts b/api/src/controllers/shows/logic.ts index 65b551dd..695517b4 100644 --- a/api/src/controllers/shows/logic.ts +++ b/api/src/controllers/shows/logic.ts @@ -17,16 +17,15 @@ import { sqlarr, } from "~/db/utils"; import type { MovieStatus } from "~/models/movie"; -import { SerieStatus, SerieTranslation } from "~/models/serie"; -import { Studio } from "~/models/studio"; +import { SerieStatus, type SerieTranslation } from "~/models/serie"; +import type { Studio } from "~/models/studio"; import { type FilterDef, Genre, type Image, Sort, - isUuid, + buildRelations, keysetPaginate, - selectTranslationQuery, sortToSql, } from "~/models/utils"; @@ -69,14 +68,60 @@ 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 - >; +const showRelations = { + translations: () => { + const { pk, language, ...trans } = getColumns(showTranslations); + return db + .select({ + json: jsonbObjectAgg( + language, + jsonbBuildObject(trans), + ).as("json"), + }) + .from(showTranslations) + .where(eq(showTranslations.pk, shows.pk)) + .as("translations"); + }, + studios: ({ languages }: { languages: string[] }) => { + const { pk: _, ...studioCol } = getColumns(studios); + const studioTransQ = db + .selectDistinctOn([studioTranslations.pk]) + .from(studioTranslations) + .orderBy( + studioTranslations.pk, + sql`array_position(${sqlarr(languages)}, ${studioTranslations.language}`, + ) + .as("t"); + const { pk, language, ...studioTrans } = getColumns(studioTransQ); + + return db + .select({ + json: coalesce( + jsonbAgg(jsonbBuildObject({ ...studioTrans, ...studioCol })), + sql`'[]'::jsonb`, + ).as("json"), + }) + .from(studios) + .leftJoin(studioTransQ, eq(studios.pk, studioTransQ.pk)) + .where( + exists( + db + .select() + .from(showStudioJoin) + .where( + and( + eq(showStudioJoin.studioPk, studios.pk), + eq(showStudioJoin.showPk, shows.pk), + ), + ), + ), + ) + .as("studios"); + }, + // only available for movies + videos: () => { + throw new Error(); + }, }; export async function getShows({ @@ -115,61 +160,6 @@ export async function getShows({ .as("t"); const { pk, ...transCol } = getColumns(transQ); - const relationsSql = buildRelations(relations, (x) => { - switch (x) { - 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))}`; - } - case "studios": { - const { pk: _, ...studioCol } = getColumns(studios); - const studioTransQ = db - .selectDistinctOn([studioTranslations.pk]) - .from(studioTranslations) - .where( - !fallbackLanguage - ? eq(showTranslations.language, sql`any(${sqlarr(languages)})`) - : undefined, - ) - .orderBy( - studioTranslations.pk, - sql`array_position(${sqlarr(languages)}, ${studioTranslations.language}`, - ) - .as("t"); - const { pk, language, ...studioTrans } = getColumns(studioTransQ); - - return sql`${db - .select({ - json: coalesce( - jsonbAgg(jsonbBuildObject({ ...studioTrans, ...studioCol })), - sql`'[]'::jsonb`, - ), - }) - .from(studios) - .leftJoin(studioTransQ, eq(studios.pk, studioTransQ.pk)) - .where( - exists( - db - .select() - .from(showStudioJoin) - .where( - and( - eq(showStudioJoin.studioPk, studios.pk), - eq(showStudioJoin.showPk, shows.pk), - ), - ), - ), - )}`; - } - } - }); - return await db .select({ ...getColumns(shows), @@ -189,7 +179,7 @@ export async function getShows({ logo: sql`coalesce(nullif(${shows.original}->'logo', 'null'::jsonb), ${transQ.logo})`, }), - ...relationsSql, + ...buildRelations(relations, showRelations, { languages }), }) .from(shows) [fallbackLanguage ? "innerJoin" : "leftJoin"]( diff --git a/api/src/db/utils.ts b/api/src/db/utils.ts index 59ef88ab..fea4e8ef 100644 --- a/api/src/db/utils.ts +++ b/api/src/db/utils.ts @@ -1,5 +1,6 @@ import { type ColumnsSelection, + InferColumnsDataTypes, type SQL, type SQLWrapper, type Subquery, @@ -94,24 +95,26 @@ export function values(items: Record[]) { }; } -export const coalesce = (val: SQLWrapper, def: SQLWrapper) => { - return sql`coalesce(${val}, ${def})`; +export const coalesce = (val: SQL, def: SQLWrapper) => { + return sql`coalesce(${val}, ${def})`; }; -export const jsonbObjectAgg = (key: SQLWrapper, value: SQLWrapper) => { - return sql`jsonb_object_agg(${sql.join([key, value], sql.raw(","))})`; +export const jsonbObjectAgg = (key: SQLWrapper, value: SQL) => { + return sql< + Record + >`jsonb_object_agg(${sql.join([key, value], sql.raw(","))})`; }; -export const jsonbAgg = (val: SQLWrapper) => { - return sql`jsonb_agg(${val})`; +export const jsonbAgg = (val: SQL) => { + return sql`jsonb_agg(${val})`; }; -export const jsonbBuildObject = (select: Record) => { +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})`; + return sql`jsonb_build_object(${query})`; }; diff --git a/api/src/models/utils/index.ts b/api/src/models/utils/index.ts index 1d0c1fa4..53ea1dff 100644 --- a/api/src/models/utils/index.ts +++ b/api/src/models/utils/index.ts @@ -9,3 +9,4 @@ export * from "./sort"; export * from "./keyset-paginate"; export * from "./db-metadata"; export * from "./original"; +export * from "./relations"; diff --git a/api/src/models/utils/relations.ts b/api/src/models/utils/relations.ts new file mode 100644 index 00000000..1e5088a7 --- /dev/null +++ b/api/src/models/utils/relations.ts @@ -0,0 +1,26 @@ +import { type SQL, type Subquery, sql } from "drizzle-orm"; +import type { SelectResultField } from "drizzle-orm/query-builders/select.types"; + +export const buildRelations = < + R extends string, + P extends object, + Rel extends Record Subquery>, +>( + enabled: R[], + relations: Rel, + params: P, +) => { + // we wrap that in a sql`` instead of using the builder because of this issue + // https://github.com/drizzle-team/drizzle-orm/pull/1674 + return Object.fromEntries( + enabled.map((x) => [x, sql`${relations[x](params)}`]), + ) as { + [P in R]?: SQL< + ReturnType["_"]["selectedFields"] extends { + [key: string]: infer TValue; + } + ? SelectResultField + : never + >; + }; +};