refactor: update repo funcs to drizzle

This commit is contained in:
Henry
2025-04-24 14:07:41 +01:00
parent c1348cf455
commit d83e9d4dc5
20 changed files with 659 additions and 604 deletions

View File

@@ -3,6 +3,7 @@ import type { NextRequest } from "next/server";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { Stripe } from "stripe"; import { Stripe } from "stripe";
import { createDrizzleClient } from "@kan/db/client";
import * as memberRepo from "@kan/db/repository/member.repo"; import * as memberRepo from "@kan/db/repository/member.repo";
import * as userRepo from "@kan/db/repository/user.repo"; import * as userRepo from "@kan/db/repository/user.repo";
import { createNextClient } from "@kan/supabase/clients"; import { createNextClient } from "@kan/supabase/clients";
@@ -46,21 +47,23 @@ export default async function handler(req: NextRequest) {
const response = NextResponse.next(); const response = NextResponse.next();
if ((tokenHash && type) ?? code) { if ((tokenHash && type) ?? code) {
const db = createNextClient(req, response); const supabaseClient = createNextClient(req, response);
if (tokenHash && type) { if (tokenHash && type) {
authRes = await db.auth.verifyOtp({ authRes = await supabaseClient.auth.verifyOtp({
type: type as EmailOtpType, type: type as EmailOtpType,
token_hash: tokenHash, token_hash: tokenHash,
}); });
} }
if (code) { if (code) {
authRes = await db.auth.exchangeCodeForSession(code); authRes = await supabaseClient.auth.exchangeCodeForSession(code);
} }
const user = authRes?.data.user; const user = authRes?.data.user;
const db = createDrizzleClient();
if (user?.id && user.email) { if (user?.id && user.email) {
const existingUser = await userRepo.getById(db, user.id); const existingUser = await userRepo.getById(db, user.id);

View File

@@ -2,6 +2,7 @@ import type { NextRequest } from "next/server";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { Stripe } from "stripe"; import { Stripe } from "stripe";
import { createDrizzleClient } from "@kan/db/client";
import * as userRepo from "@kan/db/repository/user.repo"; import * as userRepo from "@kan/db/repository/user.repo";
import { createNextClient } from "@kan/supabase/clients"; import { createNextClient } from "@kan/supabase/clients";
@@ -25,8 +26,8 @@ export default async function handler(req: NextRequest) {
try { try {
const response = NextResponse.next(); 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) { if (!data.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { 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); const user = await userRepo.getById(db, data.user.id);
if (!user?.stripeCustomerId) { if (!user?.stripeCustomerId) {

View File

@@ -3,6 +3,7 @@ import { NextResponse } from "next/server";
import { Stripe } from "stripe"; import { Stripe } from "stripe";
import { z } from "zod"; import { z } from "zod";
import { createDrizzleClient } from "@kan/db/client";
import * as userRepo from "@kan/db/repository/user.repo"; import * as userRepo from "@kan/db/repository/user.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo"; import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { createNextClient } from "@kan/supabase/clients"; import { createNextClient } from "@kan/supabase/clients";
@@ -42,9 +43,9 @@ export default async function handler(req: NextRequest) {
try { try {
const response = NextResponse.next(); 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) { if (!data.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { 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); const user = await userRepo.getById(db, data.user.id);
if (!user) { if (!user) {
@@ -87,7 +90,7 @@ export default async function handler(req: NextRequest) {
const workspace = await workspaceRepo.getAllByUserId(db, user.id); const workspace = await workspaceRepo.getAllByUserId(db, user.id);
const isMemberOfWorkspace = workspace.some( const isMemberOfWorkspace = workspace.some(
({ workspace }) => workspace?.publicId === body.workspacePublicId, ({ workspace }) => workspace.publicId === body.workspacePublicId,
); );
if (!isMemberOfWorkspace) { if (!isMemberOfWorkspace) {

View File

@@ -17,7 +17,7 @@ export const authRouter = createTRPCRouter({
.input(z.object({ email: z.string() })) .input(z.object({ email: z.string() }))
.output(z.object({ success: z.boolean() })) .output(z.object({ success: z.boolean() }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const { data } = await ctx.db.auth.signInWithOtp({ const { data } = await ctx.supabaseClient.auth.signInWithOtp({
email: input.email, email: input.email,
options: { options: {
emailRedirectTo: `${process.env.WEBSITE_URL}`, emailRedirectTo: `${process.env.WEBSITE_URL}`,
@@ -52,7 +52,7 @@ export const authRouter = createTRPCRouter({
code: "BAD_REQUEST", code: "BAD_REQUEST",
}); });
const { data } = await ctx.db.auth.signInWithOAuth({ const { data } = await ctx.supabaseClient.auth.signInWithOAuth({
provider: "google", provider: "google",
options: { options: {
queryParams: { queryParams: {

View File

@@ -333,7 +333,7 @@ export const cardRouter = createTRPCRouter({
const deletedComment = await cardCommentRepo.softDelete(ctx.db, { const deletedComment = await cardCommentRepo.softDelete(ctx.db, {
commentId: existingComment.id, commentId: existingComment.id,
deletedAt: new Date().toISOString(), deletedAt: new Date(),
deletedBy: userId, deletedBy: userId,
}); });

View File

@@ -37,7 +37,7 @@ export const userRouter = createTRPCRouter({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
}); });
const result = await userRepo.getById(ctx.drizzleDb, userId); const result = await userRepo.getById(ctx.db, userId);
if (!result?.name) { if (!result?.name) {
throw new TRPCError({ throw new TRPCError({

View File

@@ -17,15 +17,15 @@ export interface User {
interface CreateContextOptions { interface CreateContextOptions {
user: User | null; user: User | null;
db: SupabaseClient<Database>; supabaseClient: SupabaseClient<Database>;
drizzleDb: dbClient; db: dbClient;
} }
export const createInnerTRPCContext = (opts: CreateContextOptions) => { export const createInnerTRPCContext = (opts: CreateContextOptions) => {
return { return {
user: opts.user, user: opts.user,
db: opts.db, db: opts.db,
drizzleDb: opts.drizzleDb, supabaseClient: opts.supabaseClient,
}; };
}; };
@@ -33,36 +33,36 @@ export const createTRPCContext = async ({
req, req,
resHeaders, resHeaders,
}: FetchCreateContextFnOptions) => { }: FetchCreateContextFnOptions) => {
const db = createTRPCClient(req, resHeaders); const supabaseClient = createTRPCClient(req, resHeaders);
const { const {
data: { user }, 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) => { export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
const db = createNextApiClient(req); const supabaseClient = createNextApiClient(req);
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
const accessToken = authHeader?.startsWith("Bearer ") const accessToken = authHeader?.startsWith("Bearer ")
? authHeader.substring(7) ? authHeader.substring(7)
: null; : null;
const drizzleDb = createDrizzleClient(); const db = createDrizzleClient();
if (!accessToken) { if (!accessToken) {
return createInnerTRPCContext({ db, user: null, drizzleDb }); return createInnerTRPCContext({ db, user: null, supabaseClient });
} }
const { const {
data: { user }, 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 const t = initTRPC

View File

@@ -1,19 +1,19 @@
import type { SupabaseClient } from "@supabase/supabase-js"; 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 type { Database } from "@kan/db/types/database.types";
import { boards, cards, lists, workspaceMembers } from "@kan/db/schema";
import { generateUID } from "@kan/shared/utils"; import { generateUID } from "@kan/shared/utils";
export const getAllByWorkspaceId = async ( export const getAllByWorkspaceId = (db: dbClient, workspaceId: number) => {
db: SupabaseClient<Database>, return db.query.boards.findMany({
workspaceId: number, columns: {
) => { publicId: true,
const { data } = await db name: true,
.from("board") },
.select(`publicId, name`) where: and(eq(boards.workspaceId, workspaceId), isNull(boards.deletedAt)),
.is("deletedAt", null) });
.eq("workspaceId", workspaceId);
return data ?? [];
}; };
export const getIdByPublicId = async ( export const getIdByPublicId = async (
@@ -31,93 +31,133 @@ export const getIdByPublicId = async (
}; };
export const getByPublicId = async ( export const getByPublicId = async (
db: SupabaseClient<Database>, db: dbClient,
boardPublicId: string, boardPublicId: string,
filters: { filters: {
members: string[]; members: string[];
labels: string[]; labels: string[];
}, },
) => { ) => {
let query = db const board = await db.query.boards.findFirst({
.from("board") columns: {
.select( publicId: true,
` name: true,
publicId, slug: true,
name, visibility: true,
slug, },
visibility, with: {
workspace ( workspace: {
publicId, columns: {
members:workspace_members ( publicId: true,
publicId, },
user!workspace_members_userId_user_id_fk ( with: {
name, members: {
email, columns: {
image publicId: true,
) },
) with: {
), user: {
labels:label ( columns: {
publicId, name: true,
name, email: true,
colourCode image: true,
), },
lists:list ( },
publicId, },
name, where: isNull(workspaceMembers.deletedAt),
boardId, },
index, },
cards:card ( },
publicId, labels: {
title, columns: {
description, publicId: true,
listId, name: true,
index, colourCode: true,
labels:label( },
publicId, },
name, lists: {
colourCode columns: {
), publicId: true,
_filteredLabels:label${filters.labels.length > 0 ? "!inner" : ""} ( name: true,
publicId boardId: true,
), index: true,
members:workspace_members ( },
publicId, with: {
user!workspace_members_userId_user_id_fk ( cards: {
name, columns: {
email, publicId: true,
image title: true,
) description: true,
), listId: true,
_filteredMembers:workspace_members${filters.members.length > 0 ? "!inner" : ""} ( index: true,
publicId },
) with: {
) labels: {
) with: {
`, label: {
) columns: {
.eq("publicId", boardPublicId) publicId: true,
.is("deletedAt", null) name: true,
.is("lists.deletedAt", null) colourCode: true,
.is("lists.cards.deletedAt", null) },
.is("workspace.members.deletedAt", null) // where: inArray(label.publicId, filters.labels),
.is("lists.cards.members.deletedAt", null); },
},
},
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) { if (!board) return null;
query = query.in("lists.cards._filteredLabels.publicId", filters.labels);
}
if (filters.members.length > 0) { const formattedResult = {
query = query.in("lists.cards._filteredMembers.publicId", filters.members); ...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 return formattedResult;
.order("index", { foreignTable: "list", ascending: true })
.order("index", { foreignTable: "list.card", ascending: true })
.limit(1)
.single();
return data;
}; };
export const getBySlug = async ( export const getBySlug = async (

View File

@@ -1,12 +1,12 @@
import type { SupabaseClient } from "@supabase/supabase-js"; import type { dbClient } from "@kan/db/client";
import type { ActivityType } from "@kan/db/schema";
import type { Database } from "@kan/db/types/database.types"; import { cardActivities } from "@kan/db/schema";
import { generateUID } from "@kan/shared/utils"; import { generateUID } from "@kan/shared/utils";
export const create = async ( export const create = async (
db: SupabaseClient<Database>, db: dbClient,
activityInput: { activityInput: {
type: Database["public"]["Enums"]["card_activity_type"]; type: ActivityType;
cardId: number; cardId: number;
fromIndex?: number; fromIndex?: number;
toIndex?: number; toIndex?: number;
@@ -24,9 +24,9 @@ export const create = async (
toComment?: string; toComment?: string;
}, },
) => { ) => {
const { data } = await db const [result] = await db
.from("card_activity") .insert(cardActivities)
.insert({ .values({
publicId: generateUID(), publicId: generateUID(),
type: activityInput.type, type: activityInput.type,
cardId: activityInput.cardId, cardId: activityInput.cardId,
@@ -45,17 +45,15 @@ export const create = async (
fromComment: activityInput.fromComment, fromComment: activityInput.fromComment,
toComment: activityInput.toComment, toComment: activityInput.toComment,
}) })
.select(`id`) .returning({ id: cardActivities.id });
.limit(1)
.single();
return data; return result;
}; };
export const bulkCreate = async ( export const bulkCreate = async (
db: SupabaseClient<Database>, db: dbClient,
activityInputs: { activityInputs: {
type: Database["public"]["Enums"]["card_activity_type"]; type: ActivityType;
cardId: number; cardId: number;
fromIndex?: number; fromIndex?: number;
toIndex?: number; toIndex?: number;
@@ -75,10 +73,10 @@ export const bulkCreate = async (
publicId: generateUID(), publicId: generateUID(),
})); }));
const { data } = await db const results = await db
.from("card_activity") .insert(cardActivities)
.insert(activitiesWithPublicIds) .values(activitiesWithPublicIds)
.select("id"); .returning({ id: cardActivities.id });
return data; return results;
}; };

View File

@@ -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"; import { generateUID } from "@kan/shared/utils";
export const create = async ( export const create = async (
db: SupabaseClient<Database>, db: dbClient,
commentInput: { commentInput: {
cardId: number; cardId: number;
comment: string; comment: string;
createdBy: string; createdBy: string;
}, },
) => { ) => {
const { data } = await db const [result] = await db
.from("card_comments") .insert(comments)
.insert({ .values({
publicId: generateUID(), publicId: generateUID(),
comment: commentInput.comment, comment: commentInput.comment,
createdBy: commentInput.createdBy, createdBy: commentInput.createdBy,
cardId: commentInput.cardId, cardId: commentInput.cardId,
}) })
.select(`id, publicId, comment`) .returning({
.limit(1) id: comments.id,
.single(); publicId: comments.publicId,
comment: comments.comment,
});
return data; return result;
}; };
export const getByPublicId = async ( export const getByPublicId = (db: dbClient, publicId: string) => {
db: SupabaseClient<Database>, return db.query.comments.findFirst({
publicId: string, columns: {
) => { id: true,
const { data } = await db publicId: true,
.from("card_comments") comment: true,
.select(`id, publicId, comment, createdBy`) createdBy: true,
.eq("publicId", publicId) },
.limit(1) where: eq(comments.publicId, publicId),
.single(); });
return data;
}; };
export const update = async ( export const update = async (
db: SupabaseClient<Database>, db: dbClient,
commentInput: { commentInput: {
id: number; id: number;
comment: string; comment: string;
}, },
) => { ) => {
const { data } = await db const [result] = await db
.from("card_comments") .update(comments)
.update({ .set({
comment: commentInput.comment, comment: commentInput.comment,
updatedAt: new Date().toISOString(), updatedAt: new Date(),
}) })
.eq("id", commentInput.id) .where(eq(comments.id, commentInput.id))
.select(`id, publicId, comment`) .returning({
.limit(1) id: comments.id,
.order("id", { ascending: false }) publicId: comments.publicId,
.single(); comment: comments.comment,
});
return data; return result;
}; };
export const softDelete = async ( export const softDelete = async (
db: SupabaseClient<Database>, db: dbClient,
args: { args: {
commentId: number; commentId: number;
deletedAt: string; deletedAt: Date;
deletedBy: string; deletedBy: string;
}, },
) => { ) => {
const { data } = await db const [result] = await db
.from("card_comments") .update(comments)
.update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) .set({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
.eq("id", args.commentId) .where(eq(comments.id, args.commentId))
.select(`id`) .returning({ id: comments.id });
.order("id", { ascending: true })
.limit(1)
.single();
return data; return result;
}; };

View File

@@ -1,25 +1,22 @@
import type { SupabaseClient } from "@supabase/supabase-js"; import type { dbClient } from "@kan/db/client";
import { feedback } from "@kan/db/schema";
import type { Database } from "@kan/db/types/database.types";
export const create = async ( export const create = async (
db: SupabaseClient<Database>, db: dbClient,
feedbackInput: { feedbackInput: {
feedback: string; feedback: string;
createdBy: string; createdBy: string;
url: string; url: string;
}, },
) => { ) => {
const { data } = await db const [result] = await db
.from("feedback") .insert(feedback)
.insert({ .values({
feedback: feedbackInput.feedback, feedback: feedbackInput.feedback,
createdBy: feedbackInput.createdBy, createdBy: feedbackInput.createdBy,
url: feedbackInput.url, url: feedbackInput.url,
}) })
.select(`id`) .returning({ id: feedback.id });
.limit(1)
.single();
return data; return result;
}; };

View File

@@ -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"; import { generateUID } from "@kan/shared/utils";
export const create = async ( export const create = async (
db: SupabaseClient<Database>, db: dbClient,
importInput: { source: string; createdBy: string }, importInput: { source: string; createdBy: string },
) => { ) => {
const { data } = await db const [result] = await db
.from("import") .insert(imports)
.insert({ .values({
publicId: generateUID(), publicId: generateUID(),
source: "trello", source: "trello",
createdBy: importInput.createdBy, createdBy: importInput.createdBy,
status: "started", status: "started",
}) })
.select(`id`) .returning({ id: imports.id });
.limit(1)
.single();
return data; return result;
}; };
export const update = async ( export const update = async (
db: SupabaseClient<Database>, db: dbClient,
importInput: { status: "started" | "success" | "failed" }, importInput: { status: "started" | "success" | "failed" },
args: { importId: number }, args: { importId: number },
) => { ) => {
const { data } = await db const [result] = await db
.from("import") .update(imports)
.update({ status: importInput.status }) .set({ status: importInput.status })
.eq("importId", args.importId) .where(eq(imports.id, args.importId))
.limit(1) .returning({ id: imports.id, status: imports.status });
.single();
return data; return result;
}; };

View File

@@ -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"; import { generateUID } from "@kan/shared/utils";
export const create = async ( export const create = async (
db: SupabaseClient<Database>, db: dbClient,
labelInput: { labelInput: {
name: string; name: string;
colourCode: string; colourCode: string;
@@ -13,31 +14,30 @@ export const create = async (
cardId?: number; cardId?: number;
}, },
) => { ) => {
const { data } = await db const [result] = await db
.from("label") .insert(labels)
.insert({ .values({
publicId: generateUID(), publicId: generateUID(),
name: labelInput.name, name: labelInput.name,
colourCode: labelInput.colourCode, colourCode: labelInput.colourCode,
createdBy: labelInput.createdBy, createdBy: labelInput.createdBy,
boardId: labelInput.boardId, boardId: labelInput.boardId,
}) })
.select(`id`) .returning({ id: labels.id });
.limit(1)
.single();
if (labelInput.cardId && data) if (labelInput.cardId && result) {
await db.from("_card_labels").insert({ await db.insert(cardsToLabels).values({
cardId: labelInput.cardId, cardId: labelInput.cardId,
labelId: data.id, labelId: result.id,
}); });
}
return data; return result;
}; };
export const bulkCreate = async ( export const bulkCreate = async (
db: SupabaseClient<Database>, db: dbClient,
labels: { labelsInput: {
publicId: string; publicId: string;
name: string; name: string;
colourCode: string; colourCode: string;
@@ -45,61 +45,64 @@ export const bulkCreate = async (
createdBy: string; 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 ( export const getAllByPublicIds = (db: dbClient, labelPublicIds: string[]) => {
db: SupabaseClient<Database>, return db.query.labels.findMany({
labelPublicIds: string[], columns: {
) => { id: true,
const { data } = await db },
.from("label") where: inArray(labels.publicId, labelPublicIds),
.select(`id`) });
.in("publicId", labelPublicIds);
return data;
}; };
export const getByPublicId = async ( export const getByPublicId = async (db: dbClient, labelPublicId: string) => {
db: SupabaseClient<Database>, return db.query.labels.findFirst({
labelPublicId: string, columns: {
) => { id: true,
const { data } = await db publicId: true,
.from("label") name: true,
.select(`id, publicId, name, colourCode`) colourCode: true,
.eq("publicId", labelPublicId) },
.limit(1) where: eq(labels.publicId, labelPublicId),
.single(); });
return data;
}; };
export const update = async ( export const update = async (
db: SupabaseClient<Database>, db: dbClient,
labelInput: { labelInput: {
labelPublicId: string; labelPublicId: string;
name: string; name: string;
colourCode: string; colourCode: string;
}, },
) => { ) => {
const { data } = await db const [result] = await db
.from("label") .update(labels)
.update({ .set({
name: labelInput.name, name: labelInput.name,
colourCode: labelInput.colourCode, 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 ( export const hardDelete = async (db: dbClient, labelId: number) => {
db: SupabaseClient<Database>, const [result] = await db
labelId: number, .delete(labels)
) => { .where(eq(labels.id, labelId))
const { data } = await db.from("label").delete().eq("id", labelId); .returning({ id: labels.id });
return data; return result;
}; };

View File

@@ -1,10 +1,13 @@
import type { SupabaseClient } from "@supabase/supabase-js"; 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 type { Database } from "@kan/db/types/database.types";
import { lists } from "@kan/db/schema";
import { generateUID } from "@kan/shared/utils"; import { generateUID } from "@kan/shared/utils";
export const create = async ( export const create = async (
db: SupabaseClient<Database>, db: dbClient,
listInput: { listInput: {
name: string; name: string;
createdBy: string; createdBy: string;
@@ -13,9 +16,9 @@ export const create = async (
importId?: number; importId?: number;
}, },
) => { ) => {
const { data } = await db const [result] = await db
.from("list") .insert(lists)
.insert({ .values({
publicId: generateUID(), publicId: generateUID(),
name: listInput.name, name: listInput.name,
createdBy: listInput.createdBy, createdBy: listInput.createdBy,
@@ -23,48 +26,45 @@ export const create = async (
index: listInput.index, index: listInput.index,
importId: listInput.importId, importId: listInput.importId,
}) })
.select( .returning({
` id: lists.id,
id, publicId: lists.publicId,
publicId, name: lists.name,
name });
`,
)
.limit(1)
.single();
return data; return result;
}; };
export const getByPublicId = async ( export const getByPublicId = async (db: dbClient, listPublicId: string) => {
db: SupabaseClient<Database>, return db.query.lists.findFirst({
listPublicId: string, columns: {
) => { id: true,
const { data } = await db boardId: true,
.from("list") index: true,
.select(`id, boardId, index`) },
.eq("publicId", listPublicId) where: eq(lists.publicId, listPublicId),
.limit(1) });
.single();
return data;
}; };
export const getWithCardsByPublicId = async ( export const getWithCardsByPublicId = async (
db: SupabaseClient<Database>, db: dbClient,
listPublicId: string, listPublicId: string,
) => { ) => {
const { data } = await db return db.query.lists.findFirst({
.from("list") columns: {
.select(`id, cards:card (index)`) id: true,
.eq("publicId", listPublicId) },
.is("deletedAt", null) with: {
.is("card.deletedAt", null) cards: {
.order("index", { foreignTable: "card", ascending: false }) columns: {
.limit(1) index: true,
.single(); },
where: isNull(lists.deletedAt),
return data; orderBy: [desc(lists.index)],
},
},
where: and(eq(lists.publicId, listPublicId), isNull(lists.deletedAt)),
});
}; };
export const update = async ( export const update = async (
@@ -121,41 +121,37 @@ export const shiftIndex = async (
}; };
export const softDeleteAllByBoardId = async ( export const softDeleteAllByBoardId = async (
db: SupabaseClient<Database>, db: dbClient,
args: { args: {
boardId: number; boardId: number;
deletedAt: string;
deletedBy: string; deletedBy: string;
}, },
) => { ) => {
const { data } = await db const [result] = await db
.from("list") .update(lists)
.update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) .set({ deletedAt: new Date(), deletedBy: args.deletedBy })
.eq("boardId", args.boardId) .where(and(eq(lists.boardId, args.boardId), isNull(lists.deletedAt)))
.is("deletedAt", null) .returning({
.select(`id`) id: lists.id,
.order("id", { ascending: true }); });
return data; return result;
}; };
export const softDeleteById = async ( export const softDeleteById = async (
db: SupabaseClient<Database>, db: dbClient,
args: { args: {
listId: number; listId: number;
deletedAt: string;
deletedBy: string; deletedBy: string;
}, },
) => { ) => {
const { data } = await db const [updatedList] = await db
.from("list") .update(lists)
.update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) .set({ deletedAt: new Date(), deletedBy: args.deletedBy })
.eq("id", args.listId) .where(and(eq(lists.id, args.listId), isNull(lists.deletedAt)))
.is("deletedAt", null) .returning({
.select(`id`) id: lists.id,
.order("id", { ascending: true }) });
.limit(1)
.single();
return data; return updatedList;
}; };

View File

@@ -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"; import { generateUID } from "@kan/shared/utils";
export const create = async ( export const create = async (
db: SupabaseClient<Database>, db: dbClient,
memberInput: { memberInput: {
userId: string; userId: string;
workspaceId: number; workspaceId: number;
createdBy: string; createdBy: string;
role: "admin" | "member" | "guest"; role: MemberRole;
status: "invited" | "active" | "removed"; status: MemberStatus;
}, },
) => { ) => {
const { data } = await db const [result] = await db
.from("workspace_members") .insert(workspaceMembers)
.insert({ .values({
publicId: generateUID(), publicId: generateUID(),
userId: memberInput.userId, userId: memberInput.userId,
workspaceId: memberInput.workspaceId, workspaceId: memberInput.workspaceId,
@@ -23,52 +25,53 @@ export const create = async (
role: memberInput.role, role: memberInput.role,
status: memberInput.status, status: memberInput.status,
}) })
.select(`id, publicId`) .returning({
.limit(1) id: workspaceMembers.id,
.single(); publicId: workspaceMembers.publicId,
});
return data;
}; return result;
};
export const getByPublicId = async (
db: SupabaseClient<Database>, export const getByPublicId = async (db: dbClient, publicId: string) => {
publicId: string, return db.query.workspaceMembers.findFirst({
) => { where: eq(workspaceMembers.publicId, publicId),
const { data } = await db });
.from("workspace_members") };
.select()
.eq("publicId", publicId) export const acceptInvite = async (db: dbClient, id: number) => {
.limit(1) const [result] = await db
.single(); .update(workspaceMembers)
.set({ status: "active" })
return data; .where(eq(workspaceMembers.id, id))
}; .returning({
id: workspaceMembers.id,
export const acceptInvite = async ( publicId: workspaceMembers.publicId,
db: SupabaseClient<Database>, });
id: number,
) => { return result;
const { data } = await db };
.from("workspace_members")
.update({ status: "active" }) export const softDelete = async (
.eq("id", id); db: dbClient,
args: {
return data; memberId: number;
}; deletedBy: string;
},
export const softDelete = async ( ) => {
db: SupabaseClient<Database>, const [result] = await db
args: { .update(workspaceMembers)
memberId: number; .set({ deletedAt: new Date(), deletedBy: args.deletedBy })
deletedAt: string; .where(
deletedBy: string; and(
}, eq(workspaceMembers.id, args.memberId),
) => { isNull(workspaceMembers.deletedAt),
const result = await db ),
.from("workspace_members") )
.update({ deletedAt: args.deletedAt, deletedBy: args.deletedBy }) .returning({
.eq("id", args.memberId) id: workspaceMembers.id,
.is("deletedAt", null); publicId: workspaceMembers.publicId,
});
return result; return result;
}; };

View File

@@ -1,12 +1,10 @@
import type { SupabaseClient } from "@supabase/supabase-js";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { dbClient } from "@kan/db/client"; import type { dbClient } from "@kan/db/client";
import type { Database } from "@kan/db/types/database.types"; import { users } from "@kan/db/schema";
import * as schema from "@kan/db/schema";
export const getById = async (db: dbClient, userId: string) => { export const getById = async (db: dbClient, userId: string) => {
const data = await db.query.users.findFirst({ return await db.query.users.findFirst({
columns: { columns: {
id: true, id: true,
name: true, name: true,
@@ -14,55 +12,48 @@ export const getById = async (db: dbClient, userId: string) => {
image: true, image: true,
stripeCustomerId: true, stripeCustomerId: true,
}, },
where: eq(schema.users.id, userId), where: eq(users.id, userId),
}); });
return data;
}; };
export const getByEmail = async ( export const getByEmail = (db: dbClient, email: string) => {
db: SupabaseClient<Database>, return db.query.users.findFirst({
email: string, columns: {
) => { id: true,
const { data } = await db name: true,
.from("user") email: true,
.select(`id, name, email`) },
.eq("email", email) where: eq(users.email, email),
.limit(1) });
.single();
return data;
}; };
export const create = async ( export const create = (
db: SupabaseClient<Database>, db: dbClient,
user: { id: string; email: string; stripeCustomerId: string }, user: { id: string; email: string; stripeCustomerId: string },
) => { ) => {
const { data } = await db return db.insert(users).values({
.from("user") id: user.id,
.insert({ email: user.email,
id: user.id, stripeCustomerId: user.stripeCustomerId,
email: user.email, });
stripeCustomerId: user.stripeCustomerId,
})
.select()
.limit(1)
.single();
return data;
}; };
export const update = async ( export const update = async (
db: SupabaseClient<Database>, db: dbClient,
userId: string, userId: string,
updates: { image?: string; name?: string }, updates: { image?: string; name?: string },
) => { ) => {
const { data } = await db const [result] = await db
.from("user") .update(users)
.update({ image: updates.image, name: updates.name }) .set({
.eq("id", userId) name: updates.name,
.select(`image, name`) image: updates.image,
.single(); })
.where(eq(users.id, userId))
.returning({
name: users.name,
image: users.image,
});
return data; return result;
}; };

View File

@@ -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"; import { generateUID } from "@kan/shared/utils";
export const create = async ( export const create = async (
db: SupabaseClient<Database>, db: dbClient,
workspaceInput: { workspaceInput: {
publicId?: string; publicId?: string;
name: string; name: string;
@@ -12,37 +13,42 @@ export const create = async (
createdBy: string; createdBy: string;
}, },
) => { ) => {
const { data } = await db const [workspace] = await db
.from("workspace") .insert(workspaces)
.insert({ .values({
publicId: workspaceInput.publicId ?? generateUID(), publicId: workspaceInput.publicId ?? generateUID(),
name: workspaceInput.name, name: workspaceInput.name,
slug: workspaceInput.slug, slug: workspaceInput.slug,
createdBy: workspaceInput.createdBy, createdBy: workspaceInput.createdBy,
}) })
.select(`id, publicId, name, slug, description, plan`) .returning({
.limit(1) id: workspaces.id,
.single(); publicId: workspaces.publicId,
name: workspaces.name,
slug: workspaces.slug,
description: workspaces.description,
plan: workspaces.plan,
});
if (data) if (workspace) {
await db.from("workspace_members").insert({ await db.insert(workspaceMembers).values({
publicId: generateUID(), publicId: generateUID(),
userId: workspaceInput.createdBy, userId: workspaceInput.createdBy,
workspaceId: data.id, workspaceId: workspace.id,
createdBy: workspaceInput.createdBy, createdBy: workspaceInput.createdBy,
role: "admin", role: "admin",
status: "active", status: "active",
}); });
}
const newWorkspace = { ...data }; const newWorkspace = { ...workspace };
delete newWorkspace.id; delete newWorkspace.id;
return newWorkspace; return newWorkspace;
}; };
export const update = async ( export const update = async (
db: SupabaseClient<Database>, db: dbClient,
workspacePublicId: string, workspacePublicId: string,
workspaceInput: { workspaceInput: {
name?: string; name?: string;
@@ -51,168 +57,162 @@ export const update = async (
description?: string; description?: string;
}, },
) => { ) => {
const { data } = await db const [result] = await db
.from("workspace") .update(workspaces)
.update({ .set({
name: workspaceInput.name, name: workspaceInput.name,
slug: workspaceInput.slug, slug: workspaceInput.slug,
plan: workspaceInput.plan, plan: workspaceInput.plan,
description: workspaceInput.description, description: workspaceInput.description,
}) })
.eq("publicId", workspacePublicId) .where(eq(workspaces.publicId, workspacePublicId))
.is("deletedAt", null); .returning({
id: workspaces.id,
return data; publicId: workspaces.publicId,
}; name: workspaces.name,
slug: workspaces.slug,
export const getByPublicId = async ( description: workspaces.description,
db: SupabaseClient<Database>, plan: workspaces.plan,
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<Database>,
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<Database>,
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<Database>,
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<Database>,
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<Database>,
memberPublicIds: string[],
) => {
const { data } = await db
.from("workspace_members")
.select(`id`)
.eq("publicId", memberPublicIds);
return data;
};
export const hardDelete = async (
db: SupabaseClient<Database>,
workspacePublicId: string,
) => {
const result = db
.from("workspace")
.delete()
.eq("publicId", workspacePublicId);
return result; 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 ( export const isWorkspaceSlugAvailable = async (
db: SupabaseClient<Database>, db: dbClient,
workspaceSlug: string, workspaceSlug: string,
) => { ) => {
const { data } = await db const result = await db.query.workspaces.findFirst({
.from("workspace") columns: {
.select("id") id: true,
.eq("slug", workspaceSlug) },
.is("deletedAt", null) where: eq(workspaces.slug, workspaceSlug),
.limit(1) });
.single();
return data === null; return result === undefined;
}; };

View File

@@ -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 ( export const getWorkspaceSlug = (db: dbClient, slug: string) => {
db: SupabaseClient<Database>, return db.query.slugs.findFirst({
slug: string, columns: {
) => { slug: true,
const { data } = await db type: true,
.from("workspace_slugs") },
.select(`slug, type`) where: eq(slugs.slug, slug),
.eq("slug", slug) });
.single();
return data;
}; };

View File

@@ -18,7 +18,7 @@ import { lists } from "./lists";
import { users } from "./users"; import { users } from "./users";
import { workspaceMembers } from "./workspaces"; import { workspaceMembers } from "./workspaces";
export const activityTypeEnum = pgEnum("card_activity_type", [ export const activityTypes = [
"card.created", "card.created",
"card.updated.title", "card.updated.title",
"card.updated.description", "card.updated.description",
@@ -32,7 +32,11 @@ export const activityTypeEnum = pgEnum("card_activity_type", [
"card.updated.comment.updated", "card.updated.comment.updated",
"card.updated.comment.deleted", "card.updated.comment.deleted",
"card.archived", "card.archived",
]); ] as const;
export type ActivityType = (typeof activityTypes)[number];
export const activityTypeEnum = pgEnum("card_activity_type", activityTypes);
export const cards = pgTable("card", { export const cards = pgTable("card", {
id: bigserial("id", { mode: "number" }).primaryKey(), id: bigserial("id", { mode: "number" }).primaryKey(),

View File

@@ -10,20 +10,24 @@ import {
varchar, varchar,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { boards } from "./boards";
import { users } from "./users"; import { users } from "./users";
export const memberRoleEnum = pgEnum("role", ["admin", "member", "guest"]); export const memberRoles = ["admin", "member", "guest"] as const;
export const memberStatusEnum = pgEnum("member_status", [ export type MemberRole = (typeof memberRoles)[number];
"invited", export const memberRoleEnum = pgEnum("role", memberRoles);
"active",
"removed", export const memberStatuses = ["invited", "active", "removed"] as const;
]); export type MemberStatus = (typeof memberStatuses)[number];
export const slugTypeEnum = pgEnum("slug_type", ["reserved", "premium"]); export const memberStatusEnum = pgEnum("member_status", memberStatuses);
export const workspacePlanEnum = pgEnum("workspace_plan", [
"free", export const slugTypes = ["reserved", "premium"] as const;
"pro", export type SlugType = (typeof slugTypes)[number];
"enterprise", 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", { export const workspaces = pgTable("workspace", {
id: bigserial("id", { mode: "number" }).primaryKey(), id: bigserial("id", { mode: "number" }).primaryKey(),
@@ -44,6 +48,7 @@ export const workspaces = pgTable("workspace", {
export const workspaceRelations = relations(workspaces, ({ one, many }) => ({ export const workspaceRelations = relations(workspaces, ({ one, many }) => ({
user: one(users, { fields: [workspaces.createdBy], references: [users.id] }), user: one(users, { fields: [workspaces.createdBy], references: [users.id] }),
members: many(workspaceMembers), members: many(workspaceMembers),
boards: many(boards),
})); }));
export const workspaceMembers = pgTable("workspace_members", { export const workspaceMembers = pgTable("workspace_members", {
@@ -64,6 +69,20 @@ export const workspaceMembers = pgTable("workspace_members", {
status: memberStatusEnum("status").default("invited").notNull(), status: memberStatusEnum("status").default("invited").notNull(),
}).enableRLS(); }).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", { export const slugs = pgTable("workspace_slugs", {
slug: varchar("slug", { length: 255 }).notNull().unique(), slug: varchar("slug", { length: 255 }).notNull().unique(),
type: slugTypeEnum("type").notNull(), type: slugTypeEnum("type").notNull(),