Files
teatea-pension/modules/notices/server/operations.ts

181 lines
6.9 KiB
TypeScript

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]));
}