383 lines
8.7 KiB
TypeScript
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);
|
|
};
|