diff --git a/api/src/controllers/seed/insert/staff.ts b/api/src/controllers/seed/insert/staff.ts new file mode 100644 index 00000000..6512953e --- /dev/null +++ b/api/src/controllers/seed/insert/staff.ts @@ -0,0 +1,47 @@ +import { eq } from "drizzle-orm"; +import { db } from "~/db"; +import { roles, staff } from "~/db/schema"; +import { conflictUpdateAllExcept } from "~/db/utils"; +import type { SeedStaff } from "~/models/staff"; +import { processOptImage } from "../images"; + +export const insertStaff = async ( + seed: SeedStaff[] | undefined, + showPk: number, +) => { + if (!seed?.length) return []; + + return await db.transaction(async (tx) => { + const people = seed.map((x) => ({ + ...x.staff, + image: processOptImage(x.staff.image), + })); + const ret = await tx + .insert(staff) + .values(people) + .onConflictDoUpdate({ + target: staff.slug, + set: conflictUpdateAllExcept(staff, ["pk", "id", "slug", "createdAt"]), + }) + .returning({ pk: staff.pk, id: staff.id, slug: staff.slug }); + + const rval = seed.map((x, i) => ({ + showPk, + staffPk: ret[i].pk, + kind: x.kind, + order: i, + character: { + ...x.character, + image: processOptImage(x.character.image), + }, + })); + + // always replace all roles. this is because: + // - we want `order` to stay in sync (& without duplicates) + // - we don't have ways to identify a role so we can't onConflict + await tx.delete(roles).where(eq(roles.showPk, showPk)); + await tx.insert(roles).values(rval); + + return ret; + }); +}; diff --git a/api/src/controllers/seed/movies.ts b/api/src/controllers/seed/movies.ts index 28232697..ec54c2ab 100644 --- a/api/src/controllers/seed/movies.ts +++ b/api/src/controllers/seed/movies.ts @@ -5,6 +5,7 @@ import { processOptImage } from "./images"; import { insertCollection } from "./insert/collection"; import { insertEntries } from "./insert/entries"; import { insertShow, updateAvailableCount } from "./insert/shows"; +import { insertStaff } from "./insert/staff"; import { insertStudios } from "./insert/studios"; import { guessNextRefresh } from "./refresh"; @@ -26,6 +27,12 @@ export const SeedMovieResponse = t.Object({ slug: t.String({ format: "slug", examples: ["disney"] }), }), ), + staff: t.Array( + t.Object({ + id: t.String({ format: "uuid" }), + slug: t.String({ format: "slug", examples: ["hiroyuki-sawano"] }), + }), + ), }); export type SeedMovieResponse = typeof SeedMovieResponse.static; @@ -46,7 +53,7 @@ export const seedMovie = async ( seed.slug = `random-${getYear(seed.airDate)}`; } - const { translations, videos, collection, studios, ...movie } = seed; + const { translations, videos, collection, studios, staff, ...movie } = seed; const nextRefresh = guessNextRefresh(movie.airDate ?? new Date()); const original = translations[movie.originalLanguage]; if (!original) { @@ -101,6 +108,7 @@ export const seedMovie = async ( await updateAvailableCount([show.pk], false); const retStudios = await insertStudios(studios, show.pk); + const retStaff = await insertStaff(staff, show.pk); return { updated: show.updated, @@ -109,5 +117,6 @@ export const seedMovie = async ( videos: entry.videos, collection: col, studios: retStudios, + staff: retStaff, }; }; diff --git a/api/src/controllers/seed/series.ts b/api/src/controllers/seed/series.ts index 5ca15eb6..e6981c26 100644 --- a/api/src/controllers/seed/series.ts +++ b/api/src/controllers/seed/series.ts @@ -6,6 +6,7 @@ import { insertCollection } from "./insert/collection"; import { insertEntries } from "./insert/entries"; import { insertSeasons } from "./insert/seasons"; import { insertShow, updateAvailableCount } from "./insert/shows"; +import { insertStaff } from "./insert/staff"; import { insertStudios } from "./insert/studios"; import { guessNextRefresh } from "./refresh"; @@ -53,6 +54,12 @@ export const SeedSerieResponse = t.Object({ slug: t.String({ format: "slug", examples: ["mappa"] }), }), ), + staff: t.Array( + t.Object({ + id: t.String({ format: "uuid" }), + slug: t.String({ format: "slug", examples: ["hiroyuki-sawano"] }), + }), + ), }); export type SeedSerieResponse = typeof SeedSerieResponse.static; @@ -80,6 +87,7 @@ export const seedSerie = async ( extras, collection, studios, + staff, ...serie } = seed; const nextRefresh = guessNextRefresh(serie.startAir ?? new Date()); @@ -127,6 +135,7 @@ export const seedSerie = async ( await updateAvailableCount([show.pk]); const retStudios = await insertStudios(studios, show.pk); + const retStaff = await insertStaff(staff, show.pk); return { updated: show.updated, @@ -137,5 +146,6 @@ export const seedSerie = async ( extras: retExtras, collection: col, studios: retStudios, + staff: retStaff, }; }; diff --git a/api/src/controllers/staff.ts b/api/src/controllers/staff.ts index be2f3a3c..199dd691 100644 --- a/api/src/controllers/staff.ts +++ b/api/src/controllers/staff.ts @@ -7,7 +7,8 @@ import { roles, staff } from "~/db/schema/staff"; import { getColumns, sqlarr } from "~/db/utils"; import { KError } from "~/models/error"; import type { MovieStatus } from "~/models/movie"; -import { Role, RoleWShow, RoleWStaff, Staff } from "~/models/staff"; +import { Role, Staff } from "~/models/staff"; +import { RoleWShow, RoleWStaff } from "~/models/staff-roles"; import { Filter, type FilterDef, @@ -42,11 +43,13 @@ const staffRoleSort = Sort( name: { sql: staff.name, accessor: (x) => x.staff.name }, latinName: { sql: staff.latinName, accessor: (x) => x.staff.latinName }, characterName: { - sql: sql`${roles.character}->'name'`, + sql: sql`${roles.character}->>'name'`, + isNullable: true, accessor: (x) => x.character.name, }, characterLatinName: { - sql: sql`${roles.character}->'latinName'`, + sql: sql`${roles.character}->>'latinName'`, + isNullable: true, accessor: (x) => x.character.latinName, }, }, diff --git a/api/src/db/schema/staff.ts b/api/src/db/schema/staff.ts index 351de0fb..a23839cb 100644 --- a/api/src/db/schema/staff.ts +++ b/api/src/db/schema/staff.ts @@ -42,6 +42,7 @@ export const staff = schema.table("staff", { export const roles = schema.table( "roles", { + pk: integer().primaryKey().generatedAlwaysAsIdentity(), showPk: integer() .notNull() .references(() => shows.pk, { onDelete: "cascade" }), @@ -53,7 +54,6 @@ export const roles = schema.table( character: jsonb().$type(), }, (t) => [ - primaryKey({ columns: [t.showPk, t.staffPk] }), index("role_kind").using("hash", t.kind), index("role_order").on(t.order), ], diff --git a/api/src/models/movie.ts b/api/src/models/movie.ts index 86ed58d6..326f203a 100644 --- a/api/src/models/movie.ts +++ b/api/src/models/movie.ts @@ -2,6 +2,7 @@ import { t } from "elysia"; import type { Prettify } from "~/utils"; import { SeedCollection } from "./collections"; import { bubble, bubbleImages, registerExamples } from "./examples"; +import { SeedStaff } from "./staff"; import { SeedStudio, Studio } from "./studio"; import { DbMetadata, @@ -90,6 +91,7 @@ export const SeedMovie = t.Intersect([ videos: t.Optional(t.Array(t.String({ format: "uuid" }), { default: [] })), collection: t.Optional(SeedCollection), studios: t.Optional(t.Array(SeedStudio, { default: [] })), + staff: t.Optional(t.Array(SeedStaff, { default: [] })), }), ]); export type SeedMovie = Prettify; diff --git a/api/src/models/serie.ts b/api/src/models/serie.ts index 00f6eb7c..3bafdb67 100644 --- a/api/src/models/serie.ts +++ b/api/src/models/serie.ts @@ -4,6 +4,7 @@ import { SeedCollection } from "./collections"; import { SeedEntry, SeedExtra } from "./entry"; import { bubbleImages, madeInAbyss, registerExamples } from "./examples"; import { SeedSeason } from "./season"; +import { SeedStaff } from "./staff"; import { SeedStudio, Studio } from "./studio"; import { DbMetadata, @@ -106,6 +107,7 @@ export const SeedSerie = t.Intersect([ extras: t.Optional(t.Array(SeedExtra, { default: [] })), collection: t.Optional(SeedCollection), studios: t.Optional(t.Array(SeedStudio, { default: [] })), + staff: t.Optional(t.Array(SeedStaff, { default: [] })), }), ]); export type SeedSerie = typeof SeedSerie.static; diff --git a/api/src/models/staff-roles.ts b/api/src/models/staff-roles.ts new file mode 100644 index 00000000..4322ed6f --- /dev/null +++ b/api/src/models/staff-roles.ts @@ -0,0 +1,19 @@ +import { t } from "elysia"; +import { Show } from "./show"; +import { Role, Staff } from "./staff"; + +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; diff --git a/api/src/models/staff.ts b/api/src/models/staff.ts index 0c4be483..0d5686c7 100644 --- a/api/src/models/staff.ts +++ b/api/src/models/staff.ts @@ -1,6 +1,5 @@ import { t } from "elysia"; -import { Show } from "./show"; -import { DbMetadata, ExternalId, Image, Resource } from "./utils"; +import { DbMetadata, ExternalId, Image, Resource, SeedImage } from "./utils"; export const Character = t.Object({ name: t.String(), @@ -22,30 +21,31 @@ export const Role = t.Object({ }); export type Role = typeof Role.static; -export const Staff = t.Intersect([ - Resource(), - t.Object({ - name: t.String(), - latinName: t.Nullable(t.String()), - image: t.Nullable(Image), - externalId: ExternalId(), - }), - DbMetadata, -]); +const StaffData = t.Object({ + name: t.String(), + latinName: t.Nullable(t.String()), + image: t.Nullable(Image), + externalId: ExternalId(), +}); +export const Staff = t.Intersect([Resource(), StaffData, DbMetadata]); export type Staff = typeof Staff.static; -export const RoleWShow = t.Intersect([ - Role, +export const SeedStaff = t.Intersect([ + t.Omit(Role, ["character"]), t.Object({ - show: Show, + character: t.Intersect([ + t.Omit(Character, ["image"]), + t.Object({ + image: t.Nullable(SeedImage), + }), + ]), + staff: t.Intersect([ + t.Object({ + slug: t.String({ format: "slug" }), + image: t.Nullable(SeedImage), + }), + t.Omit(StaffData, ["image"]), + ]), }), ]); -export type RoleWShow = typeof RoleWShow.static; - -export const RoleWStaff = t.Intersect([ - Role, - t.Object({ - staff: Staff - }), -]); -export type RoleWStaff = typeof RoleWStaff.static; +export type SeedStaff = typeof SeedStaff.static;