Extract movie insert to a show insert method

This commit is contained in:
Zoe Roux 2025-01-26 00:11:03 +01:00
parent b9c022f614
commit ec3d48ac79
No known key found for this signature in database
5 changed files with 119 additions and 122 deletions

View File

@ -0,0 +1,89 @@
import { eq, sql } from "drizzle-orm";
import { db } from "~/db";
import { showTranslations, shows } from "~/db/schema";
import { conflictUpdateAllExcept } from "~/db/utils";
import type { SeedMovie } from "~/models/movie";
import { getYear } from "~/utils";
import { processOptImage } from "../images";
type Show = typeof shows.$inferInsert;
type ShowTrans = typeof showTranslations.$inferInsert;
export const insertShow = async (
show: Show,
translations: SeedMovie["translations"],
) => {
return await db.transaction(async (tx) => {
const ret = await insertBaseShow(tx, show);
if ("status" in ret) return ret;
const trans: ShowTrans[] = await Promise.all(
Object.entries(translations).map(async ([lang, tr]) => ({
pk: ret.pk,
language: lang,
...tr,
poster: await processOptImage(tr.poster),
thumbnail: await processOptImage(tr.thumbnail),
logo: await processOptImage(tr.logo),
banner: await processOptImage(tr.banner),
})),
);
await tx
.insert(showTranslations)
.values(trans)
.onConflictDoUpdate({
target: [showTranslations.pk, showTranslations.language],
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
});
return ret;
});
};
async function insertBaseShow(
tx: Parameters<Parameters<typeof db.transaction>[0]>[0],
show: Show,
) {
function insert() {
return tx
.insert(shows)
.values(show)
.onConflictDoUpdate({
target: shows.slug,
set: conflictUpdateAllExcept(shows, ["pk", "id", "slug", "createdAt"]),
// if year is different, this is not an update but a conflict (ex: dune-1984 vs dune-2021)
setWhere: sql`date_part('year', ${shows.startAir}) = date_part('year', excluded."start_air")`,
})
.returning({
pk: shows.pk,
id: shows.id,
slug: shows.slug,
// https://stackoverflow.com/questions/39058213/differentiate-inserted-and-updated-rows-in-upsert-using-system-columns/39204667#39204667
updated: sql<boolean>`(xmax <> 0)`.as("updated"),
});
}
let [ret] = await insert();
if (ret) return ret;
// ret is undefined when the conflict's where return false (meaning we have
// a conflicting slug but a different air year.
// try to insert adding the year at the end of the slug.
if (show.startAir && !show.slug.endsWith(`${getYear(show.startAir)}`)) {
show.slug = `${show.slug}-${getYear(show.startAir)}`;
[ret] = await insert();
if (ret) return ret;
}
// if at this point ret is still undefined, we could not reconciliate.
// simply bail and let the caller handle this.
const [{ id }] = await db
.select({ id: shows.id })
.from(shows)
.where(eq(shows.slug, show.slug))
.limit(1);
return {
status: 409 as const,
id,
slug: show.slug,
};
}

View File

