feat: build collaboration workspaces
This commit is contained in:
180
modules/notices/server/operations.ts
Normal file
180
modules/notices/server/operations.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { and, desc, eq, inArray, sql } from "drizzle-orm";
|
||||
|
||||
import { getDatabase } from "@/modules/core/server/db";
|
||||
import { accounts, noticeReadReceipts, notices } from "@/modules/core/server/schema";
|
||||
import type { Notice, NoticeCenterData, NoticeInput } from "@/modules/notices/types";
|
||||
|
||||
export type NoticeMutationFailure = {
|
||||
success: false;
|
||||
reason: string;
|
||||
status: number;
|
||||
};
|
||||
|
||||
type ReadCountRow = {
|
||||
noticeId: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export function isNoticeMutationFailure(value: unknown): value is NoticeMutationFailure {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"success" in value &&
|
||||
(value as { success?: unknown }).success === false
|
||||
);
|
||||
}
|
||||
|
||||
function iso(value: Date): string {
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
function optionalIso(value: Date | null): string | undefined {
|
||||
return value ? iso(value) : undefined;
|
||||
}
|
||||
|
||||
function toNotice(
|
||||
row: typeof notices.$inferSelect,
|
||||
accountNameById: Map<string, string>,
|
||||
readCountByNoticeId: Map<string, number>,
|
||||
readNoticeIds: Set<string>,
|
||||
): Notice {
|
||||
const createdByAccountId = row.createdByAccountId ?? undefined;
|
||||
const updatedByAccountId = row.updatedByAccountId ?? undefined;
|
||||
return {
|
||||
id: row.id,
|
||||
organizationId: row.organizationId,
|
||||
title: row.title,
|
||||
content: row.content,
|
||||
audience: row.audience,
|
||||
status: row.status,
|
||||
publishedAt: optionalIso(row.publishedAt),
|
||||
createdByAccountId,
|
||||
createdByName: createdByAccountId ? accountNameById.get(createdByAccountId) : undefined,
|
||||
updatedByAccountId,
|
||||
updatedByName: updatedByAccountId ? accountNameById.get(updatedByAccountId) : undefined,
|
||||
readCount: readCountByNoticeId.get(row.id) ?? 0,
|
||||
hasRead: readNoticeIds.has(row.id),
|
||||
createdAt: iso(row.createdAt),
|
||||
updatedAt: iso(row.updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
export async function listNoticeCenterData(organizationId: string, accountId: string): Promise<NoticeCenterData> {
|
||||
const database = getDatabase();
|
||||
const noticeRows = await database.select().from(notices).where(eq(notices.organizationId, organizationId)).orderBy(desc(notices.updatedAt));
|
||||
const noticeIds = noticeRows.map((notice) => notice.id);
|
||||
const accountIds = Array.from(
|
||||
new Set(
|
||||
noticeRows
|
||||
.flatMap((notice) => [notice.createdByAccountId, notice.updatedByAccountId])
|
||||
.filter((item): item is string => typeof item === "string"),
|
||||
),
|
||||
);
|
||||
const [accountRows, readCountRows, readRows] = await Promise.all([
|
||||
accountIds.length > 0 ? database.select({ id: accounts.id, name: accounts.name }).from(accounts).where(inArray(accounts.id, accountIds)) : [],
|
||||
noticeIds.length > 0
|
||||
? database
|
||||
.select({ noticeId: noticeReadReceipts.noticeId, count: sql<number>`count(*)::int` })
|
||||
.from(noticeReadReceipts)
|
||||
.where(and(eq(noticeReadReceipts.organizationId, organizationId), inArray(noticeReadReceipts.noticeId, noticeIds)))
|
||||
.groupBy(noticeReadReceipts.noticeId)
|
||||
: ([] as ReadCountRow[]),
|
||||
noticeIds.length > 0
|
||||
? database
|
||||
.select({ noticeId: noticeReadReceipts.noticeId })
|
||||
.from(noticeReadReceipts)
|
||||
.where(
|
||||
and(
|
||||
eq(noticeReadReceipts.organizationId, organizationId),
|
||||
eq(noticeReadReceipts.accountId, accountId),
|
||||
inArray(noticeReadReceipts.noticeId, noticeIds),
|
||||
),
|
||||
)
|
||||
: [],
|
||||
]);
|
||||
const readCountByNoticeId = new Map(readCountRows.map((row) => [row.noticeId, Number(row.count)]));
|
||||
const readNoticeIds = new Set(readRows.map((row) => row.noticeId));
|
||||
const noticeData = noticeRows.map((notice) =>
|
||||
toNotice(
|
||||
notice,
|
||||
new Map(accountRows.map((account) => [account.id, account.name])),
|
||||
readCountByNoticeId,
|
||||
readNoticeIds,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
metrics: {
|
||||
published: noticeRows.filter((notice) => notice.status === "published").length,
|
||||
drafts: noticeRows.filter((notice) => notice.status === "draft").length,
|
||||
unread: noticeRows.filter((notice) => notice.status === "published" && !readNoticeIds.has(notice.id)).length,
|
||||
retracted: noticeRows.filter((notice) => notice.status === "retracted").length,
|
||||
},
|
||||
notices: noticeData,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createNotice(input: NoticeInput & { accountId: string; organizationId: string }): Promise<Notice | NoticeMutationFailure> {
|
||||
const database = getDatabase();
|
||||
const now = new Date();
|
||||
const rows = await database
|
||||
.insert(notices)
|
||||
.values({
|
||||
organizationId: input.organizationId,
|
||||
title: input.title,
|
||||
content: input.content,
|
||||
audience: input.audience,
|
||||
status: input.status,
|
||||
publishedAt: input.status === "published" ? now : undefined,
|
||||
createdByAccountId: input.accountId,
|
||||
updatedByAccountId: input.accountId,
|
||||
})
|
||||
.returning();
|
||||
const notice = rows[0];
|
||||
return notice ? toNotice(notice, new Map(), new Map(), new Set()) : { success: false, reason: "公告创建失败", status: 500 };
|
||||
}
|
||||
|
||||
export async function updateNotice(input: NoticeInput & { accountId: string; id: string; organizationId: string }): Promise<Notice | NoticeMutationFailure> {
|
||||
const database = getDatabase();
|
||||
const now = new Date();
|
||||
const rows = await database
|
||||
.update(notices)
|
||||
.set({
|
||||
title: input.title,
|
||||
content: input.content,
|
||||
audience: input.audience,
|
||||
status: input.status,
|
||||
publishedAt: input.status === "published" ? now : null,
|
||||
updatedByAccountId: input.accountId,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(and(eq(notices.id, input.id), eq(notices.organizationId, input.organizationId)))
|
||||
.returning();
|
||||
const notice = rows[0];
|
||||
return notice ? toNotice(notice, new Map(), new Map(), new Set()) : { success: false, reason: "公告不存在", status: 404 };
|
||||
}
|
||||
|
||||
export async function deleteNotice(input: { id: string; organizationId: string }): Promise<Notice | NoticeMutationFailure> {
|
||||
const database = getDatabase();
|
||||
const rows = await database.delete(notices).where(and(eq(notices.id, input.id), eq(notices.organizationId, input.organizationId))).returning();
|
||||
const notice = rows[0];
|
||||
return notice ? toNotice(notice, new Map(), new Map(), new Set()) : { success: false, reason: "公告不存在", status: 404 };
|
||||
}
|
||||
|
||||
export async function markNoticeRead(input: { accountId: string; id: string; organizationId: string }): Promise<Notice | NoticeMutationFailure> {
|
||||
const database = getDatabase();
|
||||
const rows = await database.select().from(notices).where(and(eq(notices.id, input.id), eq(notices.organizationId, input.organizationId)));
|
||||
const notice = rows[0];
|
||||
if (!notice) {
|
||||
return { success: false, reason: "公告不存在", status: 404 };
|
||||
}
|
||||
|
||||
await database
|
||||
.insert(noticeReadReceipts)
|
||||
.values({ organizationId: input.organizationId, noticeId: input.id, accountId: input.accountId })
|
||||
.onConflictDoUpdate({
|
||||
target: [noticeReadReceipts.noticeId, noticeReadReceipts.accountId],
|
||||
set: { readAt: new Date() },
|
||||
});
|
||||
return toNotice(notice, new Map(), new Map([[notice.id, 1]]), new Set([notice.id]));
|
||||
}
|
||||
Reference in New Issue
Block a user