diff --git a/bun.lockb b/bun.lockb index 9510e147..c7680533 100755 Binary files a/bun.lockb and b/bun.lockb differ 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, },