From d83e9d4dc5497313951a1d8e25155714fd7fdf7c Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 24 Apr 2025 14:07:41 +0100 Subject: [PATCH] refactor: update repo funcs to drizzle --- apps/web/src/pages/api/auth/confirm.ts | 9 +- .../api/stripe/create_billing_session.ts | 7 +- .../api/stripe/create_checkout_session.ts | 9 +- packages/api/src/routers/auth.ts | 4 +- packages/api/src/routers/card.ts | 2 +- packages/api/src/routers/user.ts | 2 +- packages/api/src/trpc.ts | 24 +- packages/db/src/repository/board.repo.ts | 218 +++++++----- .../db/src/repository/cardActivity.repo.ts | 36 +- .../db/src/repository/cardComment.repo.ts | 87 +++-- packages/db/src/repository/feedback.repo.ts | 19 +- packages/db/src/repository/import.repo.ts | 34 +- packages/db/src/repository/label.repo.ts | 105 +++--- packages/db/src/repository/list.repo.ts | 116 +++--- packages/db/src/repository/member.repo.ts | 111 +++--- packages/db/src/repository/user.repo.ts | 73 ++-- packages/db/src/repository/workspace.repo.ts | 332 +++++++++--------- .../db/src/repository/workspaceSlug.repo.ts | 24 +- packages/db/src/schema/cards.ts | 8 +- packages/db/src/schema/workspaces.ts | 43 ++- 20 files changed, 659 insertions(+), 604 deletions(-) diff --git a/apps/web/src/pages/api/auth/confirm.ts b/apps/web/src/pages/api/auth/confirm.ts index 3aa63bbe..162e8be0 100644 --- a/apps/web/src/pages/api/auth/confirm.ts +++ b/apps/web/src/pages/api/auth/confirm.ts @@ -3,6 +3,7 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { Stripe } from "stripe"; +import { createDrizzleClient } from "@kan/db/client"; import * as memberRepo from "@kan/db/repository/member.repo"; import * as userRepo from "@kan/db/repository/user.repo"; import { createNextClient } from "@kan/supabase/clients"; @@ -46,21 +47,23 @@ export default async function handler(req: NextRequest) { const response = NextResponse.next(); if ((tokenHash && type) ?? code) { - const db = createNextClient(req, response); + const supabaseClient = createNextClient(req, response); if (tokenHash && type) { - authRes = await db.auth.verifyOtp({ + authRes = await supabaseClient.auth.verifyOtp({ type: type as EmailOtpType, token_hash: tokenHash, }); } if (code) { - authRes = await db.auth.exchangeCodeForSession(code); + authRes = await supabaseClient.auth.exchangeCodeForSession(code); } const user = authRes?.data.user; + const db = createDrizzleClient(); + if (user?.id && user.email) { const existingUser = await userRepo.getById(db, user.id); diff --git a/apps/web/src/pages/api/stripe/create_billing_session.ts b/apps/web/src/pages/api/stripe/create_billing_session.ts index ad603b3a..723a3443 100644 --- a/apps/web/src/pages/api/stripe/create_billing_session.ts +++ b/apps/web/src/pages/api/stripe/create_billing_session.ts @@ -2,6 +2,7 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { Stripe } from "stripe"; +import { createDrizzleClient } from "@kan/db/client"; import * as userRepo from "@kan/db/repository/user.repo"; import { createNextClient } from "@kan/supabase/clients"; @@ -25,8 +26,8 @@ export default async function handler(req: NextRequest) { try { const response = NextResponse.next(); - const db = createNextClient(req, response); - const { data } = await db.auth.getUser(); + const supabaseClient = createNextClient(req, response); + const { data } = await supabaseClient.auth.getUser(); if (!data.user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { @@ -35,6 +36,8 @@ export default async function handler(req: NextRequest) { }); } + const db = createDrizzleClient(); + const user = await userRepo.getById(db, data.user.id); if (!user?.stripeCustomerId) { diff --git a/apps/web/src/pages/api/stripe/create_checkout_session.ts b/apps/web/src/pages/api/stripe/create_checkout_session.ts index bd298d7d..8c26ffd2 100644 --- a/apps/web/src/pages/api/stripe/create_checkout_session.ts +++ b/apps/web/src/pages/api/stripe/create_checkout_session.ts @@ -3,6 +3,7 @@ import { NextResponse } from "next/server"; import { Stripe } from "stripe"; import { z } from "zod"; +import { createDrizzleClient } from "@kan/db/client"; import * as userRepo from "@kan/db/repository/user.repo"; import * as workspaceRepo from "@kan/db/repository/workspace.repo"; import { createNextClient } from "@kan/supabase/clients"; @@ -42,9 +43,9 @@ export default async function handler(req: NextRequest) { try { const response = NextResponse.next(); - const db = createNextClient(req, response); + const supabaseClient = createNextClient(req, response); - const { data } = await db.auth.getUser(); + const { data } = await supabaseClient.auth.getUser(); if (!data.user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { @@ -53,6 +54,8 @@ export default async function handler(req: NextRequest) { }); } + const db = createDrizzleClient(); + const user = await userRepo.getById(db, data.user.id); if (!user) { @@ -87,7 +90,7 @@ export default async function handler(req: NextRequest) { const workspace = await workspaceRepo.getAllByUserId(db, user.id); const isMemberOfWorkspace = workspace.some( - ({ workspace }) => workspace?.publicId === body.workspacePublicId, + ({ workspace }) => workspace.publicId === body.workspacePublicId, ); if (!isMemberOfWorkspace) { diff --git a/packages/api/src/routers/auth.ts b/packages/api/src/routers/auth.ts index 73be3c6c..ce25ca33 100644 --- a/packages/api/src/routers/auth.ts +++ b/packages/api/src/routers/auth.ts @@ -17,7 +17,7 @@ export const authRouter = createTRPCRouter({ .input(z.object({ email: z.string() })) .output(z.object({ success: z.boolean() })) .mutation(async ({ ctx, input }) => { - const { data } = await ctx.db.auth.signInWithOtp({ + const { data } = await ctx.supabaseClient.auth.signInWithOtp({ email: input.email, options: { emailRedirectTo: `${process.env.WEBSITE_URL}`, @@ -52,7 +52,7 @@ export const authRouter = createTRPCRouter({ code: "BAD_REQUEST", }); - const { data } = await ctx.db.auth.signInWithOAuth({ + const { data } = await ctx.supabaseClient.auth.signInWithOAuth({ provider: "google", options: { queryParams: { diff --git a/packages/api/src/routers/card.ts b/packages/api/src/routers/card.ts index cc54d560..d8482175 100644 --- a/packages/api/src/routers/card.ts +++ b/packages/api/src/routers/card.ts @@ -333,7 +333,7 @@ export const cardRouter = createTRPCRouter({ const deletedComment = await cardCommentRepo.softDelete(ctx.db, { commentId: existingComment.id, - deletedAt: new Date().toISOString(), + deletedAt: new Date(), deletedBy: userId, }); diff --git a/packages/api/src/routers/user.ts b/packages/api/src/routers/user.ts index 6eefa5ce..51b83b6d 100644 --- a/packages/api/src/routers/user.ts +++ b/packages/api/src/routers/user.ts @@ -37,7 +37,7 @@ export const userRouter = createTRPCRouter({ code: "UNAUTHORIZED", }); - const result = await userRepo.getById(ctx.drizzleDb, userId); + const result = await userRepo.getById(ctx.db, userId); if (!result?.name) { throw new TRPCError({ diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 77c9d940..edabc101 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -17,15 +17,15 @@ export interface User { interface CreateContextOptions { user: User | null; - db: SupabaseClient; - drizzleDb: dbClient; + supabaseClient: SupabaseClient; + db: dbClient; } export const createInnerTRPCContext = (opts: CreateContextOptions) => { return { user: opts.user, db: opts.db, - drizzleDb: opts.drizzleDb, + supabaseClient: opts.supabaseClient, }; }; @@ -33,36 +33,36 @@ export const createTRPCContext = async ({ req, resHeaders, }: FetchCreateContextFnOptions) => { - const db = createTRPCClient(req, resHeaders); + const supabaseClient = createTRPCClient(req, resHeaders); const { data: { user }, - } = await db.auth.getUser(); + } = await supabaseClient.auth.getUser(); - const drizzleDb = createDrizzleClient(); + const db = createDrizzleClient(); - return createInnerTRPCContext({ db, user, drizzleDb }); + return createInnerTRPCContext({ db, user, supabaseClient: supabaseClient }); }; export const createRESTContext = async ({ req }: CreateNextContextOptions) => { - const db = createNextApiClient(req); + const supabaseClient = createNextApiClient(req); const authHeader = req.headers.authorization; const accessToken = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null; - const drizzleDb = createDrizzleClient(); + const db = createDrizzleClient(); if (!accessToken) { - return createInnerTRPCContext({ db, user: null, drizzleDb }); + return createInnerTRPCContext({ db, user: null, supabaseClient }); } const { data: { user }, - } = await db.auth.getUser(accessToken); + } = await supabaseClient.auth.getUser(accessToken); - return createInnerTRPCContext({ db, user, drizzleDb }); + return createInnerTRPCContext({ db, user, supabaseClient }); }; const t = initTRPC diff --git a/packages/db/src/repository/board.repo.ts b/packages/db/src/repository/board.repo.ts index 1f4c0beb..2cef93bc 100644 --- a/packages/db/src/repository/board.repo.ts +++ b/packages/db/src/repository/board.repo.ts @@ -1,19 +1,19 @@ import type { SupabaseClient } from "@supabase/supabase-js"; +import { and, asc, eq, inArray, isNull } from "drizzle-orm"; +import type { dbClient } from "@kan/db/client"; import type { Database } from "@kan/db/types/database.types"; +import { boards, cards, lists, workspaceMembers } from "@kan/db/schema"; import { generateUID } from "@kan/shared/utils"; -export const getAllByWorkspaceId = async ( - db: SupabaseClient, - workspaceId: number, -) => { - const { data } = await db - .from("board") - .select(`publicId, name`) - .is("deletedAt", null) - .eq("workspaceId", workspaceId); - - return data ?? []; +export const getAllByWorkspaceId = (db: dbClient, workspaceId: number) => { + return db.query.boards.findMany({ + columns: { + publicId: true, + name: true, + }, + where: and(eq(boards.workspaceId, workspaceId), isNull(boards.deletedAt)), + }); }; export const getIdByPublicId = async ( @@ -31,93 +31,133 @@ export const getIdByPublicId = async ( }; export const getByPublicId = async ( - db: SupabaseClient, + db: dbClient, boardPublicId: string, filters: { members: string[]; labels: string[]; }, ) => { - let query = db - .from("board") - .select( - ` - publicId, - name, - slug, - visibility, - workspace ( - publicId, - members:workspace_members ( - publicId, - user!workspace_members_userId_user_id_fk ( - name, - email, - image - ) - ) - ), - labels:label ( - publicId, - name, - colourCode - ), - lists:list ( - publicId, - name, - boardId, - index, - cards:card ( - publicId, - title, - description, - listId, - index, - labels:label( - publicId, - name, - colourCode - ), - _filteredLabels:label${filters.labels.length > 0 ? "!inner" : ""} ( - publicId - ), - members:workspace_members ( - publicId, - user!workspace_members_userId_user_id_fk ( - name, - email, - image - ) - ), - _filteredMembers:workspace_members${filters.members.length > 0 ? "!inner" : ""} ( - publicId - ) - ) - ) - `, - ) - .eq("publicId", boardPublicId) - .is("deletedAt", null) - .is("lists.deletedAt", null) - .is("lists.cards.deletedAt", null) - .is("workspace.members.deletedAt", null) - .is("lists.cards.members.deletedAt", null); + const board = await db.query.boards.findFirst({ + columns: { + publicId: true, + name: true, + slug: true, + visibility: true, + }, + with: { + workspace: { + columns: { + publicId: true, + }, + with: { + members: { + columns: { + publicId: true, + }, + with: { + user: { + columns: { + name: true, + email: true, + image: true, + }, + }, + }, + where: isNull(workspaceMembers.deletedAt), + }, + }, + }, + labels: { + columns: { + publicId: true, + name: true, + colourCode: true, + }, + }, + lists: { + columns: { + publicId: true, + name: true, + boardId: true, + index: true, + }, + with: { + cards: { + columns: { + publicId: true, + title: true, + description: true, + listId: true, + index: true, + }, + with: { + labels: { + with: { + label: { + columns: { + publicId: true, + name: true, + colourCode: true, + }, + // where: inArray(label.publicId, filters.labels), + }, + }, + }, + members: { + with: { + member: { + columns: { + publicId: true, + deletedAt: true, + }, + with: { + user: { + columns: { + name: true, + email: true, + image: true, + }, + }, + }, + // https://github.com/drizzle-team/drizzle-orm/issues/2903 + // where: isNull(workspaceMembers.deletedAt), + }, + }, + where: + filters.members.length > 0 + ? inArray(workspaceMembers, filters.members) + : undefined, + }, + }, + where: isNull(cards.deletedAt), + orderBy: [asc(cards.index)], + }, + }, + where: isNull(lists.deletedAt), + orderBy: [asc(lists.index)], + }, + }, + where: and(eq(boards.publicId, boardPublicId), isNull(boards.deletedAt)), + }); - if (filters.labels.length > 0) { - query = query.in("lists.cards._filteredLabels.publicId", filters.labels); - } + if (!board) return null; - if (filters.members.length > 0) { - query = query.in("lists.cards._filteredMembers.publicId", filters.members); - } + const formattedResult = { + ...board, + lists: board.lists.map((list) => ({ + ...list, + cards: list.cards.map((card) => ({ + ...card, + labels: card.labels.map((label) => label.label), + members: card.members + .map((member) => member.member) + .filter((member) => member.deletedAt === null), + })), + })), + }; - const { data } = await query - .order("index", { foreignTable: "list", ascending: true }) - .order("index", { foreignTable: "list.card", ascending: true }) - .limit(1) - .single(); - - return data; + return formattedResult; }; export const getBySlug = async ( diff --git a/packages/db/src/repository/cardActivity.repo.ts b/packages/db/src/repository/cardActivity.repo.ts index 28e88d69..d65bf70b 100644 --- a/packages/db/src/repository/cardActivity.repo.ts +++ b/packages/db/src/repository/cardActivity.repo.ts @@ -1,12 +1,12 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; - -import type { Database } from "@kan/db/types/database.types"; +import type { dbClient } from "@kan/db/client"; +import type { ActivityType } from "@kan/db/schema"; +import { cardActivities } from "@kan/db/schema"; import { generateUID } from "@kan/shared/utils"; export const create = async ( - db: SupabaseClient, + db: dbClient, activityInput: { - type: Database["public"]["Enums"]["card_activity_type"]; + type: ActivityType; cardId: number; fromIndex?: number; toIndex?: number; @@ -24,9 +24,9 @@ export const create = async ( toComment?: string; }, ) => { - const { data } = await db - .from("card_activity") - .insert({ + const [result] = await db + .insert(cardActivities) + .values({ publicId: generateUID(), type: activityInput.type, cardId: activityInput.cardId, @@ -45,17 +45,15 @@ export const create = async ( fromComment: activityInput.fromComment, toComment: activityInput.toComment, }) - .select(`id`) - .limit(1) - .single(); + .returning({ id: cardActivities.id }); - return data; + return result; }; export const bulkCreate = async ( - db: SupabaseClient, + db: dbClient, activityInputs: { - type: Database["public"]["Enums"]["card_activity_type"]; + type: ActivityType; cardId: number; fromIndex?: number; toIndex?: number; @@ -75,10 +73,10 @@ export const bulkCreate = async ( publicId: generateUID(), })); - const { data } = await db - .from("card_activity") - .insert(activitiesWithPublicIds) - .select("id"); + const results = await db + .insert(cardActivities) + .values(activitiesWithPublicIds) + .returning({ id: cardActivities.id }); - return data; + return results; }; diff --git a/packages/db/src/repository/cardComment.repo.ts b/packages/db/src/repository/cardComment.repo.ts index e96ddc13..eb9c87a4 100644 --- a/packages/db/src/repository/cardComment.repo.ts +++ b/packages/db/src/repository/cardComment.repo.ts @@ -1,83 +1,82 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import { eq } from "drizzle-orm"; -import type { Database } from "@kan/db/types/database.types"; +import type { dbClient } from "@kan/db/client"; +import { comments } from "@kan/db/schema"; import { generateUID } from "@kan/shared/utils"; export const create = async ( - db: SupabaseClient, + db: dbClient, commentInput: { cardId: number; comment: string; createdBy: string; }, ) => { - const { data } = await db - .from("card_comments") - .insert({ + const [result] = await db + .insert(comments) + .values({ publicId: generateUID(), comment: commentInput.comment, createdBy: commentInput.createdBy, cardId: commentInput.cardId, }) - .select(`id, publicId, comment`) - .limit(1) - .single(); + .returning({ + id: comments.id, + publicId: comments.publicId, + comment: comments.comment, + }); - return data; + return result; }; -export const getByPublicId = async ( - db: SupabaseClient, - publicId: string, -) => { - const { data } = await db - .from("card_comments") - .select(`id, publicId, comment, createdBy`) - .eq("publicId", publicId) - .limit(1) - .single(); - - return data; +export const getByPublicId = (db: dbClient, publicId: string) => { + return db.query.comments.findFirst({ + columns: { + id: true, + publicId: true, + comment: true, + createdBy: true, + }, + where: eq(comments.publicId, publicId), + }); }; export const update = async ( - db: SupabaseClient, + db: dbClient, commentInput: { id: number; comment: string; }, ) => { - const { data } = await db - .from("card_comments") - .update({ + const [result] = await db + .update(comments) + .set({ comment: commentInput.comment, - updatedAt: new Date().toISOString(), + updatedAt: new Date(), }) - .eq("id", commentInput.id) - .select(`id, publicId, comment`) - .limit(1) - .order("id", { ascending: false }) - .single(); + .where(eq(comments.id, commentInput.id)) + .returning({ + id: comments.id, + publicId: comments.publicId, + comment: comments.comment, + }); - return data; + return result; }; export const softDelete = async ( - db: SupabaseClient, + db: dbClient, args: { commentId: number; - deletedAt: string; + deletedAt: Date; deletedBy: string; }, ) => { - const { data } = await db - .from("card_comments") - .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) - .eq("id", args.commentId) - .select(`id`) - .order("id", { ascending: true }) - .limit(1) - .single(); + const [result] = await db + .update(comments) + .set({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) + .where(eq(comments.id, args.commentId)) + .returning({ id: comments.id }); - return data; + return result; }; diff --git a/packages/db/src/repository/feedback.repo.ts b/packages/db/src/repository/feedback.repo.ts index 18a37564..bf7fbd85 100644 --- a/packages/db/src/repository/feedback.repo.ts +++ b/packages/db/src/repository/feedback.repo.ts @@ -1,25 +1,22 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; - -import type { Database } from "@kan/db/types/database.types"; +import type { dbClient } from "@kan/db/client"; +import { feedback } from "@kan/db/schema"; export const create = async ( - db: SupabaseClient, + db: dbClient, feedbackInput: { feedback: string; createdBy: string; url: string; }, ) => { - const { data } = await db - .from("feedback") - .insert({ + const [result] = await db + .insert(feedback) + .values({ feedback: feedbackInput.feedback, createdBy: feedbackInput.createdBy, url: feedbackInput.url, }) - .select(`id`) - .limit(1) - .single(); + .returning({ id: feedback.id }); - return data; + return result; }; diff --git a/packages/db/src/repository/import.repo.ts b/packages/db/src/repository/import.repo.ts index 12260892..e2fd79b1 100644 --- a/packages/db/src/repository/import.repo.ts +++ b/packages/db/src/repository/import.repo.ts @@ -1,38 +1,36 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import { eq } from "drizzle-orm"; -import type { Database } from "@kan/db/types/database.types"; +import type { dbClient } from "@kan/db/client"; +import { imports } from "@kan/db/schema"; import { generateUID } from "@kan/shared/utils"; export const create = async ( - db: SupabaseClient, + db: dbClient, importInput: { source: string; createdBy: string }, ) => { - const { data } = await db - .from("import") - .insert({ + const [result] = await db + .insert(imports) + .values({ publicId: generateUID(), source: "trello", createdBy: importInput.createdBy, status: "started", }) - .select(`id`) - .limit(1) - .single(); + .returning({ id: imports.id }); - return data; + return result; }; export const update = async ( - db: SupabaseClient, + db: dbClient, importInput: { status: "started" | "success" | "failed" }, args: { importId: number }, ) => { - const { data } = await db - .from("import") - .update({ status: importInput.status }) - .eq("importId", args.importId) - .limit(1) - .single(); + const [result] = await db + .update(imports) + .set({ status: importInput.status }) + .where(eq(imports.id, args.importId)) + .returning({ id: imports.id, status: imports.status }); - return data; + return result; }; diff --git a/packages/db/src/repository/label.repo.ts b/packages/db/src/repository/label.repo.ts index e04032e7..0d68c275 100644 --- a/packages/db/src/repository/label.repo.ts +++ b/packages/db/src/repository/label.repo.ts @@ -1,10 +1,11 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import { eq, inArray } from "drizzle-orm"; -import type { Database } from "@kan/db/types/database.types"; +import type { dbClient } from "@kan/db/client"; +import { cardsToLabels, labels } from "@kan/db/schema"; import { generateUID } from "@kan/shared/utils"; export const create = async ( - db: SupabaseClient, + db: dbClient, labelInput: { name: string; colourCode: string; @@ -13,31 +14,30 @@ export const create = async ( cardId?: number; }, ) => { - const { data } = await db - .from("label") - .insert({ + const [result] = await db + .insert(labels) + .values({ publicId: generateUID(), name: labelInput.name, colourCode: labelInput.colourCode, createdBy: labelInput.createdBy, boardId: labelInput.boardId, }) - .select(`id`) - .limit(1) - .single(); + .returning({ id: labels.id }); - if (labelInput.cardId && data) - await db.from("_card_labels").insert({ + if (labelInput.cardId && result) { + await db.insert(cardsToLabels).values({ cardId: labelInput.cardId, - labelId: data.id, + labelId: result.id, }); + } - return data; + return result; }; export const bulkCreate = async ( - db: SupabaseClient, - labels: { + db: dbClient, + labelsInput: { publicId: string; name: string; colourCode: string; @@ -45,61 +45,64 @@ export const bulkCreate = async ( createdBy: string; }[], ) => { - const { data } = await db.from("label").insert(labels).select(`id`); + const results = await db + .insert(labels) + .values(labelsInput) + .returning({ id: labels.id }); - return data; + return results; }; -export const getAllByPublicIds = async ( - db: SupabaseClient, - labelPublicIds: string[], -) => { - const { data } = await db - .from("label") - .select(`id`) - .in("publicId", labelPublicIds); - - return data; +export const getAllByPublicIds = (db: dbClient, labelPublicIds: string[]) => { + return db.query.labels.findMany({ + columns: { + id: true, + }, + where: inArray(labels.publicId, labelPublicIds), + }); }; -export const getByPublicId = async ( - db: SupabaseClient, - labelPublicId: string, -) => { - const { data } = await db - .from("label") - .select(`id, publicId, name, colourCode`) - .eq("publicId", labelPublicId) - .limit(1) - .single(); - - return data; +export const getByPublicId = async (db: dbClient, labelPublicId: string) => { + return db.query.labels.findFirst({ + columns: { + id: true, + publicId: true, + name: true, + colourCode: true, + }, + where: eq(labels.publicId, labelPublicId), + }); }; export const update = async ( - db: SupabaseClient, + db: dbClient, labelInput: { labelPublicId: string; name: string; colourCode: string; }, ) => { - const { data } = await db - .from("label") - .update({ + const [result] = await db + .update(labels) + .set({ name: labelInput.name, colourCode: labelInput.colourCode, }) - .eq("publicId", labelInput.labelPublicId); + .where(eq(labels.publicId, labelInput.labelPublicId)) + .returning({ + id: labels.id, + name: labels.name, + colourCode: labels.colourCode, + }); - return data; + return result; }; -export const hardDelete = async ( - db: SupabaseClient, - labelId: number, -) => { - const { data } = await db.from("label").delete().eq("id", labelId); +export const hardDelete = async (db: dbClient, labelId: number) => { + const [result] = await db + .delete(labels) + .where(eq(labels.id, labelId)) + .returning({ id: labels.id }); - return data; + return result; }; diff --git a/packages/db/src/repository/list.repo.ts b/packages/db/src/repository/list.repo.ts index 6c7ea799..d161a162 100644 --- a/packages/db/src/repository/list.repo.ts +++ b/packages/db/src/repository/list.repo.ts @@ -1,10 +1,13 @@ import type { SupabaseClient } from "@supabase/supabase-js"; +import { and, desc, eq, isNull } from "drizzle-orm"; +import type { dbClient } from "@kan/db/client"; import type { Database } from "@kan/db/types/database.types"; +import { lists } from "@kan/db/schema"; import { generateUID } from "@kan/shared/utils"; export const create = async ( - db: SupabaseClient, + db: dbClient, listInput: { name: string; createdBy: string; @@ -13,9 +16,9 @@ export const create = async ( importId?: number; }, ) => { - const { data } = await db - .from("list") - .insert({ + const [result] = await db + .insert(lists) + .values({ publicId: generateUID(), name: listInput.name, createdBy: listInput.createdBy, @@ -23,48 +26,45 @@ export const create = async ( index: listInput.index, importId: listInput.importId, }) - .select( - ` - id, - publicId, - name - `, - ) - .limit(1) - .single(); + .returning({ + id: lists.id, + publicId: lists.publicId, + name: lists.name, + }); - return data; + return result; }; -export const getByPublicId = async ( - db: SupabaseClient, - listPublicId: string, -) => { - const { data } = await db - .from("list") - .select(`id, boardId, index`) - .eq("publicId", listPublicId) - .limit(1) - .single(); - - return data; +export const getByPublicId = async (db: dbClient, listPublicId: string) => { + return db.query.lists.findFirst({ + columns: { + id: true, + boardId: true, + index: true, + }, + where: eq(lists.publicId, listPublicId), + }); }; export const getWithCardsByPublicId = async ( - db: SupabaseClient, + db: dbClient, listPublicId: string, ) => { - const { data } = await db - .from("list") - .select(`id, cards:card (index)`) - .eq("publicId", listPublicId) - .is("deletedAt", null) - .is("card.deletedAt", null) - .order("index", { foreignTable: "card", ascending: false }) - .limit(1) - .single(); - - return data; + return db.query.lists.findFirst({ + columns: { + id: true, + }, + with: { + cards: { + columns: { + index: true, + }, + where: isNull(lists.deletedAt), + orderBy: [desc(lists.index)], + }, + }, + where: and(eq(lists.publicId, listPublicId), isNull(lists.deletedAt)), + }); }; export const update = async ( @@ -121,41 +121,37 @@ export const shiftIndex = async ( }; export const softDeleteAllByBoardId = async ( - db: SupabaseClient, + db: dbClient, args: { boardId: number; - deletedAt: string; deletedBy: string; }, ) => { - const { data } = await db - .from("list") - .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) - .eq("boardId", args.boardId) - .is("deletedAt", null) - .select(`id`) - .order("id", { ascending: true }); + const [result] = await db + .update(lists) + .set({ deletedAt: new Date(), deletedBy: args.deletedBy }) + .where(and(eq(lists.boardId, args.boardId), isNull(lists.deletedAt))) + .returning({ + id: lists.id, + }); - return data; + return result; }; export const softDeleteById = async ( - db: SupabaseClient, + db: dbClient, args: { listId: number; - deletedAt: string; deletedBy: string; }, ) => { - const { data } = await db - .from("list") - .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) - .eq("id", args.listId) - .is("deletedAt", null) - .select(`id`) - .order("id", { ascending: true }) - .limit(1) - .single(); + const [updatedList] = await db + .update(lists) + .set({ deletedAt: new Date(), deletedBy: args.deletedBy }) + .where(and(eq(lists.id, args.listId), isNull(lists.deletedAt))) + .returning({ + id: lists.id, + }); - return data; + return updatedList; }; diff --git a/packages/db/src/repository/member.repo.ts b/packages/db/src/repository/member.repo.ts index fcf47c1e..b732881c 100644 --- a/packages/db/src/repository/member.repo.ts +++ b/packages/db/src/repository/member.repo.ts @@ -1,21 +1,23 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import { and, eq, isNull } from "drizzle-orm"; -import type { Database } from "@kan/db/types/database.types"; +import type { dbClient } from "@kan/db/client"; +import type { MemberRole, MemberStatus } from "@kan/db/schema"; +import { workspaceMembers } from "@kan/db/schema"; import { generateUID } from "@kan/shared/utils"; export const create = async ( - db: SupabaseClient, + db: dbClient, memberInput: { userId: string; workspaceId: number; createdBy: string; - role: "admin" | "member" | "guest"; - status: "invited" | "active" | "removed"; + role: MemberRole; + status: MemberStatus; }, ) => { - const { data } = await db - .from("workspace_members") - .insert({ + const [result] = await db + .insert(workspaceMembers) + .values({ publicId: generateUID(), userId: memberInput.userId, workspaceId: memberInput.workspaceId, @@ -23,52 +25,53 @@ export const create = async ( role: memberInput.role, status: memberInput.status, }) - .select(`id, publicId`) - .limit(1) - .single(); - - return data; -}; - -export const getByPublicId = async ( - db: SupabaseClient, - publicId: string, -) => { - const { data } = await db - .from("workspace_members") - .select() - .eq("publicId", publicId) - .limit(1) - .single(); - - return data; -}; - -export const acceptInvite = async ( - db: SupabaseClient, - id: number, -) => { - const { data } = await db - .from("workspace_members") - .update({ status: "active" }) - .eq("id", id); - - return data; -}; - -export const softDelete = async ( - db: SupabaseClient, - args: { - memberId: number; - deletedAt: string; - deletedBy: string; - }, -) => { - const result = await db - .from("workspace_members") - .update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) - .eq("id", args.memberId) - .is("deletedAt", null); + .returning({ + id: workspaceMembers.id, + publicId: workspaceMembers.publicId, + }); + + return result; +}; + +export const getByPublicId = async (db: dbClient, publicId: string) => { + return db.query.workspaceMembers.findFirst({ + where: eq(workspaceMembers.publicId, publicId), + }); +}; + +export const acceptInvite = async (db: dbClient, id: number) => { + const [result] = await db + .update(workspaceMembers) + .set({ status: "active" }) + .where(eq(workspaceMembers.id, id)) + .returning({ + id: workspaceMembers.id, + publicId: workspaceMembers.publicId, + }); + + return result; +}; + +export const softDelete = async ( + db: dbClient, + args: { + memberId: number; + deletedBy: string; + }, +) => { + const [result] = await db + .update(workspaceMembers) + .set({ deletedAt: new Date(), deletedBy: args.deletedBy }) + .where( + and( + eq(workspaceMembers.id, args.memberId), + isNull(workspaceMembers.deletedAt), + ), + ) + .returning({ + id: workspaceMembers.id, + publicId: workspaceMembers.publicId, + }); return result; }; diff --git a/packages/db/src/repository/user.repo.ts b/packages/db/src/repository/user.repo.ts index d4c42e6e..e6f82f10 100644 --- a/packages/db/src/repository/user.repo.ts +++ b/packages/db/src/repository/user.repo.ts @@ -1,12 +1,10 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; import { eq } from "drizzle-orm"; import type { dbClient } from "@kan/db/client"; -import type { Database } from "@kan/db/types/database.types"; -import * as schema from "@kan/db/schema"; +import { users } from "@kan/db/schema"; export const getById = async (db: dbClient, userId: string) => { - const data = await db.query.users.findFirst({ + return await db.query.users.findFirst({ columns: { id: true, name: true, @@ -14,55 +12,48 @@ export const getById = async (db: dbClient, userId: string) => { image: true, stripeCustomerId: true, }, - where: eq(schema.users.id, userId), + where: eq(users.id, userId), }); - - return data; }; -export const getByEmail = async ( - db: SupabaseClient, - email: string, -) => { - const { data } = await db - .from("user") - .select(`id, name, email`) - .eq("email", email) - .limit(1) - .single(); - - return data; +export const getByEmail = (db: dbClient, email: string) => { + return db.query.users.findFirst({ + columns: { + id: true, + name: true, + email: true, + }, + where: eq(users.email, email), + }); }; -export const create = async ( - db: SupabaseClient, +export const create = ( + db: dbClient, user: { id: string; email: string; stripeCustomerId: string }, ) => { - const { data } = await db - .from("user") - .insert({ - id: user.id, - email: user.email, - stripeCustomerId: user.stripeCustomerId, - }) - .select() - .limit(1) - .single(); - - return data; + return db.insert(users).values({ + id: user.id, + email: user.email, + stripeCustomerId: user.stripeCustomerId, + }); }; export const update = async ( - db: SupabaseClient, + db: dbClient, userId: string, updates: { image?: string; name?: string }, ) => { - const { data } = await db - .from("user") - .update({ image: updates.image, name: updates.name }) - .eq("id", userId) - .select(`image, name`) - .single(); + const [result] = await db + .update(users) + .set({ + name: updates.name, + image: updates.image, + }) + .where(eq(users.id, userId)) + .returning({ + name: users.name, + image: users.image, + }); - return data; + return result; }; diff --git a/packages/db/src/repository/workspace.repo.ts b/packages/db/src/repository/workspace.repo.ts index a1f862e8..ce2fc830 100644 --- a/packages/db/src/repository/workspace.repo.ts +++ b/packages/db/src/repository/workspace.repo.ts @@ -1,10 +1,11 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import { and, eq, inArray, isNull } from "drizzle-orm"; -import type { Database } from "@kan/db/types/database.types"; +import type { dbClient } from "@kan/db/client"; +import { boards, workspaceMembers, workspaces } from "@kan/db/schema"; import { generateUID } from "@kan/shared/utils"; export const create = async ( - db: SupabaseClient, + db: dbClient, workspaceInput: { publicId?: string; name: string; @@ -12,37 +13,42 @@ export const create = async ( createdBy: string; }, ) => { - const { data } = await db - .from("workspace") - .insert({ + const [workspace] = await db + .insert(workspaces) + .values({ publicId: workspaceInput.publicId ?? generateUID(), name: workspaceInput.name, slug: workspaceInput.slug, createdBy: workspaceInput.createdBy, }) - .select(`id, publicId, name, slug, description, plan`) - .limit(1) - .single(); + .returning({ + id: workspaces.id, + publicId: workspaces.publicId, + name: workspaces.name, + slug: workspaces.slug, + description: workspaces.description, + plan: workspaces.plan, + }); - if (data) - await db.from("workspace_members").insert({ + if (workspace) { + await db.insert(workspaceMembers).values({ publicId: generateUID(), userId: workspaceInput.createdBy, - workspaceId: data.id, + workspaceId: workspace.id, createdBy: workspaceInput.createdBy, role: "admin", status: "active", }); + } - const newWorkspace = { ...data }; - + const newWorkspace = { ...workspace }; delete newWorkspace.id; return newWorkspace; }; export const update = async ( - db: SupabaseClient, + db: dbClient, workspacePublicId: string, workspaceInput: { name?: string; @@ -51,168 +57,162 @@ export const update = async ( description?: string; }, ) => { - const { data } = await db - .from("workspace") - .update({ + const [result] = await db + .update(workspaces) + .set({ name: workspaceInput.name, slug: workspaceInput.slug, plan: workspaceInput.plan, description: workspaceInput.description, }) - .eq("publicId", workspacePublicId) - .is("deletedAt", null); - - return data; -}; - -export const getByPublicId = async ( - db: SupabaseClient, - workspacePublicId: string, -) => { - const { data } = await db - .from("workspace") - .select(`id, publicId, name, plan`) - .is("deletedAt", null) - .eq("publicId", workspacePublicId) - .limit(1) - .single(); - - return data; -}; - -export const getByPublicIdWithMembers = async ( - db: SupabaseClient, - workspacePublicId: string, -) => { - const { data } = await db - .from("workspace") - .select( - ` - id, - publicId, - members: workspace_members ( - publicId, - role, - status, - user!workspace_members_userId_user_id_fk ( - id, - name, - email, - image - ) - ) - `, - ) - .eq("publicId", workspacePublicId) - .is("deletedAt", null) - .is("members.deletedAt", null) - .limit(1) - .single(); - - return data; -}; - -export const getBySlugWithBoards = async ( - db: SupabaseClient, - workspaceSlug: string, -) => { - const { data } = await db - .from("workspace") - .select( - ` - publicId, - name, - description, - slug, - boards: board ( - publicId, - slug, - name - ) - `, - ) - .eq("slug", workspaceSlug) - .is("deletedAt", null) - .is("boards.deletedAt", null) - .limit(1) - .single(); - - return data; -}; - -export const getAllByUserId = async ( - db: SupabaseClient, - userId: string, -) => { - const { data } = await db - .from("workspace_members") - .select( - ` - role, - workspace ( - publicId, - name, - description, - slug, - plan - ) - `, - ) - .eq("userId", userId) - .is("deletedAt", null); - - return data ?? []; -}; - -export const getMemberByPublicId = async ( - db: SupabaseClient, - memberPublicId: string, -) => { - const { data } = await db - .from("workspace_members") - .select(`id`) - .eq("publicId", memberPublicId) - .limit(1) - .single(); - - return data; -}; - -export const getAllMembersByPublicIds = async ( - db: SupabaseClient, - memberPublicIds: string[], -) => { - const { data } = await db - .from("workspace_members") - .select(`id`) - .eq("publicId", memberPublicIds); - - return data; -}; - -export const hardDelete = async ( - db: SupabaseClient, - workspacePublicId: string, -) => { - const result = db - .from("workspace") - .delete() - .eq("publicId", workspacePublicId); + .where(eq(workspaces.publicId, workspacePublicId)) + .returning({ + id: workspaces.id, + publicId: workspaces.publicId, + name: workspaces.name, + slug: workspaces.slug, + description: workspaces.description, + plan: workspaces.plan, + }); return result; }; +export const getByPublicId = (db: dbClient, workspacePublicId: string) => { + return db.query.workspaces.findFirst({ + columns: { + id: true, + publicId: true, + name: true, + plan: true, + }, + where: eq(workspaces.publicId, workspacePublicId), + }); +}; + +export const getByPublicIdWithMembers = ( + db: dbClient, + workspacePublicId: string, +) => { + return db.query.workspaces.findFirst({ + columns: { + id: true, + publicId: true, + }, + with: { + members: { + columns: { + publicId: true, + role: true, + status: true, + }, + where: isNull(workspaceMembers.deletedAt), + with: { + user: { + columns: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + where: and( + eq(workspaces.publicId, workspacePublicId), + isNull(workspaces.deletedAt), + ), + }); +}; + +export const getBySlugWithBoards = (db: dbClient, workspaceSlug: string) => { + return db.query.workspaces.findFirst({ + columns: { + publicId: true, + name: true, + description: true, + slug: true, + }, + with: { + boards: { + columns: { + publicId: true, + slug: true, + name: true, + }, + where: isNull(boards.deletedAt), + }, + }, + where: and( + eq(workspaces.slug, workspaceSlug), + isNull(workspaces.deletedAt), + ), + }); +}; + +export const getAllByUserId = (db: dbClient, userId: string) => { + return db.query.workspaceMembers.findMany({ + columns: { + role: true, + }, + with: { + workspace: { + columns: { + publicId: true, + name: true, + description: true, + slug: true, + plan: true, + }, + // https://github.com/drizzle-team/drizzle-orm/issues/2903 + // where: isNull(workspaces.deletedAt), + }, + }, + where: and( + eq(workspaceMembers.userId, userId), + isNull(workspaceMembers.deletedAt), + ), + }); +}; + +export const getMemberByPublicId = (db: dbClient, memberPublicId: string) => { + return db.query.workspaceMembers.findFirst({ + columns: { + id: true, + }, + where: eq(workspaceMembers.publicId, memberPublicId), + }); +}; + +export const getAllMembersByPublicIds = ( + db: dbClient, + memberPublicIds: string[], +) => { + return db.query.workspaceMembers.findMany({ + columns: { + id: true, + }, + where: inArray(workspaceMembers.publicId, memberPublicIds), + }); +}; + +export const hardDelete = (db: dbClient, workspacePublicId: string) => { + return db + .delete(workspaces) + .where(eq(workspaces.publicId, workspacePublicId)); +}; + export const isWorkspaceSlugAvailable = async ( - db: SupabaseClient, + db: dbClient, workspaceSlug: string, ) => { - const { data } = await db - .from("workspace") - .select("id") - .eq("slug", workspaceSlug) - .is("deletedAt", null) - .limit(1) - .single(); + const result = await db.query.workspaces.findFirst({ + columns: { + id: true, + }, + where: eq(workspaces.slug, workspaceSlug), + }); - return data === null; + return result === undefined; }; diff --git a/packages/db/src/repository/workspaceSlug.repo.ts b/packages/db/src/repository/workspaceSlug.repo.ts index 8c7b34eb..1911d965 100644 --- a/packages/db/src/repository/workspaceSlug.repo.ts +++ b/packages/db/src/repository/workspaceSlug.repo.ts @@ -1,16 +1,14 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import { eq } from "drizzle-orm"; -import type { Database } from "@kan/db/types/database.types"; +import type { dbClient } from "@kan/db/client"; +import { slugs } from "@kan/db/schema"; -export const getWorkspaceSlug = async ( - db: SupabaseClient, - slug: string, -) => { - const { data } = await db - .from("workspace_slugs") - .select(`slug, type`) - .eq("slug", slug) - .single(); - - return data; +export const getWorkspaceSlug = (db: dbClient, slug: string) => { + return db.query.slugs.findFirst({ + columns: { + slug: true, + type: true, + }, + where: eq(slugs.slug, slug), + }); }; diff --git a/packages/db/src/schema/cards.ts b/packages/db/src/schema/cards.ts index ed6cbe40..001e5b9e 100644 --- a/packages/db/src/schema/cards.ts +++ b/packages/db/src/schema/cards.ts @@ -18,7 +18,7 @@ import { lists } from "./lists"; import { users } from "./users"; import { workspaceMembers } from "./workspaces"; -export const activityTypeEnum = pgEnum("card_activity_type", [ +export const activityTypes = [ "card.created", "card.updated.title", "card.updated.description", @@ -32,7 +32,11 @@ export const activityTypeEnum = pgEnum("card_activity_type", [ "card.updated.comment.updated", "card.updated.comment.deleted", "card.archived", -]); +] as const; + +export type ActivityType = (typeof activityTypes)[number]; + +export const activityTypeEnum = pgEnum("card_activity_type", activityTypes); export const cards = pgTable("card", { id: bigserial("id", { mode: "number" }).primaryKey(), diff --git a/packages/db/src/schema/workspaces.ts b/packages/db/src/schema/workspaces.ts index 7c2aa3c2..faed5922 100644 --- a/packages/db/src/schema/workspaces.ts +++ b/packages/db/src/schema/workspaces.ts @@ -10,20 +10,24 @@ import { varchar, } from "drizzle-orm/pg-core"; +import { boards } from "./boards"; import { users } from "./users"; -export const memberRoleEnum = pgEnum("role", ["admin", "member", "guest"]); -export const memberStatusEnum = pgEnum("member_status", [ - "invited", - "active", - "removed", -]); -export const slugTypeEnum = pgEnum("slug_type", ["reserved", "premium"]); -export const workspacePlanEnum = pgEnum("workspace_plan", [ - "free", - "pro", - "enterprise", -]); +export const memberRoles = ["admin", "member", "guest"] as const; +export type MemberRole = (typeof memberRoles)[number]; +export const memberRoleEnum = pgEnum("role", memberRoles); + +export const memberStatuses = ["invited", "active", "removed"] as const; +export type MemberStatus = (typeof memberStatuses)[number]; +export const memberStatusEnum = pgEnum("member_status", memberStatuses); + +export const slugTypes = ["reserved", "premium"] as const; +export type SlugType = (typeof slugTypes)[number]; +export const slugTypeEnum = pgEnum("slug_type", slugTypes); + +export const workspacePlans = ["free", "pro", "enterprise"] as const; +export type WorkspacePlan = (typeof workspacePlans)[number]; +export const workspacePlanEnum = pgEnum("workspace_plan", workspacePlans); export const workspaces = pgTable("workspace", { id: bigserial("id", { mode: "number" }).primaryKey(), @@ -44,6 +48,7 @@ export const workspaces = pgTable("workspace", { export const workspaceRelations = relations(workspaces, ({ one, many }) => ({ user: one(users, { fields: [workspaces.createdBy], references: [users.id] }), members: many(workspaceMembers), + boards: many(boards), })); export const workspaceMembers = pgTable("workspace_members", { @@ -64,6 +69,20 @@ export const workspaceMembers = pgTable("workspace_members", { status: memberStatusEnum("status").default("invited").notNull(), }).enableRLS(); +export const workspaceMembersRelations = relations( + workspaceMembers, + ({ one }) => ({ + user: one(users, { + fields: [workspaceMembers.userId], + references: [users.id], + }), + workspace: one(workspaces, { + fields: [workspaceMembers.workspaceId], + references: [workspaces.id], + }), + }), +); + export const slugs = pgTable("workspace_slugs", { slug: varchar("slug", { length: 255 }).notNull().unique(), type: slugTypeEnum("type").notNull(),