feat: setup openapi compliant rest api

This commit is contained in:
Henry
2024-11-01 08:23:39 +00:00
parent 8a7613382f
commit d5eece3ab8
19 changed files with 547 additions and 61 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -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": {

View File

@@ -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 ?? "<no-path>"}: ${error.message}`,
);
}
: undefined,
});
return await openApiHandler(req, res);
}

View File

@@ -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;

View File

@@ -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 };
}),
});

View File

@@ -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<Awaited<ReturnType<typeof boardRepo.getAllByWorkspaceId>>>(),
)
.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<Awaited<ReturnType<typeof boardRepo.getByPublicId>>>())
.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<Awaited<ReturnType<typeof boardRepo.create>>>())
.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<Awaited<ReturnType<typeof boardRepo.update>>>())
.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 };
}),
});

View File

@@ -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<Awaited<ReturnType<typeof cardRepo.create>>>())
.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<ReturnType<typeof cardRepo.getWithListAndMembersByPublicId>>
>(),
)
.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<Awaited<ReturnType<typeof cardRepo.update>>>())
.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;

View File

@@ -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;

View File

@@ -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<Awaited<ReturnType<typeof labelRepo.getByPublicId>>>())
.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<Awaited<ReturnType<typeof labelRepo.create>>>())
.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<Awaited<ReturnType<typeof labelRepo.update>>>())
.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);

View File

@@ -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<Awaited<ReturnType<typeof listRepo.create>>>())
.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<Awaited<ReturnType<typeof listRepo.reorder>>>())
.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<Awaited<ReturnType<typeof listRepo.update>>>())
.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;
}),
});

View File

@@ -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<Awaited<ReturnType<typeof memberRepo.create>>>())
.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 };
}),
});

View File

@@ -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<Awaited<ReturnType<typeof workspaceRepo.getAllByUserId>>>(),
)
.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<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>
>(),
)
.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<Awaited<ReturnType<typeof workspaceRepo.create>>>())
.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<Awaited<ReturnType<typeof workspaceRepo.update>>>())
.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<Awaited<ReturnType<typeof workspaceRepo.hardDelete>>>())
.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;
}),
});

View File

@@ -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<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
const t = initTRPC
.context<typeof createTRPCContext>()
.meta<OpenApiMeta>()
.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",
},
});

View File

@@ -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;
};

View File

@@ -102,7 +102,7 @@ export const getAllByUserId = async (
db: SupabaseClient<Database>,
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 (

12
src/server/openapi.ts Normal file
View File

@@ -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"],
});

View File

@@ -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<Database>(
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<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,

View File

@@ -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,

View File

@@ -54,9 +54,9 @@ export function LabelForm({
const { control, register, reset, handleSubmit, setValue, watch } =
useForm<LabelFormInput>({
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,
},