feat: switch next auth and drizzle to supabase (#1)
* feat: add supabase * feat: setup auth * feat: test refactoring board queries * feat: convert to pages router * feat: convert board router to supabase * feat: swap out drizzle for supabase in label, list and workspace routers * feat: swap out drizzle for supabase on the import router * feat: swap drizzle for supabase on create new card * feat: switch card router to use supabase * feat: finish swapping drizzle for supabase * chore: lint all router files * fix: signout * chore: fix types
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { authRouter } from "~/server/api/routers/auth";
|
||||
import { boardRouter } from "~/server/api/routers/board";
|
||||
import { cardRouter } from "~/server/api/routers/card";
|
||||
import { labelRouter } from "~/server/api/routers/label";
|
||||
@@ -12,6 +13,7 @@ import { createTRPCRouter } from "~/server/api/trpc";
|
||||
* All routers added in /api/routers should be manually added here.
|
||||
*/
|
||||
export const appRouter = createTRPCRouter({
|
||||
auth: authRouter,
|
||||
board: boardRouter,
|
||||
card: cardRouter,
|
||||
label: labelRouter,
|
||||
@@ -22,3 +24,13 @@ export const appRouter = createTRPCRouter({
|
||||
|
||||
// export type definition of API
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
/**
|
||||
* Create a server-side caller for the tRPC API.
|
||||
* @example
|
||||
* const trpc = createCaller(createContext);
|
||||
* const res = await trpc.post.all();
|
||||
* ^? Post[]
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
// export const createCaller = createCallerFactory(appRouter);
|
||||
|
||||
18
src/server/api/routers/auth.ts
Normal file
18
src/server/api/routers/auth.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
||||
|
||||
export const authRouter = createTRPCRouter({
|
||||
login: publicProcedure
|
||||
.input(z.object({ email: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { data } = await ctx.db.auth.signInWithOtp({
|
||||
email: input.email,
|
||||
options: {
|
||||
emailRedirectTo: "/boards",
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
}),
|
||||
});
|
||||
@@ -1,203 +1,177 @@
|
||||
import { z } from "zod";
|
||||
import { and, eq, asc, isNull, inArray } from "drizzle-orm";
|
||||
|
||||
import { boards, cards, lists, workspaces } from "~/server/db/schema";
|
||||
import { generateUID } from "~/utils/generateUID";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
publicProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
|
||||
export const boardRouter = createTRPCRouter({
|
||||
all: publicProcedure
|
||||
all: protectedProcedure
|
||||
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
const workspace = await ctx.db
|
||||
.from("workspace")
|
||||
.select(`id`)
|
||||
.eq("publicId", input.workspacePublicId)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
// @todo: validate user has access to workspace
|
||||
if (!workspace.data) return;
|
||||
|
||||
if (!userId) return;
|
||||
const { data } = await ctx.db
|
||||
.from("board")
|
||||
.select(`publicId, name`)
|
||||
.is("deletedAt", null)
|
||||
.eq("workspaceId", workspace.data.id);
|
||||
|
||||
const workspace = await ctx.db.query.workspaces.findFirst({
|
||||
where: eq(workspaces.publicId, input.workspacePublicId),
|
||||
})
|
||||
|
||||
if (!workspace) return;
|
||||
|
||||
return ctx.db.query.boards.findMany({
|
||||
where: and(eq(boards.workspaceId, workspace.id), isNull(boards.deletedAt)),
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
return data;
|
||||
}),
|
||||
byId: publicProcedure
|
||||
byId: protectedProcedure
|
||||
.input(z.object({ id: z.string().min(12) }))
|
||||
.query(({ ctx, input }) =>
|
||||
ctx.db.query.boards.findFirst({
|
||||
where: and(eq(boards.publicId, input.id), isNull(boards.deletedAt)),
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
},
|
||||
with: {
|
||||
workspace: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
},
|
||||
with: {
|
||||
members: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
},
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
labels: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
colourCode: true,
|
||||
name: true,
|
||||
}
|
||||
},
|
||||
lists: {
|
||||
orderBy: [asc(lists.index)],
|
||||
where: isNull(cards.deletedAt),
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
boardId: true,
|
||||
index: true,
|
||||
},
|
||||
with: {
|
||||
cards: {
|
||||
where: isNull(cards.deletedAt),
|
||||
orderBy: [asc(cards.index)],
|
||||
columns: {
|
||||
publicId: true,
|
||||
title: true,
|
||||
description: true,
|
||||
listId: true,
|
||||
index: true,
|
||||
},
|
||||
with: {
|
||||
labels: {
|
||||
columns: {
|
||||
labelId: false,
|
||||
cardId: false,
|
||||
},
|
||||
with: {
|
||||
label: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
colourCode: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
members: {
|
||||
columns: {
|
||||
workspaceMemberId: false,
|
||||
cardId: false,
|
||||
},
|
||||
with: {
|
||||
member: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
},
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
),
|
||||
create: publicProcedure
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { data } = await ctx.db
|
||||
.from("board")
|
||||
.select(
|
||||
`
|
||||
publicId,
|
||||
name,
|
||||
workspace (
|
||||
publicId,
|
||||
members:workspace_members (
|
||||
publicId,
|
||||
user (
|
||||
name
|
||||
)
|
||||
)
|
||||
),
|
||||
labels:label (
|
||||
publicId,
|
||||
name,
|
||||
colourCode
|
||||
),
|
||||
lists:list (
|
||||
publicId,
|
||||
name,
|
||||
boardId,
|
||||
index,
|
||||
cards:card (
|
||||
publicId,
|
||||
title,
|
||||
description,
|
||||
listId,
|
||||
index,
|
||||
labels:label (
|
||||
publicId,
|
||||
name,
|
||||
colourCode
|
||||
),
|
||||
members:workspace_members (
|
||||
publicId,
|
||||
user (
|
||||
name
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`,
|
||||
)
|
||||
.eq("publicId", input.id)
|
||||
.is("deletedAt", null)
|
||||
.is("lists.deletedAt", null)
|
||||
.is("lists.cards.deletedAt", null)
|
||||
.order("index", { foreignTable: "list", ascending: true })
|
||||
.order("index", { foreignTable: "list.card", ascending: true })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
return data;
|
||||
}),
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
workspacePublicId: z.string().min(12)
|
||||
workspacePublicId: z.string().min(12),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
const workspace = await ctx.db.query.workspaces.findFirst({
|
||||
where: eq(workspaces.publicId, input.workspacePublicId),
|
||||
})
|
||||
const workspace = await ctx.db
|
||||
.from("workspace")
|
||||
.select(`id`)
|
||||
.eq("publicId", input.workspacePublicId)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!workspace) return;
|
||||
if (!workspace.data) return;
|
||||
|
||||
return ctx.db.insert(boards).values({
|
||||
publicId: generateUID(),
|
||||
name: input.name,
|
||||
createdBy: userId,
|
||||
workspaceId: workspace.id
|
||||
});
|
||||
}),
|
||||
update: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
boardId: z.string().min(12),
|
||||
name: z.string().min(1),
|
||||
}))
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.update(boards).set({ name: input.name }).where(eq(boards.publicId, input.boardId));
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
boardPublicId: z.string().min(12),
|
||||
}))
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.transaction(async (tx) => {
|
||||
const board = await tx.query.boards.findFirst({
|
||||
where: eq(boards.publicId, input.boardPublicId),
|
||||
with: {
|
||||
lists: true,
|
||||
}
|
||||
})
|
||||
|
||||
if (!board) return;
|
||||
|
||||
const listIds = board.lists.map((list) => list.id)
|
||||
|
||||
await tx.update(boards).set({ deletedAt: new Date(), deletedBy: userId }).where(eq(boards.id, board.id));
|
||||
|
||||
if (listIds.length) {
|
||||
await tx.update(lists).set({ deletedAt: new Date(), deletedBy: userId }).where(eq(lists.boardId, board.id));
|
||||
await tx.update(cards).set({ deletedAt: new Date(), deletedBy: userId }).where(inArray(cards.listId, listIds));
|
||||
}
|
||||
const { data } = await ctx.db
|
||||
.from("board")
|
||||
.insert({
|
||||
publicId: generateUID(),
|
||||
name: input.name,
|
||||
createdBy: userId,
|
||||
workspaceId: workspace.data.id,
|
||||
})
|
||||
.select(`publicId, name`);
|
||||
|
||||
return data;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
boardId: z.string().min(12),
|
||||
name: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { data } = await ctx.db
|
||||
.from("board")
|
||||
.update({ name: input.name })
|
||||
.eq("publicId", input.boardId);
|
||||
|
||||
return data;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
boardPublicId: z.string().min(12),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
const board = await ctx.db
|
||||
.from("board")
|
||||
.select(`id, lists:list (id)`)
|
||||
.eq("publicId", input.boardPublicId)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!board.data) return;
|
||||
|
||||
const listIds = board.data.lists.map((list) => list.id);
|
||||
|
||||
const deletedAt = new Date().toISOString();
|
||||
|
||||
await ctx.db
|
||||
.from("board")
|
||||
.update({ deletedAt, deletedBy: userId })
|
||||
.eq("id", board.data.id)
|
||||
.is("deletedAt", null);
|
||||
|
||||
if (listIds.length) {
|
||||
await ctx.db
|
||||
.from("list")
|
||||
.update({ deletedAt, deletedBy: userId })
|
||||
.eq("boardId", board.data.id)
|
||||
.is("deletedAt", null);
|
||||
|
||||
await ctx.db
|
||||
.from("card")
|
||||
.update({ deletedAt, deletedBy: userId })
|
||||
.in("listId", listIds)
|
||||
.is("deletedAt", null);
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,358 +1,364 @@
|
||||
import { z } from "zod";
|
||||
import { and, desc, eq, isNull, inArray, sql } from "drizzle-orm";
|
||||
|
||||
import { cards, cardsToLabels, cardToWorkspaceMembers, labels, lists, workspaceMembers } from "~/server/db/schema";
|
||||
import { generateUID } from "~/utils/generateUID";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
publicProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
|
||||
export const cardRouter = createTRPCRouter({
|
||||
create: publicProcedure
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
title: z.string().min(1),
|
||||
listPublicId: z.string().min(12),
|
||||
labelsPublicIds: z.array(z.string().min(12)),
|
||||
memberPublicIds: z.array(z.string().min(12))
|
||||
memberPublicIds: z.array(z.string().min(12)),
|
||||
}),
|
||||
)
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.transaction(async (tx) => {
|
||||
const list = await tx.query.lists.findFirst({
|
||||
where: eq(lists.publicId, input.listPublicId),
|
||||
columns: {
|
||||
id: true
|
||||
}
|
||||
});
|
||||
const list = await ctx.db
|
||||
.from("list")
|
||||
.select(`id, cards:card (index)`)
|
||||
.eq("publicId", input.listPublicId)
|
||||
.order("index", { foreignTable: "card", ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!list) return;
|
||||
if (!list.data?.id) return;
|
||||
|
||||
const latestCard = await tx.query.cards.findFirst({
|
||||
where: and(eq(cards.listId, list.id), isNull(cards.deletedAt)),
|
||||
columns: {
|
||||
index: true
|
||||
},
|
||||
orderBy: desc(cards.index)
|
||||
});
|
||||
const latestCard = list.data.cards.length && list.data.cards[0];
|
||||
|
||||
const newCard = await tx.insert(cards).values({
|
||||
const newCard = await ctx.db
|
||||
.from("card")
|
||||
.insert({
|
||||
publicId: generateUID(),
|
||||
title: input.title,
|
||||
createdBy: userId,
|
||||
listId: list.id,
|
||||
index: latestCard ? latestCard.index + 1 : 0
|
||||
}).returning({ id: cards.id });
|
||||
listId: list.data.id,
|
||||
index: latestCard ? latestCard.index + 1 : 0,
|
||||
})
|
||||
.select(`id`)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
const newCardId = newCard[0]?.id;
|
||||
const newCardId = newCard.data?.id;
|
||||
|
||||
if (newCardId && input.labelsPublicIds.length) {
|
||||
const labels = await tx.query.labels.findMany({
|
||||
where: inArray(cards.publicId, input.labelsPublicIds),
|
||||
});
|
||||
if (newCardId && input.labelsPublicIds.length) {
|
||||
const labels = await ctx.db
|
||||
.from("label")
|
||||
.select(`id`)
|
||||
.eq("publicId", input.labelsPublicIds);
|
||||
|
||||
if (!labels.length) return;
|
||||
if (!labels.data?.length) return;
|
||||
|
||||
const labelsInsert = labels.map((label) => ({ cardId: newCardId, labelId: label.id }))
|
||||
const labelsInsert = labels.data.map((label) => ({
|
||||
cardId: newCardId,
|
||||
labelId: label.id,
|
||||
}));
|
||||
|
||||
await tx.insert(cardsToLabels).values(labelsInsert);
|
||||
}
|
||||
await ctx.db.from("_card_labels").insert(labelsInsert);
|
||||
}
|
||||
|
||||
if (newCardId && input.memberPublicIds.length) {
|
||||
const members = await tx.query.workspaceMembers.findMany({
|
||||
where: inArray(workspaceMembers.publicId, input.memberPublicIds),
|
||||
});
|
||||
if (newCardId && input.memberPublicIds.length) {
|
||||
const members = await ctx.db
|
||||
.from("workspace_members")
|
||||
.select(`id`)
|
||||
.eq("publicId", input.memberPublicIds);
|
||||
|
||||
if (!members.length) return;
|
||||
if (!members.data?.length) return;
|
||||
|
||||
const membersInsert = members.map((member) => ({ cardId: newCardId, workspaceMemberId: member.id}))
|
||||
const membersInsert = members.data.map((member) => ({
|
||||
cardId: newCardId,
|
||||
workspaceMemberId: member.id,
|
||||
}));
|
||||
|
||||
await tx.insert(cardToWorkspaceMembers).values(membersInsert);
|
||||
}
|
||||
await ctx.db.from("_card_workspace_members").insert(membersInsert);
|
||||
}
|
||||
|
||||
return newCard;
|
||||
})
|
||||
return newCard;
|
||||
}),
|
||||
addOrRemoveLabel: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
cardPublicId: z.string().min(12),
|
||||
labelPublicId: z.string().min(12),
|
||||
}),
|
||||
)
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.transaction(async (tx) => {
|
||||
const card = await tx.query.cards.findFirst({
|
||||
where: and(eq(cards.publicId, input.cardPublicId), isNull(cards.deletedAt)),
|
||||
});
|
||||
|
||||
const label = await tx.query.labels.findFirst({
|
||||
where: eq(labels.publicId, input.labelPublicId),
|
||||
});
|
||||
|
||||
if (!card || !label) return;
|
||||
|
||||
const labelExists = await tx.query.cardsToLabels.findFirst({
|
||||
where: and(eq(cardsToLabels.cardId, card.id), eq(cardsToLabels.labelId, label.id)),
|
||||
});
|
||||
|
||||
if (labelExists) {
|
||||
return tx.delete(cardsToLabels).where(and(eq(cardsToLabels.cardId, card.id), eq(cardsToLabels.labelId, label.id)),);
|
||||
}
|
||||
|
||||
return tx.insert(cardsToLabels).values({
|
||||
cardId: card.id,
|
||||
labelId: label.id,
|
||||
});
|
||||
})
|
||||
addOrRemoveLabel: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
cardPublicId: z.string().min(12),
|
||||
labelPublicId: z.string().min(12),
|
||||
}),
|
||||
addOrRemoveMember: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
cardPublicId: z.string().min(12),
|
||||
workspaceMemberPublicId: z.string().min(12),
|
||||
}),
|
||||
)
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.transaction(async (tx) => {
|
||||
const card = await tx.query.cards.findFirst({
|
||||
where: and(eq(cards.publicId, input.cardPublicId), isNull(cards.deletedAt)),
|
||||
});
|
||||
const card = await ctx.db
|
||||
.from("card")
|
||||
.select(`id`)
|
||||
.eq("publicId", input.cardPublicId)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
const member = await tx.query.workspaceMembers.findFirst({
|
||||
where: eq(workspaceMembers.publicId, input.workspaceMemberPublicId),
|
||||
});
|
||||
|
||||
if (!card || !member) return;
|
||||
const label = await ctx.db
|
||||
.from("label")
|
||||
.select(`id`)
|
||||
.eq("publicId", input.labelPublicId)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
const memberExists = await tx.query.cardToWorkspaceMembers.findFirst({
|
||||
where: and(eq(cardToWorkspaceMembers.cardId, card.id), eq(cardToWorkspaceMembers.workspaceMemberId, member.id)),
|
||||
});
|
||||
if (!card.data || !label.data) return;
|
||||
|
||||
if (memberExists) {
|
||||
return tx.delete(cardToWorkspaceMembers).where(and(eq(cardToWorkspaceMembers.cardId, card.id), eq(cardToWorkspaceMembers.workspaceMemberId, member.id)),);
|
||||
}
|
||||
const existingLabel = await ctx.db
|
||||
.from("_card_labels")
|
||||
.select()
|
||||
.eq("cardId", card.data.id)
|
||||
.eq("labelId", label.data.id)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
return tx.insert(cardToWorkspaceMembers).values({
|
||||
cardId: card.id,
|
||||
workspaceMemberId: member.id,
|
||||
});
|
||||
})
|
||||
if (existingLabel.data) {
|
||||
await ctx.db
|
||||
.from("_card_labels")
|
||||
.delete()
|
||||
.eq("cardId", card.data.id)
|
||||
.eq("labelId", label.data.id);
|
||||
|
||||
return { newLabel: false };
|
||||
}
|
||||
|
||||
await ctx.db.from("_card_labels").insert({
|
||||
cardId: card.data.id,
|
||||
labelId: label.data.id,
|
||||
});
|
||||
|
||||
return { newLabel: true };
|
||||
}),
|
||||
addOrRemoveMember: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
cardPublicId: z.string().min(12),
|
||||
workspaceMemberPublicId: z.string().min(12),
|
||||
}),
|
||||
byId: publicProcedure
|
||||
.input(z.object({ id: z.string().min(12) }))
|
||||
.query(({ ctx, input }) =>
|
||||
ctx.db.query.cards.findFirst({
|
||||
with: {
|
||||
labels: {
|
||||
columns: {
|
||||
labelId: false,
|
||||
cardId: false,
|
||||
},
|
||||
with: {
|
||||
label: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
colourCode: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
list: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
},
|
||||
with: {
|
||||
board: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
},
|
||||
with: {
|
||||
labels: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
colourCode: true,
|
||||
name: true,
|
||||
}
|
||||
},
|
||||
lists: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
},
|
||||
where: isNull(lists.deletedAt)
|
||||
},
|
||||
workspace: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
},
|
||||
with: {
|
||||
members: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
},
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
members: {
|
||||
columns: {
|
||||
workspaceMemberId: false,
|
||||
cardId: false,
|
||||
},
|
||||
with: {
|
||||
member: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
},
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
where: and(eq(cards.publicId, input.id), isNull(cards.deletedAt)),
|
||||
})
|
||||
),
|
||||
update: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
cardId: z.string().min(12),
|
||||
title: z.string().min(1),
|
||||
description: z.string(),
|
||||
}))
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.update(cards).set({ title: input.title, description: input.description }).where(and(eq(cards.publicId, input.cardId), isNull(cards.deletedAt)));
|
||||
const card = await ctx.db
|
||||
.from("card")
|
||||
.select(`id`)
|
||||
.eq("publicId", input.cardPublicId)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
const member = await ctx.db
|
||||
.from("workspace_members")
|
||||
.select(`id`)
|
||||
.eq("publicId", input.workspaceMemberPublicId)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!card.data || !member.data) return;
|
||||
|
||||
const existingMember = await ctx.db
|
||||
.from("_card_workspace_members")
|
||||
.select()
|
||||
.eq("cardId", card.data.id)
|
||||
.eq("workspaceMemberId", member.data.id)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (existingMember.data) {
|
||||
await ctx.db
|
||||
.from("_card_workspace_members")
|
||||
.delete()
|
||||
.eq("cardId", card.data.id)
|
||||
.eq("workspaceMemberId", member.data.id);
|
||||
|
||||
return { newMember: false };
|
||||
}
|
||||
|
||||
await ctx.db.from("_card_workspace_members").insert({
|
||||
cardId: card.data.id,
|
||||
workspaceMemberId: member.data.id,
|
||||
});
|
||||
|
||||
return { newMember: true };
|
||||
}),
|
||||
byId: protectedProcedure
|
||||
.input(z.object({ id: z.string().min(12) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { data } = await ctx.db
|
||||
.from("card")
|
||||
.select(
|
||||
`
|
||||
publicId,
|
||||
title,
|
||||
description,
|
||||
labels:label (
|
||||
publicId,
|
||||
name,
|
||||
colourCode
|
||||
),
|
||||
list (
|
||||
publicId,
|
||||
name,
|
||||
board (
|
||||
publicId,
|
||||
name,
|
||||
labels:label (
|
||||
publicId,
|
||||
colourCode,
|
||||
name
|
||||
),
|
||||
lists:list (
|
||||
publicId,
|
||||
name
|
||||
),
|
||||
workspace (
|
||||
publicId,
|
||||
members:workspace_members (
|
||||
publicId,
|
||||
user (
|
||||
id,
|
||||
name
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
members:workspace_members (
|
||||
publicId,
|
||||
user (
|
||||
id,
|
||||
name
|
||||
)
|
||||
)
|
||||
`,
|
||||
)
|
||||
.eq("publicId", input.id)
|
||||
.is("deletedAt", null)
|
||||
.is("list.board.lists.deletedAt", null)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
return data;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
cardId: z.string().min(12),
|
||||
title: z.string().min(1),
|
||||
description: z.string(),
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
cardPublicId: z.string().min(12),
|
||||
}))
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.transaction(async (tx) => {
|
||||
const card = await tx.query.cards.findFirst({
|
||||
where: eq(cards.publicId, input.cardPublicId),
|
||||
})
|
||||
const { data } = await ctx.db
|
||||
.from("card")
|
||||
.update({ title: input.title, description: input.description })
|
||||
.eq("publicId", input.cardId)
|
||||
.is("deletedAt", null);
|
||||
|
||||
if (!card) return;
|
||||
|
||||
await tx.update(cards).set({ deletedAt: new Date(), deletedBy: userId}).where(eq(cards.publicId, input.cardPublicId));
|
||||
|
||||
await tx.execute(sql`UPDATE ${cards} SET ${cards.index} = ${cards.index} - 1 WHERE ${cards.listId} = ${card.listId} AND ${cards.index} > ${card.index} AND ${cards.deletedAt} IS NULL;`);
|
||||
})
|
||||
return data;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
cardPublicId: z.string().min(12),
|
||||
}),
|
||||
reorder: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
cardId: z.string().min(12),
|
||||
newListId: z.string().min(12),
|
||||
newIndex: z.number().optional(),
|
||||
}))
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.transaction(async (tx) => {
|
||||
const card = await tx.query.cards.findFirst({
|
||||
with: {
|
||||
list: true,
|
||||
},
|
||||
where: and(eq(cards.publicId, input.cardId), isNull(cards.deletedAt)),
|
||||
});
|
||||
const card = await ctx.db
|
||||
.from("card")
|
||||
.select(`id, index, listId`)
|
||||
.eq("publicId", input.cardPublicId)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!card) return;
|
||||
if (!card.data) return;
|
||||
|
||||
const currentList = card.list;
|
||||
const currentIndex = card.index;
|
||||
|
||||
let newIndex = input.newIndex;
|
||||
const deletedAt = new Date().toISOString();
|
||||
|
||||
const newList = await tx.query.lists.findFirst({
|
||||
with: {
|
||||
cards: {
|
||||
orderBy: [desc(cards.index)],
|
||||
limit: 1,
|
||||
},
|
||||
},
|
||||
where: and(eq(lists.publicId, input.newListId), isNull(cards.deletedAt)),
|
||||
});
|
||||
await ctx.db
|
||||
.from("card")
|
||||
.update({ deletedAt, deletedBy: userId })
|
||||
.eq("publicId", input.cardPublicId);
|
||||
|
||||
if (!newList) return;
|
||||
await ctx.db
|
||||
.from("card")
|
||||
.update({ deletedAt, deletedBy: userId })
|
||||
.eq("publicId", input.cardPublicId);
|
||||
|
||||
if (newIndex === undefined) {
|
||||
const lastCardIndex = newList.cards.length ? newList.cards[0]?.index : undefined;
|
||||
await ctx.db.rpc("shift_card_index", {
|
||||
list_id: card.data.listId,
|
||||
card_index: card.data.index,
|
||||
});
|
||||
}),
|
||||
reorder: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
cardId: z.string().min(12),
|
||||
newListId: z.string().min(12),
|
||||
newIndex: z.number().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
newIndex = lastCardIndex !== undefined ? lastCardIndex + 1 : 0;
|
||||
}
|
||||
if (!userId) return;
|
||||
|
||||
if (!currentList?.id || !newList?.id) return;
|
||||
const card = await ctx.db
|
||||
.from("card")
|
||||
.select(`id, index, list (id)`)
|
||||
.eq("publicId", input.cardId)
|
||||
.is("deletedAt", null)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!card.data) return;
|
||||
|
||||
if (currentList.id === newList.id) {
|
||||
await tx.execute(sql`
|
||||
UPDATE ${cards}
|
||||
SET index =
|
||||
CASE
|
||||
WHEN ${cards.index} = ${currentIndex} THEN ${newIndex}
|
||||
WHEN ${currentIndex} < ${newIndex} AND ${cards.index} > ${currentIndex} AND ${cards.index} <= ${newIndex} THEN ${cards.index} - 1
|
||||
WHEN ${currentIndex} > ${newIndex} AND ${cards.index} >= ${newIndex} AND ${cards.index} < ${currentIndex} THEN ${cards.index} + 1
|
||||
ELSE ${cards.index}
|
||||
END
|
||||
WHERE ${cards.listId} = ${currentList.id} AND ${cards.deletedAt} IS NULL;
|
||||
`);
|
||||
} else {
|
||||
await tx.execute(sql`UPDATE ${cards} SET index = index + 1 WHERE ${cards.listId} = ${newList.id} AND ${cards.index} >= ${newIndex} AND ${cards.deletedAt} IS NULL;`)
|
||||
const currentList = card.data.list;
|
||||
const currentIndex = card.data.index;
|
||||
|
||||
await tx.execute(sql`UPDATE ${cards} SET index = index - 1 WHERE ${cards.listId} = ${currentList.id} AND ${cards.index} >= ${currentIndex} AND ${cards.deletedAt} IS NULL;`)
|
||||
let newIndex = input.newIndex;
|
||||
|
||||
await tx
|
||||
.update(cards)
|
||||
.set({ listId: newList.id, index: newIndex })
|
||||
.where(and(eq(cards.publicId, input.cardId), isNull(cards.deletedAt)));
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
const newList = await ctx.db
|
||||
.from("list")
|
||||
.select(`id, cards:card (index)`)
|
||||
.eq("publicId", input.newListId)
|
||||
.is("deletedAt", null)
|
||||
.order("index", { foreignTable: "card", ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!newList.data) return;
|
||||
|
||||
if (newIndex === undefined) {
|
||||
const lastCardIndex = newList.data.cards.length
|
||||
? newList.data.cards[0]?.index
|
||||
: undefined;
|
||||
|
||||
newIndex = lastCardIndex !== undefined ? lastCardIndex + 1 : 0;
|
||||
}
|
||||
|
||||
if (!currentList?.id || !newList?.data.id) return;
|
||||
|
||||
const { error } = await ctx.db.rpc("reorder_cards", {
|
||||
current_list_id: currentList.id,
|
||||
new_list_id: newList.data.id,
|
||||
current_index: currentIndex,
|
||||
new_index: newIndex,
|
||||
card_id: card.data.id,
|
||||
});
|
||||
|
||||
return { success: !!error };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
publicProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
|
||||
import { boards, cards, imports, lists, workspaces } from "~/server/db/schema";
|
||||
import { generateUID } from "~/utils/generateUID";
|
||||
|
||||
const TRELLO_API_URL = 'https://api.trello.com/1';
|
||||
const TRELLO_API_URL = "https://api.trello.com/1";
|
||||
|
||||
interface TrelloBoard {
|
||||
id: string;
|
||||
@@ -36,29 +31,29 @@ interface MemberData {
|
||||
|
||||
export const importRouter = createTRPCRouter({
|
||||
trello: createTRPCRouter({
|
||||
getBoards: publicProcedure
|
||||
getBoards: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
apiKey: z.string().length(32),
|
||||
token: z.string().length(76),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
|
||||
if (!userId) return;
|
||||
.query(async ({ input }) => {
|
||||
const fetchMemberRes = await fetch(
|
||||
`${TRELLO_API_URL}/tokens/${input.token}/member?key=${input.apiKey}`,
|
||||
);
|
||||
|
||||
const fetchMemberRes = await fetch(`${TRELLO_API_URL}/tokens/${input.token}/member?key=${input.apiKey}`)
|
||||
|
||||
const member = await fetchMemberRes.json() as MemberData;
|
||||
const member = (await fetchMemberRes.json()) as MemberData;
|
||||
|
||||
const boardIds = member.idBoards;
|
||||
|
||||
const fetchBoard = async (boardId: string) => {
|
||||
try {
|
||||
const response = await fetch(`${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}`);
|
||||
const data = await response.json() as TrelloBoard;
|
||||
|
||||
const response = await fetch(
|
||||
`${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}`,
|
||||
);
|
||||
const data = (await response.json()) as TrelloBoard;
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -72,37 +67,57 @@ export const importRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const boardDataArray = await Promise.all(boards);
|
||||
|
||||
return boardDataArray.map((board) => ({ id: board.id, name: board.name }));
|
||||
|
||||
return boardDataArray.map((board) => ({
|
||||
id: board.id,
|
||||
name: board.name,
|
||||
}));
|
||||
}),
|
||||
importBoards: publicProcedure
|
||||
importBoards: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
boardIds: z.array(z.string()),
|
||||
apiKey: z.string().length(32),
|
||||
token: z.string().length(76),
|
||||
workspacePublicId: z.string().min(12)
|
||||
workspacePublicId: z.string().min(12),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
const newImport = await ctx.db.insert(imports).values({
|
||||
publicId: generateUID(),
|
||||
source: 'trello',
|
||||
createdBy: userId,
|
||||
status: 'started'
|
||||
}).returning({ id: imports.id });
|
||||
const newImport = await ctx.db
|
||||
.from("import")
|
||||
.insert({
|
||||
publicId: generateUID(),
|
||||
source: "trello",
|
||||
createdBy: userId,
|
||||
status: "started",
|
||||
})
|
||||
.select(`id`)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
const newImportId = newImport[0]?.id;
|
||||
const newImportId = newImport.data?.id;
|
||||
|
||||
let boardsCreated = 0;
|
||||
|
||||
const workspace = await ctx.db
|
||||
.from("workspace")
|
||||
.select(`id`)
|
||||
.eq("publicId", input.workspacePublicId)
|
||||
.is("deletedAt", null)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!workspace.data) return;
|
||||
|
||||
for (const boardId of input.boardIds) {
|
||||
const response = await fetch(`${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}&lists=open&cards=open`);
|
||||
const data = await response.json() as TrelloBoard;
|
||||
const response = await fetch(
|
||||
`${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}&lists=open&cards=open`,
|
||||
);
|
||||
const data = (await response.json()) as TrelloBoard;
|
||||
|
||||
const formattedData = {
|
||||
name: data.name,
|
||||
@@ -112,72 +127,74 @@ export const importRouter = createTRPCRouter({
|
||||
.filter((card) => card.idList === list.id)
|
||||
.map((_card) => ({
|
||||
name: _card.name,
|
||||
description: _card.desc
|
||||
}))
|
||||
}))
|
||||
}
|
||||
description: _card.desc,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
const workspace = await ctx.db.query.workspaces.findFirst({
|
||||
where: eq(workspaces.publicId, input.workspacePublicId),
|
||||
})
|
||||
|
||||
if (!workspace) return;
|
||||
|
||||
await ctx.db.transaction(async (tx) => {
|
||||
const newBoard = await tx.insert(boards).values({
|
||||
const newBoard = await ctx.db
|
||||
.from("board")
|
||||
.insert({
|
||||
publicId: generateUID(),
|
||||
name: data.name,
|
||||
createdBy: userId,
|
||||
importId: newImportId,
|
||||
workspaceId: workspace.id
|
||||
}).returning({ id: boards.id });
|
||||
workspaceId: workspace.data.id,
|
||||
})
|
||||
.select(`id`)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
const newBoardId = newBoard[0]?.id
|
||||
const newBoardId = newBoard.data?.id;
|
||||
|
||||
if (!newBoardId) return;
|
||||
if (!newBoardId) return;
|
||||
|
||||
let listIndex = 0;
|
||||
let listIndex = 0;
|
||||
|
||||
for (const list of formattedData.lists) {
|
||||
const newList = await tx.insert(lists).values({
|
||||
for (const list of formattedData.lists) {
|
||||
const newList = await ctx.db
|
||||
.from("list")
|
||||
.insert({
|
||||
publicId: generateUID(),
|
||||
name: list.name,
|
||||
createdBy: userId,
|
||||
boardId: newBoardId,
|
||||
index: listIndex,
|
||||
importId: newImportId
|
||||
}).returning({ id: lists.id });
|
||||
importId: newImportId,
|
||||
})
|
||||
.select(`id`)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
const newListId = newList[0]?.id
|
||||
const newListId = newList.data?.id;
|
||||
|
||||
if (list.cards.length && newListId) {
|
||||
const cardsInsert = list.cards.map((card, index) => ({
|
||||
publicId: generateUID(),
|
||||
title: card.name,
|
||||
description: card.description,
|
||||
createdBy: userId,
|
||||
listId: newListId,
|
||||
index,
|
||||
importId: newImportId
|
||||
}))
|
||||
if (list.cards.length && newListId) {
|
||||
const cardsInsert = list.cards.map((card, index) => ({
|
||||
publicId: generateUID(),
|
||||
title: card.name,
|
||||
description: card.description,
|
||||
createdBy: userId,
|
||||
listId: newListId,
|
||||
index,
|
||||
importId: newImportId,
|
||||
}));
|
||||
|
||||
await tx.insert(cards).values(cardsInsert);
|
||||
}
|
||||
|
||||
listIndex ++
|
||||
await ctx.db.from("card").insert(cardsInsert);
|
||||
}
|
||||
})
|
||||
|
||||
boardsCreated ++
|
||||
listIndex++;
|
||||
}
|
||||
boardsCreated++;
|
||||
}
|
||||
|
||||
if (boardsCreated > 0 && newImportId) {
|
||||
await ctx.db.update(imports)
|
||||
.set({ status: 'success' })
|
||||
.where(eq(imports.id, newImportId))
|
||||
await ctx.db
|
||||
.from("import")
|
||||
.update({ status: "success" })
|
||||
.eq("importId", newImportId);
|
||||
}
|
||||
|
||||
return { boardsCreated }
|
||||
return { boardsCreated };
|
||||
}),
|
||||
})
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
|
||||
import { cards, cardsToLabels, labels } from "~/server/db/schema";
|
||||
import { generateUID } from "~/utils/generateUID";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
publicProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
|
||||
export const labelRouter = createTRPCRouter({
|
||||
create: publicProcedure
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string().min(1).max(36),
|
||||
@@ -18,41 +12,41 @@ export const labelRouter = createTRPCRouter({
|
||||
colourCode: z.string().length(7),
|
||||
}),
|
||||
)
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.transaction(async (tx) => {
|
||||
const card = await tx.query.cards.findFirst({
|
||||
where: and(eq(cards.publicId, input.cardPublicId), isNull(cards.deletedAt)),
|
||||
with: {
|
||||
list: {
|
||||
with: {
|
||||
board: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const card = await ctx.db
|
||||
.from("card")
|
||||
.select(`id, list (boardId)`)
|
||||
.eq("publicId", input.cardPublicId)
|
||||
.is("deletedAt", null)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!card) return;
|
||||
if (!card.data?.list) return;
|
||||
|
||||
const newLabel = await tx.insert(labels).values({
|
||||
const newLabel = await ctx.db
|
||||
.from("label")
|
||||
.insert({
|
||||
publicId: generateUID(),
|
||||
name: input.name,
|
||||
colourCode: input.colourCode,
|
||||
createdBy: userId,
|
||||
boardId: card.list.boardId,
|
||||
}).returning({ id: labels.id });
|
||||
boardId: card.data.list.boardId,
|
||||
})
|
||||
.select(`id`)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
const newLabelId = newLabel[0]?.id
|
||||
if (!newLabel.data) return;
|
||||
|
||||
if (!newLabelId) return;
|
||||
await ctx.db.from("_card_labels").insert({
|
||||
cardId: card.data.id,
|
||||
labelId: newLabel.data.id,
|
||||
});
|
||||
|
||||
return tx.insert(cardsToLabels).values({
|
||||
cardId: card.id,
|
||||
labelId: newLabelId,
|
||||
});
|
||||
})
|
||||
return newLabel.data;
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,120 +1,129 @@
|
||||
import { z } from "zod";
|
||||
import { and, desc, eq, sql, isNull } from "drizzle-orm";
|
||||
|
||||
import { boards, cards, lists } from "~/server/db/schema";
|
||||
import { generateUID } from "~/utils/generateUID";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
publicProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
|
||||
export const listRouter = createTRPCRouter({
|
||||
create: publicProcedure
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
boardPublicId: z.string().min(12)
|
||||
boardPublicId: z.string().min(12),
|
||||
}),
|
||||
)
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.transaction(async (tx) => {
|
||||
const board = await tx.query.boards.findFirst({
|
||||
where: eq(boards.publicId, input.boardPublicId),
|
||||
columns: {
|
||||
id: true
|
||||
}
|
||||
});
|
||||
const board = await ctx.db
|
||||
.from("board")
|
||||
.select(`id, lists:list (index)`)
|
||||
.eq("publicId", input.boardPublicId)
|
||||
.order("index", { foreignTable: "list", ascending: false })
|
||||
.is("list.deletedAt", null)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!board) return;
|
||||
if (!board.data) return;
|
||||
|
||||
const latestList = await tx.query.lists.findFirst({
|
||||
where: eq(lists.boardId, board.id),
|
||||
columns: {
|
||||
index: true
|
||||
},
|
||||
orderBy: desc(lists.index)
|
||||
});
|
||||
const latestListIndex = board.data.lists[0]?.index;
|
||||
|
||||
return tx.insert(lists).values({
|
||||
publicId: generateUID(),
|
||||
name: input.name,
|
||||
createdBy: userId,
|
||||
boardId: board.id,
|
||||
index: latestList ? latestList.index + 1 : 0
|
||||
});
|
||||
})
|
||||
const { data } = await ctx.db.from("list").insert({
|
||||
publicId: generateUID(),
|
||||
name: input.name,
|
||||
createdBy: userId,
|
||||
boardId: board.data.id,
|
||||
index: latestListIndex ? latestListIndex + 1 : 0,
|
||||
}).select(`
|
||||
publicId,
|
||||
name
|
||||
`);
|
||||
|
||||
return data;
|
||||
}),
|
||||
reorder: publicProcedure
|
||||
reorder: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
z.object({
|
||||
boardId: z.string().min(12),
|
||||
listId: z.string().min(12),
|
||||
currentIndex: z.number(),
|
||||
newIndex: z.number(),
|
||||
}))
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const list = await ctx.db
|
||||
.from("list")
|
||||
.select(`id, boardId`)
|
||||
.eq("publicId", input.listId)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!userId) return;
|
||||
if (!list?.data) return;
|
||||
|
||||
return ctx.db.transaction(async (tx) => {
|
||||
const [list] = await tx.select({ id: lists.id, boardId: lists.boardId }).from(lists).where(eq(lists.publicId, input.listId))
|
||||
const { data } = await ctx.db.rpc("reorder_lists", {
|
||||
board_id: list.data.boardId,
|
||||
list_id: list.data.id,
|
||||
current_index: input.currentIndex,
|
||||
new_index: input.newIndex,
|
||||
});
|
||||
|
||||
if (!list) return;
|
||||
|
||||
await tx.execute(sql`
|
||||
UPDATE ${lists}
|
||||
SET index =
|
||||
CASE
|
||||
WHEN ${lists.index} = ${input.currentIndex} AND ${lists.id} = ${list.id} THEN ${input.newIndex}
|
||||
WHEN ${input.currentIndex} < ${input.newIndex} AND ${lists.index} > ${input.currentIndex} AND ${lists.index} <= ${input.newIndex} THEN ${lists.index} - 1
|
||||
WHEN ${input.currentIndex} > ${input.newIndex} AND ${lists.index} >= ${input.newIndex} AND ${lists.index} < ${input.currentIndex} THEN ${lists.index} + 1
|
||||
ELSE ${lists.index}
|
||||
END
|
||||
WHERE ${lists.boardId} = ${list.boardId};
|
||||
`);
|
||||
})
|
||||
return data;
|
||||
}),
|
||||
delete: publicProcedure
|
||||
delete: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
z.object({
|
||||
listPublicId: z.string().min(12),
|
||||
}))
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
const list = await ctx.db
|
||||
.from("list")
|
||||
.select(`id, boardId, index`)
|
||||
.eq("publicId", input.listPublicId)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
return ctx.db.transaction(async (tx) => {
|
||||
const list = await tx.query.lists.findFirst({
|
||||
where: eq(lists.publicId, input.listPublicId),
|
||||
})
|
||||
if (!list.data) return;
|
||||
|
||||
if (!list) return;
|
||||
const deletedAt = new Date().toISOString();
|
||||
|
||||
await tx.update(lists).set({ deletedAt: new Date(), deletedBy: userId}).where(eq(lists.id, list.id));
|
||||
await ctx.db
|
||||
.from("list")
|
||||
.update({ deletedAt, deletedBy: userId })
|
||||
.eq("id", list.data.id)
|
||||
.is("deletedAt", null);
|
||||
|
||||
await tx.update(cards).set({ deletedAt: new Date(), deletedBy: userId}).where(eq(cards.listId, list.id));
|
||||
await ctx.db
|
||||
.from("card")
|
||||
.update({ deletedAt, deletedBy: userId })
|
||||
.eq("listId", list.data.id)
|
||||
.is("deletedAt", null);
|
||||
|
||||
await tx.execute(sql`UPDATE ${lists} SET ${lists.index} = ${lists.index} - 1 WHERE ${lists.boardId} = ${list.boardId} AND ${lists.index} > ${list.index} AND ${lists.deletedAt} IS NULL;`);
|
||||
})
|
||||
const { data } = await ctx.db.rpc("shift_list_index", {
|
||||
board_id: list.data.boardId,
|
||||
list_index: list.data.index,
|
||||
});
|
||||
|
||||
return data;
|
||||
}),
|
||||
update: publicProcedure
|
||||
update: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
z.object({
|
||||
listPublicId: z.string().min(12),
|
||||
name: z.string().min(1),
|
||||
}))
|
||||
.mutation(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { data } = await ctx.db
|
||||
.from("list")
|
||||
.update({ name: input.name })
|
||||
.eq("publicId", input.listPublicId)
|
||||
.is("deletedAt", null)
|
||||
.select(`publicId, name`);
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.update(lists).set({ name: input.name }).where(and(eq(lists.publicId, input.listPublicId), isNull(lists.deletedAt)));
|
||||
return data;
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,104 +1,95 @@
|
||||
import { z } from "zod";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
|
||||
import { workspaces, workspaceMembers } from "~/server/db/schema";
|
||||
|
||||
import { generateUID } from "~/utils/generateUID";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
publicProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
|
||||
export const workspaceRouter = createTRPCRouter({
|
||||
all: publicProcedure
|
||||
.query(({ ctx }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.query.workspaceMembers.findMany({
|
||||
where: and(eq(workspaceMembers.userId, userId), isNull(workspaceMembers.deletedAt)),
|
||||
columns: {
|
||||
role: true,
|
||||
},
|
||||
with: {
|
||||
workspace: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}),
|
||||
byId: publicProcedure
|
||||
const { data } = await ctx.db
|
||||
.from("workspace_members")
|
||||
.select(
|
||||
`
|
||||
role,
|
||||
workspace (
|
||||
publicId,
|
||||
name
|
||||
)
|
||||
`,
|
||||
)
|
||||
.eq("userId", userId)
|
||||
.is("deletedAt", null);
|
||||
|
||||
return data;
|
||||
}),
|
||||
byId: protectedProcedure
|
||||
.input(z.object({ publicId: z.string().min(12) }))
|
||||
.query(({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { data } = await ctx.db
|
||||
.from("workspace")
|
||||
.select(
|
||||
`
|
||||
publicId,
|
||||
members: workspace_members (
|
||||
publicId,
|
||||
role,
|
||||
user (
|
||||
id,
|
||||
name,
|
||||
email
|
||||
)
|
||||
)
|
||||
`,
|
||||
)
|
||||
.eq("publicId", input.publicId)
|
||||
.is("deletedAt", null)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
return ctx.db.query.workspaces.findFirst({
|
||||
where: and(eq(workspaces.publicId, input.publicId), isNull(workspaces.deletedAt)),
|
||||
columns: {
|
||||
publicId: true,
|
||||
},
|
||||
with: {
|
||||
members: {
|
||||
columns: {
|
||||
publicId: true,
|
||||
role: true,
|
||||
},
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}),
|
||||
create: publicProcedure
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
const userId = ctx.user?.id;
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
const workspace = await ctx.db.insert(workspaces).values({
|
||||
publicId: generateUID(),
|
||||
name: input.name,
|
||||
slug: input.name.toLowerCase(),
|
||||
createdBy: userId,
|
||||
}).returning({ id: workspaces.id });
|
||||
const workspace = await ctx.db
|
||||
.from("workspace")
|
||||
.insert({
|
||||
publicId: generateUID(),
|
||||
name: input.name,
|
||||
slug: input.name.toLowerCase(),
|
||||
createdBy: userId,
|
||||
})
|
||||
.select(`id, publicId, name`)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
const workspaceId = workspace[0]?.id;
|
||||
const newWorkspaceId = workspace.data?.id;
|
||||
|
||||
if (!workspaceId) return;
|
||||
if (!newWorkspaceId) return;
|
||||
|
||||
await ctx.db.insert(workspaceMembers).values({
|
||||
await ctx.db.from("workspace_members").insert({
|
||||
publicId: generateUID(),
|
||||
userId,
|
||||
workspaceId: workspaceId,
|
||||
workspaceId: newWorkspaceId,
|
||||
createdBy: userId,
|
||||
role: 'admin'
|
||||
})
|
||||
|
||||
return ctx.db.query.workspaces.findFirst({
|
||||
where: eq(workspaces.id, workspaceId),
|
||||
columns: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
},
|
||||
role: "admin",
|
||||
});
|
||||
|
||||
const newWorkspace = { ...workspace.data };
|
||||
|
||||
delete newWorkspace.id;
|
||||
|
||||
return newWorkspace;
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
|
||||
* need to use are documented accordingly near the end.
|
||||
*/
|
||||
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import { type NextRequest } from "next/server";
|
||||
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||
import superjson from "superjson";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { auth } from "~/server/auth";
|
||||
import { db } from "~/server/db";
|
||||
import createClient from "~/utils/supabase/api";
|
||||
import { type Database } from "~/types/database.types";
|
||||
import { type SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
/**
|
||||
* 1. CONTEXT
|
||||
@@ -23,8 +23,13 @@ import { db } from "~/server/db";
|
||||
* These allow you to access things when processing a request, like the database, the session, etc.
|
||||
*/
|
||||
|
||||
type User = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
interface CreateContextOptions {
|
||||
headers: Headers;
|
||||
user: User | null;
|
||||
db: SupabaseClient<Database>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,13 +42,11 @@ interface CreateContextOptions {
|
||||
*
|
||||
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
|
||||
*/
|
||||
export const createInnerTRPCContext = async (opts: CreateContextOptions) => {
|
||||
const session = await auth();
|
||||
|
||||
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||
return {
|
||||
session,
|
||||
headers: opts.headers,
|
||||
db,
|
||||
user: opts.user,
|
||||
db: opts.db,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -53,12 +56,14 @@ export const createInnerTRPCContext = async (opts: CreateContextOptions) => {
|
||||
*
|
||||
* @see https://trpc.io/docs/context
|
||||
*/
|
||||
export const createTRPCContext = async (opts: { req: NextRequest }) => {
|
||||
// Fetch stuff that depends on the request
|
||||
export const createTRPCContext = async (_opts: CreateNextContextOptions) => {
|
||||
const db = createClient(_opts.req, _opts.res);
|
||||
|
||||
return await createInnerTRPCContext({
|
||||
headers: opts.req.headers,
|
||||
});
|
||||
const {
|
||||
data: { user },
|
||||
} = await db.auth.getUser();
|
||||
|
||||
return createInnerTRPCContext({ db, user });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -83,6 +88,13 @@ const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a server-side caller.
|
||||
*
|
||||
* @see https://trpc.io/docs/server/server-side-calls
|
||||
*/
|
||||
// export const createCallerFactory = t.createCallerFactory;
|
||||
|
||||
/**
|
||||
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
||||
*
|
||||
@@ -107,15 +119,13 @@ export const createTRPCRouter = t.router;
|
||||
export const publicProcedure = t.procedure;
|
||||
|
||||
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
||||
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.session.user) {
|
||||
const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
|
||||
if (!ctx.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
// infers the `session` as non-nullable
|
||||
session: { ...ctx.session, user: ctx.session.user },
|
||||
},
|
||||
ctx,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user