diff --git a/api/src/controllers/seed/insert/shows.ts b/api/src/controllers/seed/insert/shows.ts new file mode 100644 index 00000000..eb895d7e --- /dev/null +++ b/api/src/controllers/seed/insert/shows.ts @@ -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[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`(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, + }; +} diff --git a/api/src/controllers/seed/movies.ts b/api/src/controllers/seed/movies.ts index 2c32f492..14ef30e2 100644 --- a/api/src/controllers/seed/movies.ts +++ b/api/src/controllers/seed/movies.ts @@ -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`(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(); -} diff --git a/api/src/controllers/videos.ts b/api/src/controllers/videos.ts index 47f64b71..4761e5d1 100644 --- a/api/src/controllers/videos.ts +++ b/api/src/controllers/videos.ts @@ -20,7 +20,7 @@ export const videos = new Elysia({ prefix: "/videos", tags: ["videos"] }) response: { 200: "video" }, }) .post( - "/", + "", async ({ body }) => { return await db .insert(videosT) diff --git a/api/src/models/entry/index.ts b/api/src/models/entry/index.ts index 72b5bb43..73e73e6c 100644 --- a/api/src/models/entry/index.ts +++ b/api/src/models/entry/index.ts @@ -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"; diff --git a/api/src/utils.ts b/api/src/utils.ts index 00c2f5ab..bb3d374e 100644 --- a/api/src/utils.ts +++ b/api/src/utils.ts @@ -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(); +}