214 lines
5.9 KiB
TypeScript
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;
|