Files
kan/packages/db/src/repository/workspace.repo.ts

383 lines
8.7 KiB
TypeScript

import {
and,
count,
desc,
eq,
ilike,
inArray,
isNull,
or,
sql,
} from "drizzle-orm";
import type { dbClient } from "@kan/db/client";
import {
boards,
cards,
lists,
workspaceMembers,
workspaces,
} from "@kan/db/schema";
import { generateUID } from "@kan/shared/utils";
export const getCount = async (db: dbClient) => {
const result = await db
.select({ count: count() })
.from(workspaces)
.where(isNull(workspaces.deletedAt));
return result[0]?.count ?? 0;
};
export const create = async (
db: dbClient,
workspaceInput: {
publicId?: string;
name: string;
slug: string;
createdBy: string;
createdByEmail: string;
},
) => {
const [workspace] = await db
.insert(workspaces)
.values({
publicId: workspaceInput.publicId ?? generateUID(),
name: workspaceInput.name,
slug: workspaceInput.slug,
createdBy: workspaceInput.createdBy,
})
.returning({
id: workspaces.id,
publicId: workspaces.publicId,
name: workspaces.name,
slug: workspaces.slug,
description: workspaces.description,
plan: workspaces.plan,
});
if (workspace) {
await db.insert(workspaceMembers).values({
publicId: generateUID(),
userId: workspaceInput.createdBy,
email: workspaceInput.createdByEmail,
workspaceId: workspace.id,
createdBy: workspaceInput.createdBy,
role: "admin",
status: "active",
});
}
const newWorkspace = { ...workspace };
delete newWorkspace.id;
return newWorkspace;
};
export const update = async (
db: dbClient,
workspacePublicId: string,
workspaceInput: {
name?: string;
slug?: string;
plan?: "free" | "pro" | "enterprise";
description?: string;
},
) => {
const [result] = await db
.update(workspaces)
.set({
name: workspaceInput.name,
slug: workspaceInput.slug,
plan: workspaceInput.plan,
description: workspaceInput.description,
})
.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,
slug: true,
},
where: eq(workspaces.publicId, workspacePublicId),
});
};
export const getById = (db: dbClient, workspaceId: number) => {
return db.query.workspaces.findFirst({
columns: {
id: true,
publicId: true,
name: true,
plan: true,
slug: true,
},
where: eq(workspaces.id, workspaceId),
});
};
export const getByPublicIdWithMembers = (
db: dbClient,
workspacePublicId: string,
) => {
return db.query.workspaces.findFirst({
columns: {
id: true,
publicId: true,
},
with: {
members: {
columns: {
publicId: true,
email: true,
role: true,
status: true,
},
where: isNull(workspaceMembers.deletedAt),
with: {
user: {
columns: {
id: true,
name: true,
email: true,
image: true,
},
},
},
},
subscriptions: {
columns: {
id: true,
plan: true,
status: true,
seats: true,
unlimitedSeats: true,
periodStart: true,
periodEnd: true,
},
},
},
where: and(
eq(workspaces.publicId, workspacePublicId),
isNull(workspaces.deletedAt),
),
});
};
export const getBySlugWithBoards = (db: dbClient, workspaceSlug: string) => {
return db.query.workspaces.findFirst({
columns: {
id: true,
publicId: true,
name: true,
description: true,
slug: true,
},
with: {
boards: {
columns: {
publicId: true,
slug: true,
name: true,
},
where: and(isNull(boards.deletedAt), eq(boards.visibility, "public")),
},
},
where: and(
eq(workspaces.slug, workspaceSlug),
isNull(workspaces.deletedAt),
),
});
};
export const getAllByUserId = async (db: dbClient, userId: string) => {
const result = await db.query.workspaceMembers.findMany({
columns: {
role: true,
},
with: {
workspace: {
columns: {
publicId: true,
name: true,
description: true,
slug: true,
plan: true,
deletedAt: true,
},
// https://github.com/drizzle-team/drizzle-orm/issues/2903
// where: isNull(workspaces.deletedAt),
},
},
where: and(
eq(workspaceMembers.userId, userId),
eq(workspaceMembers.status, "active"),
isNull(workspaceMembers.deletedAt),
),
});
return result.filter((member) => !member.workspace.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: dbClient,
workspaceSlug: string,
) => {
const result = await db.query.workspaces.findFirst({
columns: {
id: true,
},
where: and(
eq(workspaces.slug, workspaceSlug),
isNull(workspaces.deletedAt),
),
});
return result === undefined;
};
export const isUserInWorkspace = async (
db: dbClient,
userId: string,
workspaceId: number,
role?: "admin" | "member",
) => {
const result = await db.query.workspaceMembers.findFirst({
columns: {
id: true,
},
where: and(
eq(workspaceMembers.userId, userId),
eq(workspaceMembers.workspaceId, workspaceId),
eq(workspaceMembers.status, "active"),
isNull(workspaceMembers.deletedAt),
role ? eq(workspaceMembers.role, role) : undefined,
),
});
return result?.id !== undefined;
};
export const searchBoardsAndCards = async (
db: dbClient,
workspaceId: number,
query: string,
limit = 20,
) => {
const searchQuery = `%${query}%`;
// Search for boards
const boardResults = await db
.select({
publicId: boards.publicId,
title: boards.name,
description: boards.description,
slug: boards.slug,
updatedAt: boards.updatedAt,
createdAt: boards.createdAt,
})
.from(boards)
.where(
and(
eq(boards.workspaceId, workspaceId),
// Combine exact and fuzzy matching
or(
ilike(boards.name, `%${query}%`), // Exact substring match
sql`similarity(${boards.name}, ${query}) > 0.2`, // Fuzzy match
),
isNull(boards.deletedAt),
),
)
.orderBy(
sql`CASE WHEN ${boards.name} ILIKE ${`%${query}%`} THEN 1 ELSE 0 END DESC`,
sql`similarity(${boards.name}, ${query}) DESC`,
desc(boards.updatedAt),
)
.limit(Math.ceil(limit * 0.4));
// Search for cards
const cardResults = await db
.select({
publicId: cards.publicId,
title: cards.title,
description: cards.description,
boardPublicId: boards.publicId,
boardName: boards.name,
listName: lists.name,
updatedAt: cards.updatedAt,
createdAt: cards.createdAt,
})
.from(cards)
.innerJoin(lists, eq(cards.listId, lists.id))
.innerJoin(boards, eq(lists.boardId, boards.id))
.where(
and(
eq(boards.workspaceId, workspaceId),
or(
ilike(cards.title, searchQuery),
sql`similarity(${cards.title}, ${query}) > 0.2`,
),
isNull(cards.deletedAt),
isNull(lists.deletedAt),
isNull(boards.deletedAt),
),
)
.orderBy(
sql`CASE WHEN ${cards.title} ILIKE ${searchQuery} THEN 1 ELSE 0 END DESC`,
sql`similarity(${cards.title}, ${query}) DESC`,
desc(cards.updatedAt),
)
.limit(Math.floor(limit * 0.6));
// Combine results
const allResults = [
...boardResults.map((board) => ({ ...board, type: "board" as const })),
...cardResults.map((card) => ({ ...card, type: "card" as const })),
];
// Ensure we don't exceed the total limit
return allResults.slice(0, limit);
};