From d5eece3ab8f2785253d885f18d9e71433ee91a9b Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 1 Nov 2024 08:23:39 +0000 Subject: [PATCH] feat: setup openapi compliant rest api --- bun.lockb | Bin 336029 -> 349544 bytes package.json | 2 + src/pages/api/v1/[trpc].ts | 30 ++++++++ src/pages/api/v1/openapi.json.ts | 9 +++ src/server/api/routers/auth.ts | 75 +++++++++++++++--- src/server/api/routers/board.ts | 67 ++++++++++++++-- src/server/api/routers/card.ts | 82 +++++++++++++++++++- src/server/api/routers/import.ts | 16 ++++ src/server/api/routers/label.ts | 38 ++++++++++ src/server/api/routers/list.ts | 54 ++++++++++++- src/server/api/routers/member.ts | 24 +++++- src/server/api/routers/workspace.ts | 84 ++++++++++++++++++--- src/server/api/trpc.ts | 63 ++++++++++++---- src/server/db/repository/board.repo.ts | 7 +- src/server/db/repository/workspace.repo.ts | 6 +- src/server/openapi.ts | 12 +++ src/utils/supabase/api.ts | 27 +++++++ src/views/board/index.tsx | 6 +- src/views/card/components/LabelForm.tsx | 6 +- 19 files changed, 547 insertions(+), 61 deletions(-) create mode 100644 src/pages/api/v1/[trpc].ts create mode 100644 src/pages/api/v1/openapi.json.ts create mode 100644 src/server/openapi.ts diff --git a/bun.lockb b/bun.lockb index 9510e14722a9aaaae52b6607264973be55752ac0..c7680533ded8d39cf5c8c7682350a583842adc9c 100755 GIT binary patch delta 89656 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 e#%@|d>aIAP;*;Ym3weJ$E+H0PA#*+ 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 I*}L@ne{vzQ$N&HU delta 82129 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 z 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 ZkUm&+%80)pPBW#Z{n3u5H^lBk{tusQ5l;XB diff --git a/package.json b/package.json index 524f6547..ad51ed52 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@vercel/postgres": "^0.7.2", "drizzle-orm": "^0.28.5", "next": "^14.1.3", + "nextjs-cors": "^2.2.0", "postgres": "^3.4.4", "react": "18.2.0", "react-beautiful-dnd": "^13.1.1", @@ -42,6 +43,7 @@ "react-lottie-player": "^1.5.5", "superjson": "^1.13.1", "tailwind-merge": "^2.5.2", + "trpc-to-openapi": "^2.0.2", "zod": "^3.22.4" }, "devDependencies": { diff --git a/src/pages/api/v1/[trpc].ts b/src/pages/api/v1/[trpc].ts new file mode 100644 index 00000000..17c118a9 --- /dev/null +++ b/src/pages/api/v1/[trpc].ts @@ -0,0 +1,30 @@ +import { type NextApiRequest, type NextApiResponse } from "next"; +import cors from "nextjs-cors"; + +import { env } from "~/env.mjs"; +import { appRouter } from "~/server/api/root"; +import { createRESTContext } from "~/server/api/trpc"; +import { createOpenApiNextHandler } from "trpc-to-openapi"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + await cors(req, res); + + const openApiHandler = createOpenApiNextHandler({ + router: appRouter, + createContext: createRESTContext, + responseMeta: () => ({ headers: {} }), + onError: + env.NODE_ENV === "development" + ? ({ path, error }) => { + console.error( + `❌ tRPC failed on ${path ?? ""}: ${error.message}`, + ); + } + : undefined, + }); + + return await openApiHandler(req, res); +} diff --git a/src/pages/api/v1/openapi.json.ts b/src/pages/api/v1/openapi.json.ts new file mode 100644 index 00000000..07b03b22 --- /dev/null +++ b/src/pages/api/v1/openapi.json.ts @@ -0,0 +1,9 @@ +import { type NextApiRequest, type NextApiResponse } from "next"; + +import { openApiDocument } from "~/server/openapi"; + +const handler = (req: NextApiRequest, res: NextApiResponse) => { + res.status(200).send(openApiDocument); +}; + +export default handler; diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts index a42630ab..89a74615 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth.ts @@ -12,21 +12,52 @@ import { env } from "~/env.mjs"; import * as userRepo from "~/server/db/repository/user.repo"; export const authRouter = createTRPCRouter({ - getUser: protectedProcedure.query(async ({ ctx }) => { - const userId = ctx.user?.id; + getUser: protectedProcedure + .meta({ + openapi: { + method: "GET", + path: "/auth/user", + summary: "Get user", + }, + }) + .input(z.void()) + .output( + z.object({ + id: z.string(), + email: z.string(), + name: z.string().nullable(), + }), + ) + .query(async ({ ctx }) => { + const userId = ctx.user?.id; - if (!userId) - throw new TRPCError({ - message: `User not authenticated`, - code: "UNAUTHORIZED", - }); + if (!userId) + throw new TRPCError({ + message: `User not authenticated`, + code: "UNAUTHORIZED", + }); - const result = await userRepo.getById(ctx.db, userId); + const result = await userRepo.getById(ctx.db, userId); - return result; - }), + if (!result?.name) { + throw new TRPCError({ + message: `User not found`, + code: "NOT_FOUND", + }); + } + + return result; + }), loginWithEmail: publicProcedure + .meta({ + openapi: { + method: "POST", + path: "/auth/email", + summary: "Login with email", + }, + }) .input(z.object({ email: z.string() })) + .output(z.object({ success: z.boolean() })) .mutation(async ({ ctx, input }) => { const { data } = await ctx.db.auth.signInWithOtp({ email: input.email, @@ -35,10 +66,24 @@ export const authRouter = createTRPCRouter({ }, }); - return data; + if (!data) + throw new TRPCError({ + message: `Failed to login with email`, + code: "INTERNAL_SERVER_ERROR", + }); + + return { success: true }; }), loginWithOAuth: publicProcedure + .meta({ + openapi: { + method: "POST", + path: "/auth/oauth", + summary: "Login with OAuth", + }, + }) .input(z.object({ provider: z.string() })) + .output(z.object({ url: z.string() })) .mutation(async ({ ctx, input }) => { if (input.provider !== "google") throw new TRPCError({ @@ -57,6 +102,12 @@ export const authRouter = createTRPCRouter({ }, }); - return data; + if (!data?.url) + throw new TRPCError({ + message: `Failed to login with OAuth`, + code: "INTERNAL_SERVER_ERROR", + }); + + return { url: data.url }; }), }); diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index 35e2b9a1..e9561f0b 100644 --- a/src/server/api/routers/board.ts +++ b/src/server/api/routers/board.ts @@ -10,7 +10,17 @@ import * as workspaceRepo from "~/server/db/repository/workspace.repo"; export const boardRouter = createTRPCRouter({ all: protectedProcedure + .meta({ + openapi: { + method: "GET", + path: "/board/{workspacePublicId}", + summary: "Get all boards", + }, + }) .input(z.object({ workspacePublicId: z.string().min(12) })) + .output( + z.custom>>(), + ) .query(async ({ ctx, input }) => { const workspace = await workspaceRepo.getByPublicId( ctx.db, @@ -28,31 +38,48 @@ export const boardRouter = createTRPCRouter({ return result; }), byId: protectedProcedure + .meta({ + openapi: { + method: "GET", + path: "/board/{boardPublicId}", + summary: "Get board by public ID", + }, + }) .input( z.object({ boardPublicId: z.string().min(12), - filters: z.object({ - members: z.array(z.string().min(12)), - labels: z.array(z.string().min(12)), - }), + members: z.array(z.string().min(12)), + labels: z.array(z.string().min(12)), }), ) + .output(z.custom>>()) .query(async ({ ctx, input }) => { const result = await boardRepo.getByPublicId( ctx.db, input.boardPublicId, - input.filters, + { + members: input.members, + labels: input.labels, + }, ); return result; }), create: protectedProcedure + .meta({ + openapi: { + method: "POST", + path: "/board", + summary: "Create board", + }, + }) .input( z.object({ name: z.string().min(1), workspacePublicId: z.string().min(12), }), ) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -79,29 +106,57 @@ export const boardRouter = createTRPCRouter({ workspaceId: workspace.id, }); + if (!result) + throw new TRPCError({ + message: `Failed to create board`, + code: "INTERNAL_SERVER_ERROR", + }); + return result; }), update: protectedProcedure + .meta({ + openapi: { + method: "PUT", + path: "/board/{boardPublicId}", + summary: "Update board", + }, + }) .input( z.object({ boardPublicId: z.string().min(12), name: z.string().min(1), }), ) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { const result = await boardRepo.update(ctx.db, { name: input.name, boardPublicId: input.boardPublicId, }); + if (!result) + throw new TRPCError({ + message: `Failed to update board`, + code: "INTERNAL_SERVER_ERROR", + }); + return result; }), delete: protectedProcedure + .meta({ + openapi: { + method: "DELETE", + path: "/board/{boardPublicId}", + summary: "Delete board", + }, + }) .input( z.object({ boardPublicId: z.string().min(12), }), ) + .output(z.object({ success: z.boolean() })) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -145,5 +200,7 @@ export const boardRouter = createTRPCRouter({ deletedBy: userId, }); } + + return { success: true }; }), }); diff --git a/src/server/api/routers/card.ts b/src/server/api/routers/card.ts index 95a882f2..fb4ae389 100644 --- a/src/server/api/routers/card.ts +++ b/src/server/api/routers/card.ts @@ -10,6 +10,13 @@ import * as workspaceRepo from "~/server/db/repository/workspace.repo"; export const cardRouter = createTRPCRouter({ create: protectedProcedure + .meta({ + openapi: { + summary: "Create a card", + method: "POST", + path: "/", + }, + }) .input( z.object({ title: z.string().min(1), @@ -20,6 +27,7 @@ export const cardRouter = createTRPCRouter({ position: z.enum(["start", "end"]), }), ) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -65,6 +73,12 @@ export const cardRouter = createTRPCRouter({ const newCardId = newCard?.id; + if (!newCardId) + throw new TRPCError({ + message: `Failed to create card`, + code: "INTERNAL_SERVER_ERROR", + }); + if (newCardId && input.labelPublicIds.length) { const labels = await labelRepo.getAllByPublicIds( ctx.db, @@ -111,12 +125,20 @@ export const cardRouter = createTRPCRouter({ return newCard; }), addOrRemoveLabel: protectedProcedure + .meta({ + openapi: { + summary: "Add or remove a label from a card", + method: "POST", + path: "/{cardPublicId}/label/{labelPublicId}", + }, + }) .input( z.object({ cardPublicId: z.string().min(12), labelPublicId: z.string().min(12), }), ) + .output(z.object({ newLabel: z.boolean() })) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -159,12 +181,20 @@ export const cardRouter = createTRPCRouter({ return { newLabel: true }; }), addOrRemoveMember: protectedProcedure + .meta({ + openapi: { + summary: "Add or remove a member from a card", + method: "POST", + path: "/{cardPublicId}/member/{workspaceMemberPublicId}", + }, + }) .input( z.object({ cardPublicId: z.string().min(12), workspaceMemberPublicId: z.string().min(12), }), ) + .output(z.object({ newMember: z.boolean() })) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -210,16 +240,41 @@ export const cardRouter = createTRPCRouter({ return { newMember: true }; }), byId: protectedProcedure + .meta({ + openapi: { + summary: "Get a card by ID", + method: "GET", + path: "/{id}", + }, + }) .input(z.object({ id: z.string().min(12) })) + .output( + z.custom< + Awaited> + >(), + ) .query(async ({ ctx, input }) => { const result = await cardRepo.getWithListAndMembersByPublicId( ctx.db, input.id, ); + if (!result) + throw new TRPCError({ + message: `Card with ID ${input.id} not found`, + code: "NOT_FOUND", + }); + return result; }), update: protectedProcedure + .meta({ + openapi: { + summary: "Update a card", + method: "PUT", + path: "/{cardId}", + }, + }) .input( z.object({ cardId: z.string().min(12), @@ -227,6 +282,7 @@ export const cardRouter = createTRPCRouter({ description: z.string(), }), ) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -236,20 +292,34 @@ export const cardRouter = createTRPCRouter({ code: "UNAUTHORIZED", }); - const result = cardRepo.update( + const result = await cardRepo.update( ctx.db, { title: input.title, description: input.description }, { cardPublicId: input.cardId }, ); + if (!result) + throw new TRPCError({ + message: `Failed to update card`, + code: "INTERNAL_SERVER_ERROR", + }); + return result; }), delete: protectedProcedure + .meta({ + openapi: { + summary: "Delete a card", + method: "DELETE", + path: "/{cardPublicId}", + }, + }) .input( z.object({ cardPublicId: z.string().min(12), }), ) + .output(z.object({ success: z.boolean() })) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -282,8 +352,17 @@ export const cardRouter = createTRPCRouter({ listId: card.list.id, cardIndex: card.index, }); + + return { success: true }; }), reorder: protectedProcedure + .meta({ + openapi: { + summary: "Reorder a card", + method: "POST", + path: "/{cardId}/reorder", + }, + }) .input( z.object({ cardId: z.string().min(12), @@ -291,6 +370,7 @@ export const cardRouter = createTRPCRouter({ newIndex: z.number().optional(), }), ) + .output(z.object({ success: z.boolean() })) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; diff --git a/src/server/api/routers/import.ts b/src/server/api/routers/import.ts index feb1e4ff..f06e4208 100644 --- a/src/server/api/routers/import.ts +++ b/src/server/api/routers/import.ts @@ -37,12 +37,20 @@ interface MemberData { export const importRouter = createTRPCRouter({ trello: createTRPCRouter({ getBoards: protectedProcedure + .meta({ + openapi: { + summary: "Get boards from Trello", + method: "GET", + path: "/boards", + }, + }) .input( z.object({ apiKey: z.string().length(32), token: z.string().length(76), }), ) + .output(z.array(z.object({ id: z.string(), name: z.string() }))) .query(async ({ input }) => { const fetchMemberRes = await fetch( `${TRELLO_API_URL}/tokens/${input.token}/member?key=${input.apiKey}`, @@ -79,6 +87,13 @@ export const importRouter = createTRPCRouter({ })); }), importBoards: protectedProcedure + .meta({ + openapi: { + summary: "Import boards from Trello", + method: "POST", + path: "/import", + }, + }) .input( z.object({ boardIds: z.array(z.string()), @@ -87,6 +102,7 @@ export const importRouter = createTRPCRouter({ workspacePublicId: z.string().min(12), }), ) + .output(z.object({ boardsCreated: z.number() })) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; diff --git a/src/server/api/routers/label.ts b/src/server/api/routers/label.ts index bb741c8e..353c59ed 100644 --- a/src/server/api/routers/label.ts +++ b/src/server/api/routers/label.ts @@ -8,7 +8,15 @@ import * as labelRepo from "~/server/db/repository/label.repo"; export const labelRouter = createTRPCRouter({ byPublicId: protectedProcedure + .meta({ + openapi: { + summary: "Get a label by public ID", + method: "GET", + path: "/{publicId}", + }, + }) .input(z.object({ publicId: z.string().min(12) })) + .output(z.custom>>()) .query(async ({ ctx, input }) => { const label = await labelRepo.getByPublicId(ctx.db, input.publicId); @@ -21,6 +29,13 @@ export const labelRouter = createTRPCRouter({ return label; }), create: protectedProcedure + .meta({ + openapi: { + summary: "Create a label", + method: "POST", + path: "/create", + }, + }) .input( z.object({ name: z.string().min(1).max(36), @@ -28,6 +43,7 @@ export const labelRouter = createTRPCRouter({ colourCode: z.string().length(7), }), ) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -55,9 +71,22 @@ export const labelRouter = createTRPCRouter({ boardId: card.list.boardId, }); + if (!result) + throw new TRPCError({ + message: `Failed to create label`, + code: "INTERNAL_SERVER_ERROR", + }); + return result; }), update: protectedProcedure + .meta({ + openapi: { + summary: "Update a label", + method: "PUT", + path: "/{publicId}", + }, + }) .input( z.object({ publicId: z.string().min(12), @@ -65,13 +94,22 @@ export const labelRouter = createTRPCRouter({ colourCode: z.string().length(7), }), ) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { const result = await labelRepo.update(ctx.db, input); return result; }), delete: protectedProcedure + .meta({ + openapi: { + summary: "Delete a label", + method: "DELETE", + path: "/{publicId}", + }, + }) .input(z.object({ publicId: z.string().min(12) })) + .output(z.object({ success: z.boolean() })) .mutation(async ({ ctx, input }) => { const label = await labelRepo.getByPublicId(ctx.db, input.publicId); diff --git a/src/server/api/routers/list.ts b/src/server/api/routers/list.ts index 9b4b489d..87516fdc 100644 --- a/src/server/api/routers/list.ts +++ b/src/server/api/routers/list.ts @@ -9,12 +9,20 @@ import * as listRepo from "~/server/db/repository/list.repo"; export const listRouter = createTRPCRouter({ create: protectedProcedure + .meta({ + openapi: { + summary: "Create a list", + method: "POST", + path: "/list/create", + }, + }) .input( z.object({ name: z.string().min(1), boardPublicId: z.string().min(12), }), ) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -44,9 +52,22 @@ export const listRouter = createTRPCRouter({ index: latestListIndex ? latestListIndex + 1 : 0, }); + if (!result) + throw new TRPCError({ + message: `Failed to create list`, + code: "INTERNAL_SERVER_ERROR", + }); + return result; }), reorder: protectedProcedure + .meta({ + openapi: { + summary: "Reorder a list", + method: "POST", + path: "/{listId}/reorder", + }, + }) .input( z.object({ boardId: z.string().min(12), @@ -55,6 +76,7 @@ export const listRouter = createTRPCRouter({ newIndex: z.number(), }), ) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { const list = await listRepo.getByPublicId(ctx.db, input.listId); @@ -64,21 +86,35 @@ export const listRouter = createTRPCRouter({ code: "NOT_FOUND", }); - const result = listRepo.reorder(ctx.db, { + const result = await listRepo.reorder(ctx.db, { boardPublicId: list.boardId, listPublicId: list.id, currentIndex: input.currentIndex, newIndex: input.newIndex, }); + if (!result) + throw new TRPCError({ + message: `Failed to reorder list`, + code: "INTERNAL_SERVER_ERROR", + }); + return result; }), delete: protectedProcedure + .meta({ + openapi: { + summary: "Delete a list", + method: "DELETE", + path: "/{listPublicId}", + }, + }) .input( z.object({ listPublicId: z.string().min(12), }), ) + .output(z.object({ success: z.boolean() })) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -114,14 +150,24 @@ export const listRouter = createTRPCRouter({ boardId: list.boardId, listIndex: list.id, }); + + return { success: true }; }), update: protectedProcedure + .meta({ + openapi: { + summary: "Update a list", + method: "PUT", + path: "/list/{listPublicId}", + }, + }) .input( z.object({ listPublicId: z.string().min(12), name: z.string().min(1), }), ) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { const result = await listRepo.update( ctx.db, @@ -129,6 +175,12 @@ export const listRouter = createTRPCRouter({ { listPublicId: input.listPublicId }, ); + if (!result) + throw new TRPCError({ + message: `Failed to update list`, + code: "INTERNAL_SERVER_ERROR", + }); + return result; }), }); diff --git a/src/server/api/routers/member.ts b/src/server/api/routers/member.ts index 785fc241..b286f03f 100644 --- a/src/server/api/routers/member.ts +++ b/src/server/api/routers/member.ts @@ -11,12 +11,20 @@ import { sendEmail } from "~/email/sendEmail"; export const memberRouter = createTRPCRouter({ invite: protectedProcedure + .meta({ + openapi: { + summary: "Invite a member to a workspace", + method: "POST", + path: "/invite", + }, + }) .input( z.object({ email: z.string().email(), workspacePublicId: z.string().min(12), }), ) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -132,11 +140,19 @@ export const memberRouter = createTRPCRouter({ return invite; }), delete: protectedProcedure + .meta({ + openapi: { + summary: "Delete a member from a workspace", + method: "DELETE", + path: "/{memberPublicId}", + }, + }) .input( z.object({ memberPublicId: z.string().min(12), }), ) + .output(z.object({ success: z.boolean() })) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -163,6 +179,12 @@ export const memberRouter = createTRPCRouter({ deletedBy: userId, }); - return deletedMember; + if (!deletedMember) + throw new TRPCError({ + message: `Failed to delete member with public ID ${input.memberPublicId}`, + code: "INTERNAL_SERVER_ERROR", + }); + + return { success: true }; }), }); diff --git a/src/server/api/routers/workspace.ts b/src/server/api/routers/workspace.ts index b2c87d37..c8ae67f8 100644 --- a/src/server/api/routers/workspace.ts +++ b/src/server/api/routers/workspace.ts @@ -5,35 +5,73 @@ import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import * as workspaceRepo from "~/server/db/repository/workspace.repo"; export const workspaceRouter = createTRPCRouter({ - all: protectedProcedure.query(async ({ ctx }) => { - const userId = ctx.user?.id; + all: protectedProcedure + .meta({ + openapi: { + summary: "Get all workspaces", + method: "GET", + path: "/", + }, + }) + .input(z.void()) + .output( + z.custom>>(), + ) + .query(async ({ ctx }) => { + const userId = ctx.user?.id; - if (!userId) - throw new TRPCError({ - message: `User not authenticated`, - code: "UNAUTHORIZED", - }); + if (!userId) + throw new TRPCError({ + message: `User not authenticated`, + code: "UNAUTHORIZED", + }); - const result = await workspaceRepo.getAllByUserId(ctx.db, userId); + const result = await workspaceRepo.getAllByUserId(ctx.db, userId); - return result; - }), + return result; + }), byId: protectedProcedure + .meta({ + openapi: { + summary: "Get a workspace by public ID", + method: "GET", + path: "/workspace/{publicId}", + }, + }) .input(z.object({ publicId: z.string().min(12) })) + .output( + z.custom< + Awaited> + >(), + ) .query(async ({ ctx, input }) => { const result = await workspaceRepo.getByPublicIdWithMembers( ctx.db, input.publicId, ); + if (!result) + throw new TRPCError({ + message: `Workspace not found`, + code: "NOT_FOUND", + }); + return result; }), create: protectedProcedure + .meta({ + openapi: { + summary: "Create a workspace", + method: "POST", + path: "/workspace/create", + }, + }) .input( z.object({ name: z.string().min(1), }), ) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; @@ -58,12 +96,20 @@ export const workspaceRouter = createTRPCRouter({ return result; }), update: protectedProcedure + .meta({ + openapi: { + summary: "Update a workspace", + method: "PUT", + path: "/workspace/{workspacePublicId}", + }, + }) .input( z.object({ workspacePublicId: z.string().min(12), name: z.string().min(3).max(24), }), ) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { const result = await workspaceRepo.update( ctx.db, @@ -74,13 +120,27 @@ export const workspaceRouter = createTRPCRouter({ return result; }), delete: protectedProcedure + .meta({ + openapi: { + summary: "Delete a workspace", + method: "DELETE", + path: "/workspace/{workspacePublicId}", + }, + }) .input(z.object({ workspacePublicId: z.string().min(12) })) + .output(z.custom>>()) .mutation(async ({ ctx, input }) => { - const { data } = await workspaceRepo.hardDelete( + const result = await workspaceRepo.hardDelete( ctx.db, input.workspacePublicId, ); - return data; + if (!result) + throw new TRPCError({ + message: `Unable to delete workspace`, + code: "INTERNAL_SERVER_ERROR", + }); + + return result; }), }); diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index b5ef9d07..4d8a59bf 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -7,11 +7,18 @@ * need to use are documented accordingly near the end. */ import { initTRPC, TRPCError } from "@trpc/server"; +import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; import { type FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch"; +import { type OpenApiMeta } from "trpc-to-openapi"; + import superjson from "superjson"; import { ZodError } from "zod"; -import { createTRPCClient, createTRPCAdminClient } from "~/utils/supabase/api"; +import { + createNextApiClient, + createTRPCClient, + createTRPCAdminClient, +} from "~/utils/supabase/api"; import { type Database } from "~/types/database.types"; import { type SupabaseClient } from "@supabase/supabase-js"; @@ -72,6 +79,20 @@ export const createTRPCContext = async ({ return createInnerTRPCContext({ db, adminDb, user }); }; +export const createRESTContext = async ({ + req, + res, +}: CreateNextContextOptions) => { + const db = createNextApiClient(req, res); + const adminDb = createTRPCAdminClient(); + + const { + data: { user }, + } = await db.auth.getUser(); + + return createInnerTRPCContext({ db, adminDb, user }); +}; + /** * 2. INITIALIZATION * @@ -80,19 +101,22 @@ export const createTRPCContext = async ({ * errors on the backend. */ -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); +const t = initTRPC + .context() + .meta() + .create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, + }); /** * Create a server-side caller. @@ -122,7 +146,9 @@ export const createTRPCRouter = t.router; * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ -export const publicProcedure = t.procedure; +export const publicProcedure = t.procedure.meta({ + openapi: { method: "GET", path: "/public" }, +}); /** Reusable middleware that enforces users are logged in before running the procedure. */ const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => { @@ -143,4 +169,9 @@ const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => { * * @see https://trpc.io/docs/procedures */ -export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); +export const protectedProcedure = t.procedure.use(enforceUserIsAuthed).meta({ + openapi: { + method: "GET", + path: "/protected", + }, +}); diff --git a/src/server/db/repository/board.repo.ts b/src/server/db/repository/board.repo.ts index 6bcfb85d..0a93eda5 100644 --- a/src/server/db/repository/board.repo.ts +++ b/src/server/db/repository/board.repo.ts @@ -12,7 +12,7 @@ export const getAllByWorkspaceId = async ( .is("deletedAt", null) .eq("workspaceId", workspaceId); - return data; + return data ?? []; }; export const getByPublicId = async ( @@ -157,7 +157,10 @@ export const update = async ( const { data } = await db .from("board") .update({ name: boardInput.name }) - .eq("publicId", boardInput.boardPublicId); + .eq("publicId", boardInput.boardPublicId) + .select(`publicId, name`) + .limit(1) + .single(); return data; }; diff --git a/src/server/db/repository/workspace.repo.ts b/src/server/db/repository/workspace.repo.ts index 54384914..2eb8d30b 100644 --- a/src/server/db/repository/workspace.repo.ts +++ b/src/server/db/repository/workspace.repo.ts @@ -102,7 +102,7 @@ export const getAllByUserId = async ( db: SupabaseClient, userId: string, ) => { - const { data, error } = await db + const { data } = await db .from("workspace_members") .select( ` @@ -116,9 +116,7 @@ export const getAllByUserId = async ( .eq("userId", userId) .is("deletedAt", null); - console.log({ error }); - - return data; + return data ?? []; }; export const getMemberByPublicId = async ( diff --git a/src/server/openapi.ts b/src/server/openapi.ts new file mode 100644 index 00000000..7334d148 --- /dev/null +++ b/src/server/openapi.ts @@ -0,0 +1,12 @@ +import { generateOpenApiDocument } from "trpc-to-openapi"; + +import { appRouter } from "~/server/api/root"; + +export const openApiDocument = generateOpenApiDocument(appRouter, { + title: "Kan API", + description: "OpenAPI compliant REST API", + version: "1.0.0", + baseUrl: "http://localhost:3000/api/v1", + docsUrl: "", + tags: ["auth", "users", "posts"], +}); diff --git a/src/utils/supabase/api.ts b/src/utils/supabase/api.ts index 8654dcb7..0f8f2715 100644 --- a/src/utils/supabase/api.ts +++ b/src/utils/supabase/api.ts @@ -6,6 +6,7 @@ import { import { RequestCookies } from "@edge-runtime/cookies"; import { type Database } from "~/types/database.types"; +import { type NextApiRequest, type NextApiResponse } from "next"; import { type NextRequest, type NextResponse } from "next/server"; export function createNextClient(req: NextRequest, res: NextResponse) { @@ -30,6 +31,32 @@ export function createNextClient(req: NextRequest, res: NextResponse) { return supabase; } +export function createNextApiClient( + _req: NextApiRequest, + _res: NextApiResponse, +) { + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_API_KEY!, + { + cookies: { + get(_name: string) { + // return req.cookies.get(name)?.value; + return ""; + }, + set(_name: string, _value: string, _options: CookieOptions) { + // res.headers.append("Set-Cookie", serialize(name, value, options)); + }, + remove(_name: string, _options: CookieOptions) { + // res.headers.append("Set-Cookie", serialize(name, "", options)); + }, + }, + }, + ); + + return supabase; +} + export function createTRPCClient(req: Request, resHeaders: Headers) { const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, diff --git a/src/views/board/index.tsx b/src/views/board/index.tsx index 96e685c2..634f503c 100644 --- a/src/views/board/index.tsx +++ b/src/views/board/index.tsx @@ -61,10 +61,8 @@ export default function BoardPage() { const { data, isSuccess, isLoading } = api.board.byId.useQuery( { boardPublicId: boardId ?? "", - filters: { - members: formatToArray(router.query.members), - labels: formatToArray(router.query.labels), - }, + members: formatToArray(router.query.members), + labels: formatToArray(router.query.labels), }, { enabled: !!boardId, diff --git a/src/views/card/components/LabelForm.tsx b/src/views/card/components/LabelForm.tsx index 586276a9..a1335067 100644 --- a/src/views/card/components/LabelForm.tsx +++ b/src/views/card/components/LabelForm.tsx @@ -54,9 +54,9 @@ export function LabelForm({ const { control, register, reset, handleSubmit, setValue, watch } = useForm({ values: { - name: isEdit && label.data?.name ? label.data.name : "", - colour: (isEdit && label.data?.colourCode - ? colours.find((c) => c.code === label.data.colourCode) + name: isEdit && label?.data?.name ? label?.data?.name : "", + colour: (isEdit && label?.data?.colourCode + ? colours.find((c) => c.code === label?.data?.colourCode) : colours[0]) as Colour, isCreateAnotherEnabled: false, },