From e51f0f0fe842f86d2bcb1fda096c7d0c079736a6 Mon Sep 17 00:00:00 2001 From: GizmoShiba Date: Tue, 23 Jun 2026 17:27:30 -0500 Subject: [PATCH 1/2] fix: character ref deleteion --- src/routes/v1/Characters/controllers.ts | 127 +++++++++++++++--------- 1 file changed, 78 insertions(+), 49 deletions(-) diff --git a/src/routes/v1/Characters/controllers.ts b/src/routes/v1/Characters/controllers.ts index aeb5fab..f0f1187 100644 --- a/src/routes/v1/Characters/controllers.ts +++ b/src/routes/v1/Characters/controllers.ts @@ -2,6 +2,7 @@ import type { FastifyReply, FastifyRequest } from "fastify" import { ILike, type EntityManager } from "typeorm" import { Attributes, Character, Comment, RefSheet, RefSheetVariant, User } from "../../../models" import Artwork from "../../../models/Artwork" +import CharacterDashboard from "../../../models/CharacterDashboard" import Folder from "../../../models/Folder" import type { @@ -604,6 +605,20 @@ export const uploadRefSheet = async (request: FastifyRequest, reply: FastifyRepl await variantRepo.save(newVariant) } + + if (refSheet.id) { + const existingForSheet = await variantRepo.find({ + where: { refSheet: { id: refSheet.id } }, + }) + const keepIds = new Set( + body.refSheet.variants.filter((variant) => variant.id).map((variant) => variant.id!) + ) + const removedVariants = existingForSheet.filter((variant) => !keepIds.has(variant.id)) + + if (removedVariants.length > 0) { + await variantRepo.remove(removedVariants) + } + } }) return reply.code(200).send({ message: "Ref sheet uploaded successfully" }) @@ -639,21 +654,23 @@ export const deleteRefsheet = async (request: FastifyRequest, reply: FastifyRepl const user = request.user as { id: string; profileId: string } const { id } = request.params as { id: string } - const refSheet = await request.server.db.getRepository(RefSheet).findOne({ - where: { id: id, character: { owner: { id: user.profileId } } } - }) - - if (!refSheet) return reply.status(404).send("No ref sheet found.") - - const variants = await request.server.db.getRepository(RefSheetVariant).find({ - where: { refSheet: { id: refSheet.id } } - }) + const refSheet = await request.server.db + .getRepository(RefSheet) + .createQueryBuilder("refSheet") + .innerJoin("refSheet.character", "character") + .innerJoin("character.owner", "owner") + .where("refSheet.id = :id", { id }) + .andWhere("owner.id = :profileId", { profileId: user.profileId }) + .getOne() - for (const variant of variants) { - await request.server.db.getRepository(RefSheetVariant).remove(variant) + if (!refSheet) { + return reply.status(404).send({ error: "No ref sheet found." }) } - await request.server.db.getRepository(RefSheet).remove(refSheet) + await request.server.db.transaction(async (manager) => { + await manager.delete(RefSheetVariant, { refSheet: { id: refSheet.id } }) + await manager.remove(RefSheet, refSheet) + }) return reply.code(200).send({ message: "Ref sheet deleted" }) } @@ -747,19 +764,28 @@ export const deleteCharacter = async (request: FastifyRequest, reply: FastifyRep refSheets: true, migration: true, adoptionStatus: true, - owner: true + owner: true, + mainOwner: true, + dashboards: true, + artworks: { charactersFeatured: true }, + favoritedBy: true, }, - where: { id: id, owner: { id: user.profileId } } + where: { id, owner: { id: user.profileId } }, }) if (!character) { return reply.code(404).send({ error: "Character not found" }) } - await request.server.db.transaction(async (manager) => { - await updateOrDeleteRelatedEntities(character, manager) - await manager.remove(Character, character) - }) + try { + await request.server.db.transaction(async (manager) => { + await updateOrDeleteRelatedEntities(character, manager) + await manager.remove(Character, character) + }) + } catch (error) { + request.log.error(error, "Failed to delete character") + return reply.code(500).send({ error: "Failed to delete character" }) + } return reply.code(200).send({ message: "Character deleted" }) } @@ -768,11 +794,34 @@ async function updateOrDeleteRelatedEntities( character: Character, entityManager: EntityManager ) { - if (character.artworks) { + await entityManager.update( + User, + { mainCharacter: { id: character.id } }, + { mainCharacter: null } + ) + + if (character.favoritedBy?.length) { + for (const favoritingUser of character.favoritedBy) { + await entityManager + .createQueryBuilder() + .relation(User, "favoriteCharacters") + .of(favoritingUser.id) + .remove(character.id) + } + } + + await entityManager.update( + Artwork, + { publishedCharacter: { id: character.id } }, + { publishedCharacter: null } + ) + + if (character.artworks?.length) { for (const artwork of character.artworks) { - artwork.charactersFeatured = artwork.charactersFeatured.filter( - (c) => c.id !== character.id + artwork.charactersFeatured = (artwork.charactersFeatured ?? []).filter( + (featured) => featured.id !== character.id ) + if (artwork.charactersFeatured.length === 0) { await entityManager.remove(Artwork, artwork) } else { @@ -781,25 +830,17 @@ async function updateOrDeleteRelatedEntities( } } - if (character.refSheets) { - for (const refSheet of character.refSheets) { - if (!refSheet.variants) continue - for (const variant of refSheet.variants) { - await entityManager.remove(variant) - } - - await entityManager.remove(RefSheet, refSheet) - } + for (const refSheet of character.refSheets ?? []) { + await entityManager.delete(RefSheetVariant, { refSheet: { id: refSheet.id } }) + await entityManager.remove(RefSheet, refSheet) } + await entityManager.delete(Comment, { character: { id: character.id } }) + await entityManager.delete(CharacterDashboard, { character: { id: character.id } }) + await entityManager.delete(Folder, { character: { id: character.id } }) + if (character.attributes) { - // @ts-expect-error - await entityManager.update(Character, { id: character.id }, { attributes: null }) - const attributes = await entityManager.findOne(Attributes, { - where: { character: { id: character.id } } - }) - await entityManager.remove(attributes) - await entityManager.remove(character.attributes) + await entityManager.remove(Attributes, character.attributes) } if (character.migration) { @@ -814,16 +855,4 @@ async function updateOrDeleteRelatedEntities( character.mainOwner.mainCharacter = null await entityManager.save(User, character.mainOwner) } - - if (character.owner) { - const owner = await entityManager.findOne(User, { - where: { id: character.owner.id }, - relations: { characters: true } - }) - - if (owner) { - owner.characters = owner.characters.filter((c) => c.id !== character.id) - await entityManager.save(User, owner) - } - } } From 9ffd215dd408329c6c633c848cd13debc7c79c61 Mon Sep 17 00:00:00 2001 From: GizmoShiba Date: Tue, 23 Jun 2026 17:40:49 -0500 Subject: [PATCH 2/2] chore: ref sheet artist --- src/models/RefSheet.ts | 16 +++- src/routes/v1/Characters/controllers.ts | 88 ++++++++++++------- src/utils/artistCredit.ts | 109 ++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 33 deletions(-) create mode 100644 src/utils/artistCredit.ts diff --git a/src/models/RefSheet.ts b/src/models/RefSheet.ts index f05d7bd..f50ce00 100644 --- a/src/models/RefSheet.ts +++ b/src/models/RefSheet.ts @@ -31,10 +31,22 @@ export default class RefSheet { @ManyToOne(() => User, { nullable: true }) @JoinColumn() - artistUser: User + artistUser: User | null + + @Column({ type: "varchar", length: 32, nullable: true }) + artistPlatform: string | null + + @Column({ type: "varchar", length: 200, nullable: true }) + artistExternalHandle: string | null + + @Column({ type: "varchar", nullable: true }) + artistUrl: string | null + + @Column({ type: "varchar", nullable: true }) + artistExternalAvatarUrl: string | null @Column({ type: "text", nullable: true }) - artistExternal: string + artistExternal: string | null @OneToMany(() => RefSheetVariant, (variant) => variant.refSheet, { eager: true }) @JoinColumn() diff --git a/src/routes/v1/Characters/controllers.ts b/src/routes/v1/Characters/controllers.ts index f0f1187..c52bcf2 100644 --- a/src/routes/v1/Characters/controllers.ts +++ b/src/routes/v1/Characters/controllers.ts @@ -16,6 +16,15 @@ import { loadUserUploadLimit, UploadLimitError, } from "../../../utils/uploadLimits" +import { + applyRefSheetArtistCredit, + type ArtistCreditPayload, +} from "../../../utils/artistCredit" + +const REF_SHEET_RELATIONS = { + variants: true, + artistUser: true, +} as const export const getCharacters = async (request: FastifyRequest, reply: FastifyReply) => { const user = request.user as { id: string; profileId: string } @@ -23,8 +32,8 @@ export const getCharacters = async (request: FastifyRequest, reply: FastifyReply where: { owner: { id: user.profileId } }, relations: { attributes: true, - refSheets: true - } + refSheets: REF_SHEET_RELATIONS, + }, }) if (!character) return reply.status(404).send("No characters found.") @@ -60,6 +69,7 @@ export const getOwnersCharacters = async ( folder: true, refSheets: { variants: true, + artistUser: true, }, }, }, @@ -98,7 +108,12 @@ export const getCharacterById = async (request: FastifyRequest, reply: FastifyRe try { const data = await request.server.db.getRepository(Character).findOne({ - where: { id } + where: { id }, + relations: { + owner: true, + attributes: true, + refSheets: REF_SHEET_RELATIONS, + }, }) if (!data) { @@ -176,8 +191,8 @@ export const getCharacterWithOwner = async ( owner: true, attributes: true, mainOwner: true, - refSheets: true - } + refSheets: REF_SHEET_RELATIONS, + }, }) const attributes = await request.server.db.getRepository(Attributes).findOne({ @@ -454,7 +469,8 @@ export const getRefsheets = async (request: FastifyRequest, reply: FastifyReply) relations: { refSheets: { variants: true, - character: true + artistUser: true, + character: true, }, owner: true }, @@ -498,18 +514,18 @@ export const setArtAsAvatar = async (_request: FastifyRequest, reply: FastifyRep export const uploadRefSheet = async (request: FastifyRequest, reply: FastifyReply) => { const user = request.user as { id: string; profileId: string } const body = request.body as { - characterId: string, + characterId: string refSheet: { id?: string name: string - description: string - artist: string - primary: boolean + description?: string + primary?: boolean + userAsArtist?: boolean + artistCredit?: ArtistCreditPayload | null variants: { id?: string title: string description?: string - artist?: string image: string primary: boolean nsfw?: boolean @@ -518,35 +534,40 @@ export const uploadRefSheet = async (request: FastifyRequest, reply: FastifyRepl } } - console.log(body) - - const character = await request.server.db.getRepository(Character).findOneBy({ - id: body.characterId, - owner: { id: user.profileId }, + const character = await request.server.db.getRepository(Character).findOne({ + where: { + id: body.characterId, + owner: { id: user.profileId }, + }, }) - if (!character) return reply.status(404).send("No character found.") + if (!character) return reply.status(404).send({ error: "No character found." }) if (!body.refSheet.name?.trim()) { return reply.code(400).send({ error: "Ref sheet name is required." }) } - const artistUser = await request.server.db.getRepository(User).findOne({ - where: { handle: body.refSheet.artist } + const owner = await request.server.db.getRepository(User).findOne({ + where: { id: user.profileId }, }) - if (!artistUser) { - + if (!owner) { + return reply.code(404).send({ error: "User not found" }) } - await request.server.db.transaction(async (entityManager) => { const refSheetRepo = entityManager.getRepository(RefSheet) const variantRepo = entityManager.getRepository(RefSheetVariant) let refSheet: RefSheet | null = null if (body.refSheet.id) { - refSheet = await refSheetRepo.findOne({ where: { id: body.refSheet.id } }) + refSheet = await refSheetRepo.findOne({ + where: { + id: body.refSheet.id, + character: { owner: { id: user.profileId } }, + }, + relations: { artistUser: true }, + }) } if (!refSheet) { @@ -556,32 +577,38 @@ export const uploadRefSheet = async (request: FastifyRequest, reply: FastifyRepl name: body.refSheet.name, description: body.refSheet.description ?? "", primary: body.refSheet.primary ?? false, - variants: body.refSheet.variants }) } else { + refSheet.character = character refSheet.name = body.refSheet.name refSheet.description = body.refSheet.description ?? refSheet.description refSheet.primary = body.refSheet.primary ?? refSheet.primary refSheet.active = true } - await refSheetRepo.save(refSheet) + await applyRefSheetArtistCredit({ + refSheet, + db: entityManager, + currentUser: owner, + userAsArtist: body.refSheet.userAsArtist, + artistCredit: body.refSheet.artistCredit ?? null, + }) - const variantIds = body.refSheet.variants.filter(v => v.id).map(v => v.id!) + await refSheetRepo.save(refSheet) + const variantIds = body.refSheet.variants.filter((v) => v.id).map((v) => v.id!) const existingVariants = variantIds.length ? await variantRepo.findByIds(variantIds) : [] for (const variant of body.refSheet.variants) { if (variant.id) { - const existing = existingVariants.find(v => v.id === variant.id) + const existing = existingVariants.find((v) => v.id === variant.id) if (existing) { Object.assign(existing, { title: variant.title, description: variant.description ?? existing.description, url: variant.image, - artistExternal: variant.artist ?? existing.artistExternal, main: variant.primary, nsfw: variant.nsfw ?? existing.nsfw, colors: variant.colors, @@ -596,11 +623,10 @@ export const uploadRefSheet = async (request: FastifyRequest, reply: FastifyRepl title: variant.title, description: variant.description ?? "", url: variant.image, - artistExternal: variant.artist ?? "", nsfw: variant.nsfw ?? false, main: variant.primary, colors: variant.colors, - refSheet: refSheet, + refSheet, }) await variantRepo.save(newVariant) @@ -761,7 +787,7 @@ export const deleteCharacter = async (request: FastifyRequest, reply: FastifyRep const character = await request.server.db.getRepository(Character).findOne({ relations: { attributes: true, - refSheets: true, + refSheets: REF_SHEET_RELATIONS, migration: true, adoptionStatus: true, owner: true, diff --git a/src/utils/artistCredit.ts b/src/utils/artistCredit.ts new file mode 100644 index 0000000..bc7bbab --- /dev/null +++ b/src/utils/artistCredit.ts @@ -0,0 +1,109 @@ +import type { DataSource, EntityManager } from "typeorm" +import RefSheet from "../models/RefSheet" +import User from "../models/Users" + +export type ArtistCreditPayload = { + platform: string + handle?: string + url?: string + mavUserId?: string + avatarUrl?: string | null +} + +type Db = DataSource | EntityManager + +function clearRefSheetArtist(refSheet: RefSheet) { + refSheet.artistUser = null + refSheet.artistPlatform = null + refSheet.artistExternalHandle = null + refSheet.artistUrl = null + refSheet.artistExternalAvatarUrl = null + refSheet.artistExternal = null +} + +export async function applyRefSheetArtistCredit({ + refSheet, + db, + currentUser, + userAsArtist, + artistCredit, +}: { + refSheet: RefSheet + db: Db + currentUser: User + userAsArtist?: boolean + artistCredit?: ArtistCreditPayload | null +}) { + if (userAsArtist) { + refSheet.artistUser = currentUser + refSheet.artistPlatform = "mav" + refSheet.artistExternalHandle = currentUser.handle + refSheet.artistExternalAvatarUrl = currentUser.avatarUrl ?? null + refSheet.artistUrl = null + refSheet.artistExternal = null + return + } + + if (!artistCredit) { + clearRefSheetArtist(refSheet) + return + } + + const platform = artistCredit.platform?.trim() + if (!platform) { + clearRefSheetArtist(refSheet) + return + } + + if (platform === "url") { + const url = artistCredit.url?.trim() + if (!url) { + clearRefSheetArtist(refSheet) + return + } + + refSheet.artistUser = null + refSheet.artistPlatform = "url" + refSheet.artistUrl = url + refSheet.artistExternalHandle = null + refSheet.artistExternalAvatarUrl = null + refSheet.artistExternal = url + return + } + + if (platform === "mav") { + const userRepo = db.getRepository(User) + let mavArtist: User | null = null + + if (artistCredit.mavUserId) { + mavArtist = await userRepo.findOne({ where: { id: artistCredit.mavUserId } }) + } + + const handle = artistCredit.handle?.trim().replace(/^@+/, "") + if (!mavArtist && handle) { + mavArtist = await userRepo.findOne({ where: { handle } }) + } + + refSheet.artistUser = mavArtist + refSheet.artistPlatform = "mav" + refSheet.artistExternalHandle = mavArtist?.handle ?? handle ?? null + refSheet.artistExternalAvatarUrl = + mavArtist?.avatarUrl ?? artistCredit.avatarUrl ?? null + refSheet.artistUrl = null + refSheet.artistExternal = refSheet.artistExternalHandle + return + } + + const handle = artistCredit.handle?.trim().replace(/^@+/, "") + if (!handle) { + clearRefSheetArtist(refSheet) + return + } + + refSheet.artistUser = null + refSheet.artistPlatform = platform + refSheet.artistExternalHandle = handle + refSheet.artistExternalAvatarUrl = artistCredit.avatarUrl ?? null + refSheet.artistUrl = null + refSheet.artistExternal = handle +}