From 011cf39dee0f3179909cae2e46f9d93a9b23b656 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:16:02 +0200 Subject: [PATCH] Delete orphan art tags routine --- app/features/art/ArtRepository.server.test.ts | 48 +++++++++++++++++++ app/features/art/ArtRepository.server.ts | 9 ++++ app/features/art/loaders/art.server.ts | 4 +- app/routines/deleteOrphanArtTags.ts | 11 +++++ app/routines/list.server.ts | 2 + 5 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 app/routines/deleteOrphanArtTags.ts diff --git a/app/features/art/ArtRepository.server.test.ts b/app/features/art/ArtRepository.server.test.ts index e52699d6f..6d1940d39 100644 --- a/app/features/art/ArtRepository.server.test.ts +++ b/app/features/art/ArtRepository.server.test.ts @@ -303,6 +303,54 @@ describe("deleteById", () => { }); }); +describe("deleteOrphanTags", () => { + beforeEach(async () => { + imageCounter = 0; + await dbInsertUsers(1); + }); + + afterEach(() => { + dbReset(); + }); + + test("deletes tags with no associated art", async () => { + const art = await ArtRepository.insert({ + authorId: 1, + url: "https://example.com/image-1.png", + validatedAt: Date.now(), + description: null, + linkedUsers: [], + tags: [{ name: "Orphan1" }, { name: "Orphan2" }], + }); + + await ArtRepository.deleteById(art.id); + + const deletedCount = await ArtRepository.deleteOrphanTags(); + expect(deletedCount).toBe(2); + + const tags = await ArtRepository.findAllTags(); + expect(tags).toHaveLength(0); + }); + + test("does not delete tags that are still linked to art", async () => { + await ArtRepository.insert({ + authorId: 1, + url: "https://example.com/image-1.png", + validatedAt: Date.now(), + description: null, + linkedUsers: [], + tags: [{ name: "InUse" }], + }); + + const deletedCount = await ArtRepository.deleteOrphanTags(); + expect(deletedCount).toBe(0); + + const tags = await ArtRepository.findAllTags(); + expect(tags).toHaveLength(1); + expect(tags[0].name).toBe("InUse"); + }); +}); + describe("insert", () => { beforeEach(async () => { imageCounter = 0; diff --git a/app/features/art/ArtRepository.server.ts b/app/features/art/ArtRepository.server.ts index 7af0e21f8..9040f48b2 100644 --- a/app/features/art/ArtRepository.server.ts +++ b/app/features/art/ArtRepository.server.ts @@ -171,6 +171,15 @@ export async function findAllTags() { return db.selectFrom("ArtTag").select(["id", "name"]).execute(); } +export async function deleteOrphanTags() { + const result = await db + .deleteFrom("ArtTag") + .where("id", "not in", db.selectFrom("TaggedArt").select("TaggedArt.tagId")) + .executeTakeFirst(); + + return Number(result.numDeletedRows); +} + export async function findArtsByUserId( userId: number, { includeAuthored = true, includeTagged = true } = {}, diff --git a/app/features/art/loaders/art.server.ts b/app/features/art/loaders/art.server.ts index 82a4ebae3..d436b9ed2 100644 --- a/app/features/art/loaders/art.server.ts +++ b/app/features/art/loaders/art.server.ts @@ -27,9 +27,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { : null; if (!filteredTag) { - return filteredTagName - ? { ...cachedArts, showcaseArts: [] } - : cachedArts; + return filteredTagName ? { ...cachedArts, showcaseArts: [] } : cachedArts; } return { diff --git a/app/routines/deleteOrphanArtTags.ts b/app/routines/deleteOrphanArtTags.ts new file mode 100644 index 000000000..f97963b86 --- /dev/null +++ b/app/routines/deleteOrphanArtTags.ts @@ -0,0 +1,11 @@ +import * as ArtRepository from "../features/art/ArtRepository.server"; +import { logger } from "../utils/logger"; +import { Routine } from "./routine.server"; + +export const DeleteOrphanArtTagsRoutine = new Routine({ + name: "DeleteOrphanArtTags", + func: async () => { + const deletedCount = await ArtRepository.deleteOrphanTags(); + logger.info(`Deleted ${deletedCount} orphan art tags`); + }, +}); diff --git a/app/routines/list.server.ts b/app/routines/list.server.ts index ff0a6bde4..01198bfb4 100644 --- a/app/routines/list.server.ts +++ b/app/routines/list.server.ts @@ -1,5 +1,6 @@ import { CloseExpiredCommissionsRoutine } from "./closeExpiredCommissions"; import { DeleteOldNotificationsRoutine } from "./deleteOldNotifications"; +import { DeleteOrphanArtTagsRoutine } from "./deleteOrphanArtTags"; import { NotifyCheckInStartRoutine } from "./notifyCheckInStart"; import { NotifyPlusServerVotingRoutine } from "./notifyPlusServerVoting"; import { NotifyScrimStartingSoonRoutine } from "./notifyScrimStartingSoon"; @@ -28,6 +29,7 @@ export const everyHourAt30 = [ export const daily = [ DeleteOldNotificationsRoutine, CloseExpiredCommissionsRoutine, + DeleteOrphanArtTagsRoutine, ]; /** List of Routines that should occur every 2 minutes */