import { router, protectedProcedure } from '@/src/trpc/trpc-index' import { z } from 'zod'; import { db } from '@/src/db/db_index' import { productTagInfo } from '@/src/db/schema' import { eq } from 'drizzle-orm'; import { ApiError } from '@/src/lib/api-error' import { scaffoldAssetUrl, claimUploadUrl } from '@/src/lib/s3-client' import { deleteS3Image } from '@/src/lib/delete-image' import { scheduleStoreInitialization } from '@/src/stores/store-initializer' export const tagRouter = router({ getTags: protectedProcedure .query(async () => { const tags = await db .select() .from(productTagInfo) .orderBy(productTagInfo.tagName); // Generate asset URLs for tag images const tagsWithUrls = tags.map(tag => ({ ...tag, imageUrl: tag.imageUrl ? scaffoldAssetUrl(tag.imageUrl) : null, })); return { tags: tagsWithUrls, message: "Tags retrieved successfully", }; }), getTagById: protectedProcedure .input(z.object({ id: z.number(), })) .query(async ({ input }) => { const tag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.id, input.id), }); if (!tag) { throw new ApiError("Tag not found", 404); } // Generate asset URL for tag image const tagWithUrl = { ...tag, imageUrl: tag.imageUrl ? scaffoldAssetUrl(tag.imageUrl) : null, }; return { tag: tagWithUrl, message: "Tag retrieved successfully", }; }), createTag: protectedProcedure .input(z.object({ tagName: z.string().min(1), tagDescription: z.string().optional(), isDashboardTag: z.boolean().default(false), relatedStores: z.array(z.number()).default([]), imageKey: z.string().optional(), })) .mutation(async ({ input }) => { const { tagName, tagDescription, isDashboardTag, relatedStores, imageKey } = input; // Check for duplicate tag name const existingTag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.tagName, tagName.trim()), }); if (existingTag) { throw new ApiError("A tag with this name already exists", 400); } const [newTag] = await db .insert(productTagInfo) .values({ tagName: tagName.trim(), tagDescription, imageUrl: imageKey || null, isDashboardTag, relatedStores, }) .returning(); // Claim upload URL if image was provided if (imageKey) { try { await claimUploadUrl(imageKey); } catch (e) { console.warn(`Failed to claim upload URL for key: ${imageKey}`, e); } } scheduleStoreInitialization(); return { tag: newTag, message: "Tag created successfully", }; }), updateTag: protectedProcedure .input(z.object({ id: z.number(), tagName: z.string().min(1), tagDescription: z.string().optional(), isDashboardTag: z.boolean(), relatedStores: z.array(z.number()), imageKey: z.string().optional(), deleteExistingImage: z.boolean().optional(), })) .mutation(async ({ input }) => { const { id, imageKey, deleteExistingImage, ...updateData } = input; // Get current tag const currentTag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.id, id), }); if (!currentTag) { throw new ApiError("Tag not found", 404); } let newImageUrl = currentTag.imageUrl; // Handle image deletion if (deleteExistingImage && currentTag.imageUrl) { try { await deleteS3Image(currentTag.imageUrl); } catch (e) { console.error(`Failed to delete old image: ${currentTag.imageUrl}`, e); } newImageUrl = null; } // Handle new image upload (only if different from existing) if (imageKey && imageKey !== currentTag.imageUrl) { // Delete old image if exists and not already deleted if (currentTag.imageUrl && !deleteExistingImage) { try { await deleteS3Image(currentTag.imageUrl); } catch (e) { console.error(`Failed to delete old image: ${currentTag.imageUrl}`, e); } } newImageUrl = imageKey; // Claim upload URL try { await claimUploadUrl(imageKey); } catch (e) { console.warn(`Failed to claim upload URL for key: ${imageKey}`, e); } } const [updatedTag] = await db .update(productTagInfo) .set({ tagName: updateData.tagName.trim(), tagDescription: updateData.tagDescription, isDashboardTag: updateData.isDashboardTag, relatedStores: updateData.relatedStores, imageUrl: newImageUrl, }) .where(eq(productTagInfo.id, id)) .returning(); scheduleStoreInitialization(); return { tag: updatedTag, message: "Tag updated successfully", }; }), deleteTag: protectedProcedure .input(z.object({ id: z.number(), })) .mutation(async ({ input }) => { const { id } = input; // Get tag to check for image const tag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.id, id), }); if (!tag) { throw new ApiError("Tag not found", 404); } // Delete image from S3 if exists if (tag.imageUrl) { try { await deleteS3Image(tag.imageUrl); } catch (e) { console.error(`Failed to delete image: ${tag.imageUrl}`, e); } } // Delete tag (will fail if tag is assigned to products due to FK constraint) await db.delete(productTagInfo).where(eq(productTagInfo.id, id)); scheduleStoreInitialization(); return { message: "Tag deleted successfully", }; }), }); export type TagRouter = typeof tagRouter;