@ -1,23 +1,19 @@
import { eq, inArray, sql } from "drizzle-orm";
import { inArray, sql } from "drizzle-orm";
import { t } from "elysia";
import { db } from "~/db";
import {
entries,
type entries,
entryTranslations,
entryVideoJointure as evj,
showTranslations,
shows,
videos,
} from "~/db/schema";
import { conflictUpdateAllExcept } from "~/db/utils";
import type { SeedMovie } from "~/models/movie";
import { processOptImage } from "./images";
import { getYear } from "~/utils";
import { insertEntries } from "./insert/entries";
import { insertShow } from "./insert/shows";
import { guessNextRefresh } from "./refresh";
type Show = typeof shows.$inferInsert;
type ShowTrans = typeof showTranslations.$inferInsert;
type Entry = typeof entries.$inferInsert;
export const SeedMovieResponse = t.Object({
id: t.String({ format: "uuid" }),
slug: t.String({ format: "slug", examples: ["bubble"] }),
@ -30,7 +26,8 @@ export type SeedMovieResponse = typeof SeedMovieResponse.static;
export const seedMovie = async (
seed: SeedMovie,
): Promise<
| (SeedMovieResponse & { status: "Created" | "OK" | "Conflict" })
| (SeedMovieResponse & { updated: boolean })
| { status: 409; id: string; slug: string }
| { status: 422; message: string }
> => {
if (seed.slug === "random") {
@ -44,116 +41,27 @@ export const seedMovie = async (
}
const { translations, videos: vids, ...bMovie } = seed;
const nextRefresh = guessNextRefresh(bMovie.airDate ?? new Date());
const ret = await db.transaction(async (tx) => {
const movie: Show = {
const ret = await insertShow(
{
kind: "movie",
startAir: bMovie.airDate,
nextRefresh: guessNextRefresh(bMovie.airDate ?? new Date()),
nextRefresh,
...bMovie,
};
},
translations,
);
if ("status" in ret) return ret;
const insert = () =>
tx
.insert(shows)
.values(movie)
.onConflictDoUpdate({
target: shows.slug,
set: conflictUpdateAllExcept(shows, [
"pk",
"id",
"slug",
"createdAt",
]),
// if year is different, this is not an update but a conflict (ex: dune-1984 vs dune-2021)
setWhere: sql`date_part('year', ${shows.startAir}) = date_part('year', excluded."start_air")`,
})
.returning({
pk: shows.pk,
id: shows.id,
slug: shows.slug,
// https://stackoverflow.com/questions/39058213/differentiate-inserted-and-updated-rows-in-upsert-using-system-columns/39204667#39204667
updated: sql<boolean>`(xmax <> 0)`.as("updated"),
});
let [ret] = await insert();
if (!ret) {
// ret is undefined when the conflict's where return false (meaning we have
// a conflicting slug but a different air year.
// try to insert adding the year at the end of the slug.
if (
movie.startAir &&
!movie.slug.endsWith(`${getYear(movie.startAir)}`)
) {
movie.slug = `${movie.slug}-${getYear(movie.startAir)}`;
[ret] = await insert();
}
// if at this point ret is still undefined, we could not reconciliate.
// simply bail and let the caller handle this.
if (!ret) {
const [{ id }] = await db
.select({ id: shows.id })
.from(shows)
.where(eq(shows.slug, movie.slug))
.limit(1);
return {
status: "Conflict" as const,
id,
slug: movie.slug,
videos: [],
};
}
}
// even if never shown to the user, a movie still has an entry.
const movieEntry: Entry = { type: "movie", ...bMovie };
const [entry] = await tx
.insert(entries)
.values(movieEntry)
.onConflictDoUpdate({
target: entries.slug,
set: conflictUpdateAllExcept(entries, [
"pk",
"id",
"slug",
"createdAt",
]),
})
.returning({ pk: entries.pk });
const trans: ShowTrans[] = await Promise.all(
Object.entries(translations).map(async ([lang, tr]) => ({
pk: ret.pk,
// TODO: normalize lang or error if invalid
language: lang,
...tr,
poster: await processOptImage(tr.poster),
thumbnail: await processOptImage(tr.thumbnail),
logo: await processOptImage(tr.logo),
banner: await processOptImage(tr.banner),
})),
);
await tx
.insert(showTranslations)
.values(trans)
.onConflictDoUpdate({
target: [showTranslations.pk, showTranslations.language],
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
});
const entryTrans = trans.map((x) => ({ ...x, pk: entry.pk }));
await tx
.insert(entryTranslations)
.values(entryTrans)
.onConflictDoUpdate({
target: [entryTranslations.pk, entryTranslations.language],
set: conflictUpdateAllExcept(entryTranslations, ["pk", "language"]),
});
return { ...ret, entry: entry.pk };
});
if (ret.status === "Conflict") return ret;
// even if never shown to the user, a movie still has an entry.
const [entry] = await insertEntries(ret.pk, [
{
kind: "movie",
nextRefresh,
...bMovie,
},
]);
let retVideos: { slug: string }[] = [];
if (vids) {
@ -182,13 +90,9 @@ export const seedMovie = async (
}
return {
status: ret.updated ? "OK" : "Created",
updated: ret.updated,
id: ret.id,
slug: ret.slug,
videos: retVideos,
};
};
function getYear(date: string) {
return new Date(date).getUTCFullYear();
}

View File

@ -20,7 +20,7 @@ export const videos = new Elysia({ prefix: "/videos", tags: ["videos"] })
response: { 200: "video" },
})
.post(
"/",
"",
async ({ body }) => {
return await db
.insert(videosT)

View File

@ -12,7 +12,7 @@ export const Entry = t.Union([Episode, MovieEntry, Special]);
export type Entry = typeof Entry.static;
export const SeedEntry = t.Union([SeedEpisode, SeedMovieEntry, SeedSpecial]);
export type SeedEntry = typeof Entry.static;
export type SeedEntry = typeof SeedEntry.static;
export * from "./episode";
export * from "./movie-entry";

View File

@ -6,3 +6,7 @@ export const comment = (str: TemplateStringsArray, ...values: any[]) =>
.replace(/^[ \t]+/gm, "") // leading spaces
.replace(/([^\n])\n([^\n])/g, "$1 $2") // two lines to space separated line
.replace(/\n{2}/g, "\n"); // keep newline if there's an empty line
export function getYear(date: string) {
return new Date(date).getUTCFullYear();
}