Rework & type relations

This commit is contained in:
Zoe Roux 2025-03-09 16:26:28 +01:00
parent f9ff6c00d7
commit aab38f6a89
No known key found for this signature in database
4 changed files with 96 additions and 76 deletions

View File

@ -17,16 +17,15 @@ import {
sqlarr, sqlarr,
} from "~/db/utils"; } from "~/db/utils";
import type { MovieStatus } from "~/models/movie"; import type { MovieStatus } from "~/models/movie";
import { SerieStatus, SerieTranslation } from "~/models/serie"; import { SerieStatus, type SerieTranslation } from "~/models/serie";
import { Studio } from "~/models/studio"; import type { Studio } from "~/models/studio";
import { import {
type FilterDef, type FilterDef,
Genre, Genre,
type Image, type Image,
Sort, Sort,
isUuid, buildRelations,
keysetPaginate, keysetPaginate,
selectTranslationQuery,
sortToSql, sortToSql,
} from "~/models/utils"; } from "~/models/utils";
@ -69,14 +68,60 @@ export const showSort = Sort(
}, },
); );
const buildRelations = <R extends string>( const showRelations = {
relations: R[], translations: () => {
toSql: (relation: R) => SQL, const { pk, language, ...trans } = getColumns(showTranslations);
) => { return db
return Object.fromEntries(relations.map((x) => [x, toSql(x)])) as Record< .select({
R, json: jsonbObjectAgg(
SQL language,
>; jsonbBuildObject<SerieTranslation>(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<Studio>({ ...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({ export async function getShows({
@ -115,61 +160,6 @@ export async function getShows({
.as("t"); .as("t");
const { pk, ...transCol } = getColumns(transQ); 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<SerieTranslation[]>`${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<Studio>`${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 return await db
.select({ .select({
...getColumns(shows), ...getColumns(shows),
@ -189,7 +179,7 @@ export async function getShows({
logo: sql<Image>`coalesce(nullif(${shows.original}->'logo', 'null'::jsonb), ${transQ.logo})`, logo: sql<Image>`coalesce(nullif(${shows.original}->'logo', 'null'::jsonb), ${transQ.logo})`,
}), }),
...relationsSql, ...buildRelations(relations, showRelations, { languages }),
}) })
.from(shows) .from(shows)
[fallbackLanguage ? "innerJoin" : "leftJoin"]( [fallbackLanguage ? "innerJoin" : "leftJoin"](

View File

@ -1,5 +1,6 @@
import { import {
type ColumnsSelection, type ColumnsSelection,
InferColumnsDataTypes,
type SQL, type SQL,
type SQLWrapper, type SQLWrapper,
type Subquery, type Subquery,
@ -94,24 +95,26 @@ export function values(items: Record<string, unknown>[]) {
}; };
} }
export const coalesce = (val: SQLWrapper, def: SQLWrapper) => { export const coalesce = <T>(val: SQL<T>, def: SQLWrapper) => {
return sql`coalesce(${val}, ${def})`; return sql<T>`coalesce(${val}, ${def})`;
}; };
export const jsonbObjectAgg = (key: SQLWrapper, value: SQLWrapper) => { export const jsonbObjectAgg = <T>(key: SQLWrapper, value: SQL<T>) => {
return sql`jsonb_object_agg(${sql.join([key, value], sql.raw(","))})`; return sql<
Record<string, T>
>`jsonb_object_agg(${sql.join([key, value], sql.raw(","))})`;
}; };
export const jsonbAgg = (val: SQLWrapper) => { export const jsonbAgg = <T>(val: SQL<T>) => {
return sql`jsonb_agg(${val})`; return sql<T>`jsonb_agg(${val})`;
}; };
export const jsonbBuildObject = (select: Record<string, SQLWrapper>) => { export const jsonbBuildObject = <T>(select: Record<string, SQLWrapper>) => {
const query = sql.join( const query = sql.join(
Object.entries(select).flatMap(([k, v]) => { Object.entries(select).flatMap(([k, v]) => {
return [sql.raw(`'${k}'`), v]; return [sql.raw(`'${k}'`), v];
}), }),
sql.raw(", "), sql.raw(", "),
); );
return sql`jsonb_build_object(${query})`; return sql<T>`jsonb_build_object(${query})`;
}; };

View File

@ -9,3 +9,4 @@ export * from "./sort";
export * from "./keyset-paginate"; export * from "./keyset-paginate";
export * from "./db-metadata"; export * from "./db-metadata";
export * from "./original"; export * from "./original";
export * from "./relations";

View File

@ -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<R, (languages: P) => 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<Rel[P]>["_"]["selectedFields"] extends {
[key: string]: infer TValue;
}
? SelectResultField<TValue>
: never
>;
};
};