freshyo/verifier/apis/tag.ts
2026-03-22 20:20:18 +05:30

214 lines
5.9 KiB
TypeScript

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;