From ed81662ae5e047a7c0c22e9c0f78d43f41526d29 Mon Sep 17 00:00:00 2001 From: Citali Date: Mon, 2 Feb 2026 13:48:49 +0100 Subject: [PATCH] Moving the arttags table to tags table part 2 --- prisma/schema.prisma | 170 +++++++----------- src/actions/artworks/deleteArtwork.ts | 2 - src/actions/artworks/getArtworks.ts | 2 +- src/actions/artworks/getArtworksTablePage.ts | 6 +- src/actions/artworks/updateArtwork.ts | 2 +- src/actions/tags/migrateArtTags.ts | 99 +--------- src/actions/tags/migrateArtworkTagJoin.ts | 75 ++++++++ src/app/(admin)/tags/page.tsx | 21 ++- .../artworks/single/ArtworkDetails.tsx | 2 +- .../artworks/single/EditArtworkForm.tsx | 14 +- src/components/ui/multiselect.tsx | 43 ++--- src/lib/queryArtworks.ts | 2 +- src/types/Artwork.ts | 2 +- 13 files changed, 195 insertions(+), 245 deletions(-) create mode 100644 src/actions/tags/migrateArtworkTagJoin.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cf97c26..eb52aa8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -53,8 +53,7 @@ model Artwork { albums Album[] categories ArtCategory[] colors ArtworkColor[] - tags ArtTag[] - tagsV2 Tag[] @relation("ArtworkTagsV2") + tags Tag[] @relation("ArtworkTags") variants FileVariant[] @@index([colorStatus]) @@ -102,111 +101,9 @@ model ArtCategory { description String? artworks Artwork[] - tags ArtTag[] tagLinks TagCategory[] } -model ArtTag { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - sortIndex Int @default(0) - - name String @unique - slug String @unique - isParent Boolean @default(false) - showOnAnimalPage Boolean @default(false) - - description String? - - aliases ArtTagAlias[] - artworks Artwork[] - categories ArtCategory[] - - parentId String? - parent ArtTag? @relation("TagHierarchy", fields: [parentId], references: [id], onDelete: SetNull) - children ArtTag[] @relation("TagHierarchy") -} - -model Tag { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - sortIndex Int @default(0) - - name String @unique - slug String @unique - isVisible Boolean @default(true) - - description String? - - aliases TagAlias[] - categoryLinks TagCategory[] - categoryParents TagCategory[] @relation("TagCategoryParent") - artworks Artwork[] @relation("ArtworkTagsV2") - commissionTypes CommissionType[] @relation("CommissionTypeTags") - miniatures Miniature[] @relation("MiniatureTags") -} - -model TagAlias { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - alias String @unique - - tagId String - tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) - - @@unique([tagId, alias]) - @@index([alias]) -} - -model TagCategory { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - tagId String - categoryId String - - isParent Boolean @default(false) - showOnAnimalPage Boolean @default(false) - parentTagId String? - - tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) - category ArtCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade) - parentTag Tag? @relation("TagCategoryParent", fields: [parentTagId], references: [id], onDelete: SetNull) - - @@unique([tagId, categoryId]) - @@index([categoryId]) - @@index([tagId]) - @@index([parentTagId]) - @@index([categoryId, parentTagId]) -} - -model ArtTagAlias { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - alias String @unique - - tagId String - tag ArtTag @relation(fields: [tagId], references: [id], onDelete: Cascade) - - @@unique([tagId, alias]) - @@index([alias]) -} - -model Miniature { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - tags Tag[] @relation("MiniatureTags") -} - model Color { id String @id @default(cuid()) createdAt DateTime @default(now()) @@ -315,6 +212,71 @@ model FileVariant { @@unique([artworkId, type]) } +model Tag { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + sortIndex Int @default(0) + + name String @unique + slug String @unique + isVisible Boolean @default(true) + + description String? + + aliases TagAlias[] + categoryLinks TagCategory[] + categoryParents TagCategory[] @relation("TagCategoryParent") + artworks Artwork[] @relation("ArtworkTags") + commissionTypes CommissionType[] @relation("CommissionTypeTags") + miniatures Miniature[] @relation("MiniatureTags") +} + +model TagAlias { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + alias String @unique + + tagId String + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@unique([tagId, alias]) + @@index([alias]) +} + +model TagCategory { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tagId String + categoryId String + + isParent Boolean @default(false) + showOnAnimalPage Boolean @default(false) + parentTagId String? + + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + category ArtCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade) + parentTag Tag? @relation("TagCategoryParent", fields: [parentTagId], references: [id], onDelete: SetNull) + + @@unique([tagId, categoryId]) + @@index([categoryId]) + @@index([tagId]) + @@index([parentTagId]) + @@index([categoryId, parentTagId]) +} + +model Miniature { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tags Tag[] @relation("MiniatureTags") +} + model Commission { id String @id @default(cuid()) createdAt DateTime @default(now()) diff --git a/src/actions/artworks/deleteArtwork.ts b/src/actions/artworks/deleteArtwork.ts index a6e4df3..daae5b9 100644 --- a/src/actions/artworks/deleteArtwork.ts +++ b/src/actions/artworks/deleteArtwork.ts @@ -13,7 +13,6 @@ export async function deleteArtwork(artworkId: string) { colors: true, metadata: true, tags: true, - tagsV2: true, categories: true, }, }); @@ -75,7 +74,6 @@ export async function deleteArtwork(artworkId: string) { where: { id: artworkId }, data: { tags: { set: [] }, - tagsV2: { set: [] }, categories: { set: [] }, }, }); diff --git a/src/actions/artworks/getArtworks.ts b/src/actions/artworks/getArtworks.ts index 50b6391..cac2a7d 100644 --- a/src/actions/artworks/getArtworks.ts +++ b/src/actions/artworks/getArtworks.ts @@ -15,7 +15,7 @@ export async function getSingleArtwork(id: string) { categories: true, colors: { include: { color: true } }, // sortContexts: true, - tagsV2: true, + tags: true, variants: true, timelapse: true } diff --git a/src/actions/artworks/getArtworksTablePage.ts b/src/actions/artworks/getArtworksTablePage.ts index 8e5de81..c7d3099 100644 --- a/src/actions/artworks/getArtworksTablePage.ts +++ b/src/actions/artworks/getArtworksTablePage.ts @@ -22,7 +22,7 @@ function mapSortingToOrderBy(sorting: ArtworkTableInput["sorting"]) { // relation counts: Prisma supports ordering by _count albumsCount: (desc) => ({ albums: { _count: desc ? "desc" : "asc" } }), categoriesCount: (desc) => ({ categories: { _count: desc ? "desc" : "asc" } }), - tagsCount: (desc) => ({ tagsV2: { _count: desc ? "desc" : "asc" } }), + tagsCount: (desc) => ({ tags: { _count: desc ? "desc" : "asc" } }), }; const orderBy = sorting @@ -89,7 +89,7 @@ export async function getArtworksTablePage(input: unknown) { gallery: { select: { id: true, name: true } }, albums: { select: { id: true, name: true } }, categories: { select: { id: true, name: true } }, - _count: { select: { albums: true, categories: true, tagsV2: true } }, + _count: { select: { albums: true, categories: true, tags: true } }, }, }), ]); @@ -109,7 +109,7 @@ export async function getArtworksTablePage(input: unknown) { categories: a.categories, albumsCount: a._count.albums, categoriesCount: a._count.categories, - tagsCount: a._count.tagsV2, + tagsCount: a._count.tags, })); const out = { rows, total, pageIndex, pageSize }; diff --git a/src/actions/artworks/updateArtwork.ts b/src/actions/artworks/updateArtwork.ts index d7917b0..cab1ae4 100644 --- a/src/actions/artworks/updateArtwork.ts +++ b/src/actions/artworks/updateArtwork.ts @@ -47,7 +47,7 @@ export async function updateArtwork( const tagsRelation = tagIds || tagsToCreate.length ? { - tagsV2: { + tags: { set: [], // replace entire relation connect: (tagIds ?? []).map((tagId) => ({ id: tagId })), connectOrCreate: tagsToCreate.map((tName) => ({ diff --git a/src/actions/tags/migrateArtTags.ts b/src/actions/tags/migrateArtTags.ts index 0c4837e..22b3a8c 100644 --- a/src/actions/tags/migrateArtTags.ts +++ b/src/actions/tags/migrateArtTags.ts @@ -1,102 +1,5 @@ "use server"; -import { prisma } from "@/lib/prisma"; -import { revalidatePath } from "next/cache"; - export async function migrateArtTags() { - const artTags = await prisma.artTag.findMany({ - include: { - aliases: true, - categories: true, - artworks: { select: { id: true } }, - }, - orderBy: [{ sortIndex: "asc" }, { name: "asc" }], - }); - - const idMap = new Map(); - - await prisma.$transaction(async (tx) => { - for (const artTag of artTags) { - const tag = await tx.tag.upsert({ - where: { slug: artTag.slug }, - update: { - name: artTag.name, - description: artTag.description, - isVisible: true, - }, - create: { - name: artTag.name, - slug: artTag.slug, - description: artTag.description, - isVisible: true, - }, - }); - - idMap.set(artTag.id, tag.id); - } - - const aliasRows = artTags.flatMap((artTag) => { - const tagId = idMap.get(artTag.id); - if (!tagId) return []; - return artTag.aliases.map((a) => ({ - tagId, - alias: a.alias, - })); - }); - - if (aliasRows.length > 0) { - await tx.tagAlias.createMany({ - data: aliasRows, - skipDuplicates: true, - }); - } - - const categoryRows = artTags.flatMap((artTag) => { - const tagId = idMap.get(artTag.id); - if (!tagId) return []; - const parentTagId = artTag.parentId - ? idMap.get(artTag.parentId) ?? null - : null; - - return artTag.categories.map((category) => ({ - tagId, - categoryId: category.id, - isParent: artTag.isParent, - showOnAnimalPage: artTag.showOnAnimalPage, - parentTagId, - })); - }); - - if (categoryRows.length > 0) { - await tx.tagCategory.createMany({ - data: categoryRows, - skipDuplicates: true, - }); - } - }); - - // Connect artwork relations outside the transaction to avoid timeouts. - for (const artTag of artTags) { - const tagId = idMap.get(artTag.id); - if (!tagId) continue; - - for (const artwork of artTag.artworks) { - await prisma.artwork.update({ - where: { id: artwork.id }, - data: { - tagsV2: { - connect: { id: tagId }, - }, - }, - }); - } - } - - const summary = { - tags: artTags.length, - aliases: artTags.reduce((sum, t) => sum + t.aliases.length, 0), - categoryLinks: artTags.reduce((sum, t) => sum + t.categories.length, 0), - }; - revalidatePath("/tags"); - return summary; + throw new Error("Migration disabled: ArtTag models removed."); } diff --git a/src/actions/tags/migrateArtworkTagJoin.ts b/src/actions/tags/migrateArtworkTagJoin.ts new file mode 100644 index 0000000..0016244 --- /dev/null +++ b/src/actions/tags/migrateArtworkTagJoin.ts @@ -0,0 +1,75 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; + +type JoinMigrationResult = { + ok: boolean; + copied: number; + oldExists: boolean; + newExists: boolean; + droppedOld: boolean; + message?: string; +}; + +export async function migrateArtworkTagJoin( + opts: { dropOld?: boolean } = {}, +): Promise { + const dropOld = Boolean(opts.dropOld); + + const [oldRow, newRow] = await Promise.all([ + prisma.$queryRaw<{ name: string | null }[]>` + select to_regclass('_ArtworkTagsV2')::text as name; + `, + prisma.$queryRaw<{ name: string | null }[]>` + select to_regclass('_ArtworkTags')::text as name; + `, + ]); + + const oldExists = Boolean(oldRow?.[0]?.name); + const newExists = Boolean(newRow?.[0]?.name); + + if (!newExists) { + return { + ok: false, + copied: 0, + oldExists, + newExists, + droppedOld: false, + message: "New join table _ArtworkTags does not exist. Run the migration first.", + }; + } + + if (!oldExists) { + return { + ok: true, + copied: 0, + oldExists, + newExists, + droppedOld: false, + message: "Old join table _ArtworkTagsV2 not found. Nothing to copy.", + }; + } + + const copied = await prisma.$executeRawUnsafe(` + INSERT INTO "_ArtworkTags" ("A","B") + SELECT "A","B" FROM "_ArtworkTagsV2" + ON CONFLICT ("A","B") DO NOTHING; + `); + + let droppedOld = false; + if (dropOld) { + await prisma.$executeRawUnsafe(`DROP TABLE "_ArtworkTagsV2";`); + droppedOld = true; + } + + revalidatePath("/tags"); + + return { + ok: true, + copied: Number(copied ?? 0), + oldExists, + newExists, + droppedOld, + }; +} diff --git a/src/app/(admin)/tags/page.tsx b/src/app/(admin)/tags/page.tsx index 4828010..e11d1ed 100644 --- a/src/app/(admin)/tags/page.tsx +++ b/src/app/(admin)/tags/page.tsx @@ -1,9 +1,19 @@ +import { migrateArtworkTagJoin } from "@/actions/tags/migrateArtworkTagJoin"; import TagTabs from "@/components/tags/TagTabs"; import { Button } from "@/components/ui/button"; import { prisma } from "@/lib/prisma"; import { PlusCircleIcon } from "lucide-react"; import Link from "next/link"; -import { migrateArtTags } from "@/actions/tags/migrateArtTags"; + +async function migrateArtworkTagJoinCopy() { + "use server"; + await migrateArtworkTagJoin(); +} + +async function migrateArtworkTagJoinDropOld() { + "use server"; + await migrateArtworkTagJoin({ dropOld: true }); +} export default async function ArtTagsPage() { const items = await prisma.tag.findMany({ @@ -52,9 +62,14 @@ export default async function ArtTagsPage() {
-
+ +
+
+
- +
{open && ( { e.preventDefault(); diff --git a/src/lib/queryArtworks.ts b/src/lib/queryArtworks.ts index 5999e8b..3947712 100644 --- a/src/lib/queryArtworks.ts +++ b/src/lib/queryArtworks.ts @@ -26,7 +26,7 @@ export async function getArtworksPage(params: ArtworkListParams) { albums: true, categories: true, colors: true, - tagsV2: true, + tags: true, variants: true, }, orderBy: [{ createdAt: "desc" }, { id: "asc" }], diff --git a/src/types/Artwork.ts b/src/types/Artwork.ts index aba94ea..d30d36d 100644 --- a/src/types/Artwork.ts +++ b/src/types/Artwork.ts @@ -8,7 +8,7 @@ export type ArtworkWithRelations = Prisma.ArtworkGetPayload<{ albums: true; categories: true; colors: true; - tagsV2: true; + tags: true; variants: true; timelapse: true; };