/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { Art, CreateArtDto, UpdateArtDto } from "@library/shared-types"; import { prisma } from "../lib/prisma"; import { validateUrl, validateStringLength, MAX_LENGTHS, } from "../utils/validation"; export class ArtService { private prisma = prisma; constructor() {} /** * Validate art data for security. */ private validateArtData(data: CreateArtDto | UpdateArtDto): void { // Validate string lengths if (!validateStringLength(data.title, MAX_LENGTHS.TITLE)) { throw new Error(`Title must be ${MAX_LENGTHS.TITLE} characters or less.`); } if (!validateStringLength(data.artist, MAX_LENGTHS.AUTHOR)) { throw new Error(`Artist must be ${MAX_LENGTHS.AUTHOR} characters or less.`); } if (!validateStringLength(data.description, MAX_LENGTHS.DESCRIPTION)) { throw new Error(`Description must be ${MAX_LENGTHS.DESCRIPTION} characters or less.`); } // Validate image URL (required) if (!data.imageUrl) { throw new Error("Image URL is required."); } if (!validateUrl(data.imageUrl)) { throw new Error("Invalid image URL. Only http and https URLs are allowed."); } if (!validateStringLength(data.imageUrl, MAX_LENGTHS.URL)) { throw new Error(`Image URL must be ${MAX_LENGTHS.URL} characters or less.`); } // Validate tags if (data.tags) { for (const tag of data.tags) { if (!validateStringLength(tag, MAX_LENGTHS.TAGS)) { throw new Error(`Each tag must be ${MAX_LENGTHS.TAGS} characters or less.`); } } } // Validate link URLs if (data.links) { for (const link of data.links) { if (!validateUrl(link.url)) { throw new Error(`Invalid link URL: ${link.title}. Only http and https URLs are allowed.`); } if (!validateStringLength(link.title, MAX_LENGTHS.AUTHOR)) { throw new Error(`Link title must be ${MAX_LENGTHS.AUTHOR} characters or less.`); } if (!validateStringLength(link.url, MAX_LENGTHS.URL)) { throw new Error(`Link URL must be ${MAX_LENGTHS.URL} characters or less.`); } } } } /** * Get all art pieces. */ async getAllArt(): Promise { const artPieces = await this.prisma.art.findMany({ orderBy: { createdAt: "desc" }, }); return artPieces.map((art) => ({ ...art, description: art.description || undefined, tags: art.tags ?? [], links: art.links ?? [], dateAdded: art.dateAdded, createdAt: art.createdAt, updatedAt: art.updatedAt, })); } /** * Get art by ID. */ async getArtById(id: string): Promise { const art = await this.prisma.art.findUnique({ where: { id }, }); if (!art) return null; return { ...art, description: art.description || undefined, tags: art.tags ?? [], links: art.links ?? [], dateAdded: art.dateAdded, createdAt: art.createdAt, updatedAt: art.updatedAt, }; } /** * Create new art piece. */ async createArt(data: CreateArtDto): Promise { // Validate input this.validateArtData(data); const art = await this.prisma.art.create({ data, }); return { ...art, description: art.description || undefined, tags: art.tags ?? [], links: art.links ?? [], dateAdded: art.dateAdded, createdAt: art.createdAt, updatedAt: art.updatedAt, }; } /** * Update art by ID. */ async updateArt(id: string, data: UpdateArtDto): Promise { // Validate input this.validateArtData(data); const art = await this.prisma.art.update({ where: { id }, data, }); return { ...art, description: art.description || undefined, tags: art.tags ?? [], links: art.links ?? [], dateAdded: art.dateAdded, createdAt: art.createdAt, updatedAt: art.updatedAt, }; } /** * Delete art by ID. */ async deleteArt(id: string): Promise { await this.prisma.art.delete({ where: { id }, }); } }