290 lines
7.5 KiB
TypeScript
290 lines
7.5 KiB
TypeScript
import { TRPCError } from "@trpc/server";
|
|
import { z } from "zod";
|
|
|
|
import * as cardRepo from "@kan/db/repository/card.repo";
|
|
import * as checklistRepo from "@kan/db/repository/checklist.repo";
|
|
|
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
|
import { assertUserInWorkspace } from "../utils/auth";
|
|
|
|
const checklistSchema = z.object({
|
|
publicId: z.string().length(12),
|
|
name: z.string().min(1).max(255),
|
|
});
|
|
|
|
const checklistItemSchema = z.object({
|
|
publicId: z.string().length(12),
|
|
title: z.string().min(1).max(500),
|
|
completed: z.boolean(),
|
|
});
|
|
|
|
export const checklistRouter = createTRPCRouter({
|
|
create: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
summary: "Add a checklist to a card",
|
|
method: "POST",
|
|
path: "/cards/{cardPublicId}/checklists",
|
|
description: "Adds a checklist to a card",
|
|
tags: ["Cards"],
|
|
protect: true,
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
cardPublicId: z.string().length(12),
|
|
name: z.string().min(1).max(255),
|
|
}),
|
|
)
|
|
.output(checklistSchema)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const userId = ctx.user?.id;
|
|
|
|
if (!userId)
|
|
throw new TRPCError({
|
|
message: `User not authenticated`,
|
|
code: "UNAUTHORIZED",
|
|
});
|
|
|
|
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
|
|
ctx.db,
|
|
input.cardPublicId,
|
|
);
|
|
|
|
if (!card)
|
|
throw new TRPCError({
|
|
message: `Card with public ID ${input.cardPublicId} not found`,
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
await assertUserInWorkspace(ctx.db, userId, card.workspaceId);
|
|
|
|
const newChecklist = await checklistRepo.create(ctx.db, {
|
|
name: input.name,
|
|
createdBy: userId,
|
|
cardId: card.id,
|
|
});
|
|
|
|
if (!newChecklist?.id)
|
|
throw new TRPCError({
|
|
message: `Failed to create checklist`,
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
});
|
|
|
|
return newChecklist;
|
|
}),
|
|
update: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
checklistPublicId: z.string().length(12),
|
|
name: z.string().min(1).max(255),
|
|
}),
|
|
)
|
|
.output(z.object({ publicId: z.string().length(12), name: z.string() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
const userId = ctx.user?.id;
|
|
if (!userId)
|
|
throw new TRPCError({
|
|
message: `User not authenticated`,
|
|
code: "UNAUTHORIZED",
|
|
});
|
|
|
|
const checklist = await checklistRepo.getChecklistByPublicId(
|
|
ctx.db,
|
|
input.checklistPublicId,
|
|
);
|
|
if (!checklist)
|
|
throw new TRPCError({
|
|
message: `Checklist with public ID ${input.checklistPublicId} not found`,
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
await assertUserInWorkspace(
|
|
ctx.db,
|
|
userId,
|
|
checklist.card.list.board.workspace.id,
|
|
);
|
|
|
|
const updated = await checklistRepo.updateChecklistById(ctx.db, {
|
|
id: checklist.id,
|
|
name: input.name,
|
|
});
|
|
|
|
if (!updated)
|
|
throw new TRPCError({
|
|
message: `Failed to update checklist`,
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
});
|
|
|
|
return updated;
|
|
}),
|
|
createItem: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
summary: "Add an item to a checklist",
|
|
method: "POST",
|
|
path: "/checklists/{checklistPublicId}/items",
|
|
description: "Adds an item to a checklist",
|
|
tags: ["Cards"],
|
|
protect: true,
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
checklistPublicId: z.string().length(12),
|
|
title: z.string().min(1).max(500),
|
|
}),
|
|
)
|
|
.output(checklistItemSchema)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const userId = ctx.user?.id;
|
|
|
|
if (!userId)
|
|
throw new TRPCError({
|
|
message: `User not authenticated`,
|
|
code: "UNAUTHORIZED",
|
|
});
|
|
|
|
const checklist = await checklistRepo.getChecklistByPublicId(
|
|
ctx.db,
|
|
input.checklistPublicId,
|
|
);
|
|
|
|
if (!checklist)
|
|
throw new TRPCError({
|
|
message: `Checklist with public ID ${input.checklistPublicId} not found`,
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
await assertUserInWorkspace(
|
|
ctx.db,
|
|
userId,
|
|
checklist.card.list.board.workspace.id,
|
|
);
|
|
|
|
const newChecklistItem = await checklistRepo.createItem(ctx.db, {
|
|
title: input.title,
|
|
createdBy: userId,
|
|
checklistId: checklist.id,
|
|
});
|
|
|
|
if (!newChecklistItem?.id)
|
|
throw new TRPCError({
|
|
message: `Failed to create checklist item`,
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
});
|
|
|
|
return newChecklistItem;
|
|
}),
|
|
updateItem: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
summary: "Update a checklist item",
|
|
method: "PUT",
|
|
path: "/checklists/items/{checklistItemPublicId}",
|
|
description: "Updates a checklist item (title/completed)",
|
|
tags: ["Cards"],
|
|
protect: true,
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
checklistItemPublicId: z.string().length(12),
|
|
title: z.string().min(1).max(500).optional(),
|
|
completed: z.boolean().optional(),
|
|
}),
|
|
)
|
|
.output(checklistItemSchema)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const userId = ctx.user?.id;
|
|
|
|
if (!userId)
|
|
throw new TRPCError({
|
|
message: `User not authenticated`,
|
|
code: "UNAUTHORIZED",
|
|
});
|
|
|
|
const item = await checklistRepo.getChecklistItemByPublicIdWithChecklist(
|
|
ctx.db,
|
|
input.checklistItemPublicId,
|
|
);
|
|
|
|
if (!item)
|
|
throw new TRPCError({
|
|
message: `Checklist item with public ID ${input.checklistItemPublicId} not found`,
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
await assertUserInWorkspace(
|
|
ctx.db,
|
|
userId,
|
|
item.checklist.card.list.board.workspace.id,
|
|
);
|
|
|
|
const updated = await checklistRepo.updateItemById(ctx.db, {
|
|
id: item.id,
|
|
title: input.title,
|
|
completed: input.completed,
|
|
});
|
|
|
|
if (!updated)
|
|
throw new TRPCError({
|
|
message: `Failed to update checklist item`,
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
});
|
|
|
|
return updated;
|
|
}),
|
|
deleteItem: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
summary: "Delete a checklist item",
|
|
method: "DELETE",
|
|
path: "/checklists/items/{checklistItemPublicId}",
|
|
description: "Deletes a checklist item",
|
|
tags: ["Cards"],
|
|
protect: true,
|
|
},
|
|
})
|
|
.input(z.object({ checklistItemPublicId: z.string().length(12) }))
|
|
.output(z.object({ success: z.boolean() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
const userId = ctx.user?.id;
|
|
if (!userId)
|
|
throw new TRPCError({
|
|
message: `User not authenticated`,
|
|
code: "UNAUTHORIZED",
|
|
});
|
|
|
|
const item = await checklistRepo.getChecklistItemByPublicIdWithChecklist(
|
|
ctx.db,
|
|
input.checklistItemPublicId,
|
|
);
|
|
if (!item)
|
|
throw new TRPCError({
|
|
message: `Checklist item with public ID ${input.checklistItemPublicId} not found`,
|
|
code: "NOT_FOUND",
|
|
});
|
|
|
|
await assertUserInWorkspace(
|
|
ctx.db,
|
|
userId,
|
|
item.checklist.card.list.board.workspace.id,
|
|
);
|
|
|
|
const deleted = await checklistRepo.softDeleteItemById(ctx.db, {
|
|
id: item.id,
|
|
deletedAt: new Date(),
|
|
deletedBy: userId,
|
|
});
|
|
|
|
if (!deleted)
|
|
throw new TRPCError({
|
|
message: `Failed to delete item`,
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
});
|
|
|
|
return { success: true };
|
|
}),
|
|
});
|