feat: setup openapi compliant rest api
This commit is contained in:
@@ -32,6 +32,7 @@
|
|||||||
"@vercel/postgres": "^0.7.2",
|
"@vercel/postgres": "^0.7.2",
|
||||||
"drizzle-orm": "^0.28.5",
|
"drizzle-orm": "^0.28.5",
|
||||||
"next": "^14.1.3",
|
"next": "^14.1.3",
|
||||||
|
"nextjs-cors": "^2.2.0",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
"react-lottie-player": "^1.5.5",
|
"react-lottie-player": "^1.5.5",
|
||||||
"superjson": "^1.13.1",
|
"superjson": "^1.13.1",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
|
"trpc-to-openapi": "^2.0.2",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
30
src/pages/api/v1/[trpc].ts
Normal file
30
src/pages/api/v1/[trpc].ts
Normal 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);
|
||||||
|
}
|
||||||
9
src/pages/api/v1/openapi.json.ts
Normal file
9
src/pages/api/v1/openapi.json.ts
Normal 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;
|
||||||
@@ -12,21 +12,52 @@ import { env } from "~/env.mjs";
|
|||||||
import * as userRepo from "~/server/db/repository/user.repo";
|
import * as userRepo from "~/server/db/repository/user.repo";
|
||||||
|
|
||||||
export const authRouter = createTRPCRouter({
|
export const authRouter = createTRPCRouter({
|
||||||
getUser: protectedProcedure.query(async ({ ctx }) => {
|
getUser: protectedProcedure
|
||||||
const userId = ctx.user?.id;
|
.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)
|
if (!userId)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
message: `User not authenticated`,
|
message: `User not authenticated`,
|
||||||
code: "UNAUTHORIZED",
|
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
|
loginWithEmail: publicProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "POST",
|
||||||
|
path: "/auth/email",
|
||||||
|
summary: "Login with email",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(z.object({ email: z.string() }))
|
.input(z.object({ email: z.string() }))
|
||||||
|
.output(z.object({ success: z.boolean() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { data } = await ctx.db.auth.signInWithOtp({
|
const { data } = await ctx.db.auth.signInWithOtp({
|
||||||
email: input.email,
|
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
|
loginWithOAuth: publicProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "POST",
|
||||||
|
path: "/auth/oauth",
|
||||||
|
summary: "Login with OAuth",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(z.object({ provider: z.string() }))
|
.input(z.object({ provider: z.string() }))
|
||||||
|
.output(z.object({ url: z.string() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (input.provider !== "google")
|
if (input.provider !== "google")
|
||||||
throw new TRPCError({
|
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 };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,17 @@ import * as workspaceRepo from "~/server/db/repository/workspace.repo";
|
|||||||
|
|
||||||
export const boardRouter = createTRPCRouter({
|
export const boardRouter = createTRPCRouter({
|
||||||
all: protectedProcedure
|
all: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "GET",
|
||||||
|
path: "/board/{workspacePublicId}",
|
||||||
|
summary: "Get all boards",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||||
|
.output(
|
||||||
|
z.custom<Awaited<ReturnType<typeof boardRepo.getAllByWorkspaceId>>>(),
|
||||||
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const workspace = await workspaceRepo.getByPublicId(
|
const workspace = await workspaceRepo.getByPublicId(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
@@ -28,31 +38,48 @@ export const boardRouter = createTRPCRouter({
|
|||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
byId: protectedProcedure
|
byId: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "GET",
|
||||||
|
path: "/board/{boardPublicId}",
|
||||||
|
summary: "Get board by public ID",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
boardPublicId: z.string().min(12),
|
boardPublicId: z.string().min(12),
|
||||||
filters: z.object({
|
members: z.array(z.string().min(12)),
|
||||||
members: z.array(z.string().min(12)),
|
labels: 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 }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const result = await boardRepo.getByPublicId(
|
const result = await boardRepo.getByPublicId(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
input.boardPublicId,
|
input.boardPublicId,
|
||||||
input.filters,
|
{
|
||||||
|
members: input.members,
|
||||||
|
labels: input.labels,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "POST",
|
||||||
|
path: "/board",
|
||||||
|
summary: "Create board",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
workspacePublicId: z.string().min(12),
|
workspacePublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof boardRepo.create>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -79,29 +106,57 @@ export const boardRouter = createTRPCRouter({
|
|||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Failed to create board`,
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "PUT",
|
||||||
|
path: "/board/{boardPublicId}",
|
||||||
|
summary: "Update board",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
boardPublicId: z.string().min(12),
|
boardPublicId: z.string().min(12),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof boardRepo.update>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const result = await boardRepo.update(ctx.db, {
|
const result = await boardRepo.update(ctx.db, {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
boardPublicId: input.boardPublicId,
|
boardPublicId: input.boardPublicId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Failed to update board`,
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "DELETE",
|
||||||
|
path: "/board/{boardPublicId}",
|
||||||
|
summary: "Delete board",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
boardPublicId: z.string().min(12),
|
boardPublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.object({ success: z.boolean() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -145,5 +200,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
deletedBy: userId,
|
deletedBy: userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ import * as workspaceRepo from "~/server/db/repository/workspace.repo";
|
|||||||
|
|
||||||
export const cardRouter = createTRPCRouter({
|
export const cardRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Create a card",
|
||||||
|
method: "POST",
|
||||||
|
path: "/",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
title: z.string().min(1),
|
title: z.string().min(1),
|
||||||
@@ -20,6 +27,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
position: z.enum(["start", "end"]),
|
position: z.enum(["start", "end"]),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof cardRepo.create>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -65,6 +73,12 @@ export const cardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const newCardId = newCard?.id;
|
const newCardId = newCard?.id;
|
||||||
|
|
||||||
|
if (!newCardId)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Failed to create card`,
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
if (newCardId && input.labelPublicIds.length) {
|
if (newCardId && input.labelPublicIds.length) {
|
||||||
const labels = await labelRepo.getAllByPublicIds(
|
const labels = await labelRepo.getAllByPublicIds(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
@@ -111,12 +125,20 @@ export const cardRouter = createTRPCRouter({
|
|||||||
return newCard;
|
return newCard;
|
||||||
}),
|
}),
|
||||||
addOrRemoveLabel: protectedProcedure
|
addOrRemoveLabel: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Add or remove a label from a card",
|
||||||
|
method: "POST",
|
||||||
|
path: "/{cardPublicId}/label/{labelPublicId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
cardPublicId: z.string().min(12),
|
cardPublicId: z.string().min(12),
|
||||||
labelPublicId: z.string().min(12),
|
labelPublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.object({ newLabel: z.boolean() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -159,12 +181,20 @@ export const cardRouter = createTRPCRouter({
|
|||||||
return { newLabel: true };
|
return { newLabel: true };
|
||||||
}),
|
}),
|
||||||
addOrRemoveMember: protectedProcedure
|
addOrRemoveMember: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Add or remove a member from a card",
|
||||||
|
method: "POST",
|
||||||
|
path: "/{cardPublicId}/member/{workspaceMemberPublicId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
cardPublicId: z.string().min(12),
|
cardPublicId: z.string().min(12),
|
||||||
workspaceMemberPublicId: z.string().min(12),
|
workspaceMemberPublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.object({ newMember: z.boolean() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -210,16 +240,41 @@ export const cardRouter = createTRPCRouter({
|
|||||||
return { newMember: true };
|
return { newMember: true };
|
||||||
}),
|
}),
|
||||||
byId: protectedProcedure
|
byId: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Get a card by ID",
|
||||||
|
method: "GET",
|
||||||
|
path: "/{id}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(z.object({ id: z.string().min(12) }))
|
.input(z.object({ id: z.string().min(12) }))
|
||||||
|
.output(
|
||||||
|
z.custom<
|
||||||
|
Awaited<ReturnType<typeof cardRepo.getWithListAndMembersByPublicId>>
|
||||||
|
>(),
|
||||||
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const result = await cardRepo.getWithListAndMembersByPublicId(
|
const result = await cardRepo.getWithListAndMembersByPublicId(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
input.id,
|
input.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Card with ID ${input.id} not found`,
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Update a card",
|
||||||
|
method: "PUT",
|
||||||
|
path: "/{cardId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
cardId: z.string().min(12),
|
cardId: z.string().min(12),
|
||||||
@@ -227,6 +282,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
description: z.string(),
|
description: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof cardRepo.update>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -236,20 +292,34 @@ export const cardRouter = createTRPCRouter({
|
|||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = cardRepo.update(
|
const result = await cardRepo.update(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
{ title: input.title, description: input.description },
|
{ title: input.title, description: input.description },
|
||||||
{ cardPublicId: input.cardId },
|
{ cardPublicId: input.cardId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Failed to update card`,
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Delete a card",
|
||||||
|
method: "DELETE",
|
||||||
|
path: "/{cardPublicId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
cardPublicId: z.string().min(12),
|
cardPublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.object({ success: z.boolean() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -282,8 +352,17 @@ export const cardRouter = createTRPCRouter({
|
|||||||
listId: card.list.id,
|
listId: card.list.id,
|
||||||
cardIndex: card.index,
|
cardIndex: card.index,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
reorder: protectedProcedure
|
reorder: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Reorder a card",
|
||||||
|
method: "POST",
|
||||||
|
path: "/{cardId}/reorder",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
cardId: z.string().min(12),
|
cardId: z.string().min(12),
|
||||||
@@ -291,6 +370,7 @@ export const cardRouter = createTRPCRouter({
|
|||||||
newIndex: z.number().optional(),
|
newIndex: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.object({ success: z.boolean() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -37,12 +37,20 @@ interface MemberData {
|
|||||||
export const importRouter = createTRPCRouter({
|
export const importRouter = createTRPCRouter({
|
||||||
trello: createTRPCRouter({
|
trello: createTRPCRouter({
|
||||||
getBoards: protectedProcedure
|
getBoards: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Get boards from Trello",
|
||||||
|
method: "GET",
|
||||||
|
path: "/boards",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
apiKey: z.string().length(32),
|
apiKey: z.string().length(32),
|
||||||
token: z.string().length(76),
|
token: z.string().length(76),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.array(z.object({ id: z.string(), name: z.string() })))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const fetchMemberRes = await fetch(
|
const fetchMemberRes = await fetch(
|
||||||
`${TRELLO_API_URL}/tokens/${input.token}/member?key=${input.apiKey}`,
|
`${TRELLO_API_URL}/tokens/${input.token}/member?key=${input.apiKey}`,
|
||||||
@@ -79,6 +87,13 @@ export const importRouter = createTRPCRouter({
|
|||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
importBoards: protectedProcedure
|
importBoards: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Import boards from Trello",
|
||||||
|
method: "POST",
|
||||||
|
path: "/import",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
boardIds: z.array(z.string()),
|
boardIds: z.array(z.string()),
|
||||||
@@ -87,6 +102,7 @@ export const importRouter = createTRPCRouter({
|
|||||||
workspacePublicId: z.string().min(12),
|
workspacePublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.object({ boardsCreated: z.number() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,15 @@ import * as labelRepo from "~/server/db/repository/label.repo";
|
|||||||
|
|
||||||
export const labelRouter = createTRPCRouter({
|
export const labelRouter = createTRPCRouter({
|
||||||
byPublicId: protectedProcedure
|
byPublicId: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Get a label by public ID",
|
||||||
|
method: "GET",
|
||||||
|
path: "/{publicId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(z.object({ publicId: z.string().min(12) }))
|
.input(z.object({ publicId: z.string().min(12) }))
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof labelRepo.getByPublicId>>>())
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const label = await labelRepo.getByPublicId(ctx.db, input.publicId);
|
const label = await labelRepo.getByPublicId(ctx.db, input.publicId);
|
||||||
|
|
||||||
@@ -21,6 +29,13 @@ export const labelRouter = createTRPCRouter({
|
|||||||
return label;
|
return label;
|
||||||
}),
|
}),
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Create a label",
|
||||||
|
method: "POST",
|
||||||
|
path: "/create",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1).max(36),
|
name: z.string().min(1).max(36),
|
||||||
@@ -28,6 +43,7 @@ export const labelRouter = createTRPCRouter({
|
|||||||
colourCode: z.string().length(7),
|
colourCode: z.string().length(7),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof labelRepo.create>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -55,9 +71,22 @@ export const labelRouter = createTRPCRouter({
|
|||||||
boardId: card.list.boardId,
|
boardId: card.list.boardId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Failed to create label`,
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Update a label",
|
||||||
|
method: "PUT",
|
||||||
|
path: "/{publicId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
publicId: z.string().min(12),
|
publicId: z.string().min(12),
|
||||||
@@ -65,13 +94,22 @@ export const labelRouter = createTRPCRouter({
|
|||||||
colourCode: z.string().length(7),
|
colourCode: z.string().length(7),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof labelRepo.update>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const result = await labelRepo.update(ctx.db, input);
|
const result = await labelRepo.update(ctx.db, input);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Delete a label",
|
||||||
|
method: "DELETE",
|
||||||
|
path: "/{publicId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(z.object({ publicId: z.string().min(12) }))
|
.input(z.object({ publicId: z.string().min(12) }))
|
||||||
|
.output(z.object({ success: z.boolean() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const label = await labelRepo.getByPublicId(ctx.db, input.publicId);
|
const label = await labelRepo.getByPublicId(ctx.db, input.publicId);
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,20 @@ import * as listRepo from "~/server/db/repository/list.repo";
|
|||||||
|
|
||||||
export const listRouter = createTRPCRouter({
|
export const listRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Create a list",
|
||||||
|
method: "POST",
|
||||||
|
path: "/list/create",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
boardPublicId: z.string().min(12),
|
boardPublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof listRepo.create>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -44,9 +52,22 @@ export const listRouter = createTRPCRouter({
|
|||||||
index: latestListIndex ? latestListIndex + 1 : 0,
|
index: latestListIndex ? latestListIndex + 1 : 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Failed to create list`,
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
reorder: protectedProcedure
|
reorder: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Reorder a list",
|
||||||
|
method: "POST",
|
||||||
|
path: "/{listId}/reorder",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
boardId: z.string().min(12),
|
boardId: z.string().min(12),
|
||||||
@@ -55,6 +76,7 @@ export const listRouter = createTRPCRouter({
|
|||||||
newIndex: z.number(),
|
newIndex: z.number(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof listRepo.reorder>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const list = await listRepo.getByPublicId(ctx.db, input.listId);
|
const list = await listRepo.getByPublicId(ctx.db, input.listId);
|
||||||
|
|
||||||
@@ -64,21 +86,35 @@ export const listRouter = createTRPCRouter({
|
|||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = listRepo.reorder(ctx.db, {
|
const result = await listRepo.reorder(ctx.db, {
|
||||||
boardPublicId: list.boardId,
|
boardPublicId: list.boardId,
|
||||||
listPublicId: list.id,
|
listPublicId: list.id,
|
||||||
currentIndex: input.currentIndex,
|
currentIndex: input.currentIndex,
|
||||||
newIndex: input.newIndex,
|
newIndex: input.newIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Failed to reorder list`,
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Delete a list",
|
||||||
|
method: "DELETE",
|
||||||
|
path: "/{listPublicId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
listPublicId: z.string().min(12),
|
listPublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.object({ success: z.boolean() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -114,14 +150,24 @@ export const listRouter = createTRPCRouter({
|
|||||||
boardId: list.boardId,
|
boardId: list.boardId,
|
||||||
listIndex: list.id,
|
listIndex: list.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Update a list",
|
||||||
|
method: "PUT",
|
||||||
|
path: "/list/{listPublicId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
listPublicId: z.string().min(12),
|
listPublicId: z.string().min(12),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof listRepo.update>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const result = await listRepo.update(
|
const result = await listRepo.update(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
@@ -129,6 +175,12 @@ export const listRouter = createTRPCRouter({
|
|||||||
{ listPublicId: input.listPublicId },
|
{ listPublicId: input.listPublicId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Failed to update list`,
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,12 +11,20 @@ import { sendEmail } from "~/email/sendEmail";
|
|||||||
|
|
||||||
export const memberRouter = createTRPCRouter({
|
export const memberRouter = createTRPCRouter({
|
||||||
invite: protectedProcedure
|
invite: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Invite a member to a workspace",
|
||||||
|
method: "POST",
|
||||||
|
path: "/invite",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
workspacePublicId: z.string().min(12),
|
workspacePublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof memberRepo.create>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -132,11 +140,19 @@ export const memberRouter = createTRPCRouter({
|
|||||||
return invite;
|
return invite;
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Delete a member from a workspace",
|
||||||
|
method: "DELETE",
|
||||||
|
path: "/{memberPublicId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
memberPublicId: z.string().min(12),
|
memberPublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.object({ success: z.boolean() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -163,6 +179,12 @@ export const memberRouter = createTRPCRouter({
|
|||||||
deletedBy: userId,
|
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 };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,35 +5,73 @@ import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
|||||||
import * as workspaceRepo from "~/server/db/repository/workspace.repo";
|
import * as workspaceRepo from "~/server/db/repository/workspace.repo";
|
||||||
|
|
||||||
export const workspaceRouter = createTRPCRouter({
|
export const workspaceRouter = createTRPCRouter({
|
||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure
|
||||||
const userId = ctx.user?.id;
|
.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)
|
if (!userId)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
message: `User not authenticated`,
|
message: `User not authenticated`,
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await workspaceRepo.getAllByUserId(ctx.db, userId);
|
const result = await workspaceRepo.getAllByUserId(ctx.db, userId);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
byId: protectedProcedure
|
byId: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Get a workspace by public ID",
|
||||||
|
method: "GET",
|
||||||
|
path: "/workspace/{publicId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(z.object({ publicId: z.string().min(12) }))
|
.input(z.object({ publicId: z.string().min(12) }))
|
||||||
|
.output(
|
||||||
|
z.custom<
|
||||||
|
Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>
|
||||||
|
>(),
|
||||||
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const result = await workspaceRepo.getByPublicIdWithMembers(
|
const result = await workspaceRepo.getByPublicIdWithMembers(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
input.publicId,
|
input.publicId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Workspace not found`,
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Create a workspace",
|
||||||
|
method: "POST",
|
||||||
|
path: "/workspace/create",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.create>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user?.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
@@ -58,12 +96,20 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Update a workspace",
|
||||||
|
method: "PUT",
|
||||||
|
path: "/workspace/{workspacePublicId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
workspacePublicId: z.string().min(12),
|
workspacePublicId: z.string().min(12),
|
||||||
name: z.string().min(3).max(24),
|
name: z.string().min(3).max(24),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.update>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const result = await workspaceRepo.update(
|
const result = await workspaceRepo.update(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
@@ -74,13 +120,27 @@ export const workspaceRouter = createTRPCRouter({
|
|||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Delete a workspace",
|
||||||
|
method: "DELETE",
|
||||||
|
path: "/workspace/{workspacePublicId}",
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.hardDelete>>>())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { data } = await workspaceRepo.hardDelete(
|
const result = await workspaceRepo.hardDelete(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
input.workspacePublicId,
|
input.workspacePublicId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
if (!result)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Unable to delete workspace`,
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,11 +7,18 @@
|
|||||||
* need to use are documented accordingly near the end.
|
* need to use are documented accordingly near the end.
|
||||||
*/
|
*/
|
||||||
import { initTRPC, TRPCError } from "@trpc/server";
|
import { initTRPC, TRPCError } from "@trpc/server";
|
||||||
|
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||||
import { type FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
|
import { type FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
|
||||||
|
import { type OpenApiMeta } from "trpc-to-openapi";
|
||||||
|
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { ZodError } from "zod";
|
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 Database } from "~/types/database.types";
|
||||||
import { type SupabaseClient } from "@supabase/supabase-js";
|
import { type SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
@@ -72,6 +79,20 @@ export const createTRPCContext = async ({
|
|||||||
return createInnerTRPCContext({ db, adminDb, user });
|
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
|
* 2. INITIALIZATION
|
||||||
*
|
*
|
||||||
@@ -80,19 +101,22 @@ export const createTRPCContext = async ({
|
|||||||
* errors on the backend.
|
* errors on the backend.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
const t = initTRPC
|
||||||
transformer: superjson,
|
.context<typeof createTRPCContext>()
|
||||||
errorFormatter({ shape, error }) {
|
.meta<OpenApiMeta>()
|
||||||
return {
|
.create({
|
||||||
...shape,
|
transformer: superjson,
|
||||||
data: {
|
errorFormatter({ shape, error }) {
|
||||||
...shape.data,
|
return {
|
||||||
zodError:
|
...shape,
|
||||||
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
data: {
|
||||||
},
|
...shape.data,
|
||||||
};
|
zodError:
|
||||||
},
|
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||||
});
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a server-side caller.
|
* 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
|
* guarantee that a user querying is authorized, but you can still access user session data if they
|
||||||
* are logged in.
|
* 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. */
|
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
||||||
const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
|
const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
|
||||||
@@ -143,4 +169,9 @@ const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
|
|||||||
*
|
*
|
||||||
* @see https://trpc.io/docs/procedures
|
* @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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const getAllByWorkspaceId = async (
|
|||||||
.is("deletedAt", null)
|
.is("deletedAt", null)
|
||||||
.eq("workspaceId", workspaceId);
|
.eq("workspaceId", workspaceId);
|
||||||
|
|
||||||
return data;
|
return data ?? [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getByPublicId = async (
|
export const getByPublicId = async (
|
||||||
@@ -157,7 +157,10 @@ export const update = async (
|
|||||||
const { data } = await db
|
const { data } = await db
|
||||||
.from("board")
|
.from("board")
|
||||||
.update({ name: boardInput.name })
|
.update({ name: boardInput.name })
|
||||||
.eq("publicId", boardInput.boardPublicId);
|
.eq("publicId", boardInput.boardPublicId)
|
||||||
|
.select(`publicId, name`)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export const getAllByUserId = async (
|
|||||||
db: SupabaseClient<Database>,
|
db: SupabaseClient<Database>,
|
||||||
userId: string,
|
userId: string,
|
||||||
) => {
|
) => {
|
||||||
const { data, error } = await db
|
const { data } = await db
|
||||||
.from("workspace_members")
|
.from("workspace_members")
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
@@ -116,9 +116,7 @@ export const getAllByUserId = async (
|
|||||||
.eq("userId", userId)
|
.eq("userId", userId)
|
||||||
.is("deletedAt", null);
|
.is("deletedAt", null);
|
||||||
|
|
||||||
console.log({ error });
|
return data ?? [];
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMemberByPublicId = async (
|
export const getMemberByPublicId = async (
|
||||||
|
|||||||
12
src/server/openapi.ts
Normal file
12
src/server/openapi.ts
Normal 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"],
|
||||||
|
});
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
import { RequestCookies } from "@edge-runtime/cookies";
|
import { RequestCookies } from "@edge-runtime/cookies";
|
||||||
import { type Database } from "~/types/database.types";
|
import { type Database } from "~/types/database.types";
|
||||||
|
|
||||||
|
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||||
import { type NextRequest, type NextResponse } from "next/server";
|
import { type NextRequest, type NextResponse } from "next/server";
|
||||||
|
|
||||||
export function createNextClient(req: NextRequest, res: NextResponse) {
|
export function createNextClient(req: NextRequest, res: NextResponse) {
|
||||||
@@ -30,6 +31,32 @@ export function createNextClient(req: NextRequest, res: NextResponse) {
|
|||||||
return supabase;
|
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) {
|
export function createTRPCClient(req: Request, resHeaders: Headers) {
|
||||||
const supabase = createServerClient<Database>(
|
const supabase = createServerClient<Database>(
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
|||||||
@@ -61,10 +61,8 @@ export default function BoardPage() {
|
|||||||
const { data, isSuccess, isLoading } = api.board.byId.useQuery(
|
const { data, isSuccess, isLoading } = api.board.byId.useQuery(
|
||||||
{
|
{
|
||||||
boardPublicId: boardId ?? "",
|
boardPublicId: boardId ?? "",
|
||||||
filters: {
|
members: formatToArray(router.query.members),
|
||||||
members: formatToArray(router.query.members),
|
labels: formatToArray(router.query.labels),
|
||||||
labels: formatToArray(router.query.labels),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!boardId,
|
enabled: !!boardId,
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ export function LabelForm({
|
|||||||
const { control, register, reset, handleSubmit, setValue, watch } =
|
const { control, register, reset, handleSubmit, setValue, watch } =
|
||||||
useForm<LabelFormInput>({
|
useForm<LabelFormInput>({
|
||||||
values: {
|
values: {
|
||||||
name: isEdit && label.data?.name ? label.data.name : "",
|
name: isEdit && label?.data?.name ? label?.data?.name : "",
|
||||||
colour: (isEdit && label.data?.colourCode
|
colour: (isEdit && label?.data?.colourCode
|
||||||
? colours.find((c) => c.code === label.data.colourCode)
|
? colours.find((c) => c.code === label?.data?.colourCode)
|
||||||
: colours[0]) as Colour,
|
: colours[0]) as Colour,
|
||||||
isCreateAnotherEnabled: false,
|
isCreateAnotherEnabled: false,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user