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 { 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);

View File

@@ -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) {

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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({

View File

@@ -17,15 +17,15 @@ export interface User {
interface CreateContextOptions {
user: User | null;
db: SupabaseClient<Database>;
drizzleDb: dbClient;
supabaseClient: SupabaseClient<Database>;
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

View File

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

View File

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

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";
export const create = async (
db: SupabaseClient<Database>,
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<Database>,
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<Database>,
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<Database>,
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;
};

View File

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

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";
export const create = async (
db: SupabaseClient<Database>,
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<Database>,
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;
};

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";
export const create = async (
db: SupabaseClient<Database>,
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<Database>,
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<Database>,
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<Database>,
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<Database>,
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<Database>,
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;
};

View File

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

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";
export const create = async (
db: SupabaseClient<Database>,
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<Database>,
publicId: string,
) => {
const { data } = await db
.from("workspace_members")
.select()
.eq("publicId", publicId)
.limit(1)
.single();
return data;
};
export const acceptInvite = async (
db: SupabaseClient<Database>,
id: number,
) => {
const { data } = await db
.from("workspace_members")
.update({ status: "active" })
.eq("id", id);
return data;
};
export const softDelete = async (
db: SupabaseClient<Database>,
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;
};

View File

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

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";
export const create = async (
db: SupabaseClient<Database>,
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<Database>,
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<Database>,
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);
.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<Database>,
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;
};

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 (
db: SupabaseClient<Database>,
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),
});
};

View File

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

View File

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