/** minute 的持久化(编码规范 §10.2:框架只许出现在 adapter)。 */ import { asc, desc, eq, inArray } from 'drizzle-orm'; import type { Db } from '@shared/db.ts'; import type { Minute, MinuteResolution, ResolutionState } from '../domain/minute.ts'; import type { MinuteStore } from '../../port.ts'; import { minuteResolutions, minutes } from './minute-schema.ts'; type MinuteRow = typeof minutes.$inferSelect; type ResolutionRow = typeof minuteResolutions.$inferSelect; const toResolution = (row: ResolutionRow): MinuteResolution => ({ id: row.id, text: row.text, ownerId: row.ownerId ?? undefined, state: row.state as ResolutionState, workflowId: row.workflowId ?? undefined, registeredById: row.registeredById ?? undefined, registeredAt: row.registeredAt?.toISOString(), }); const toMinute = (row: MinuteRow, resolutions: readonly ResolutionRow[]): Minute => ({ id: row.id, title: row.title, body: row.body, authorId: row.authorId, participants: row.participants as string[], occurredOn: row.occurredOn, resolutions: resolutions.map(toResolution), createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), }); export function createMinuteStore(db: Db): MinuteStore { async function loadResolutions(minuteIds: readonly string[]): Promise> { if (minuteIds.length === 0) return new Map(); const rows = await db .select() .from(minuteResolutions) .where(inArray(minuteResolutions.minuteId, [...minuteIds])) .orderBy(asc(minuteResolutions.id)); const byMinute = new Map(); for (const row of rows) { const list = byMinute.get(row.minuteId) ?? []; list.push(row); byMinute.set(row.minuteId, list); } return byMinute; } return { async list(limit) { const rows = await db.select().from(minutes).orderBy(desc(minutes.occurredOn)).limit(limit); const byMinute = await loadResolutions(rows.map((r) => r.id)); return rows.map((row) => toMinute(row, byMinute.get(row.id) ?? [])); }, async get(minuteId) { const [row] = await db.select().from(minutes).where(eq(minutes.id, minuteId)).limit(1); if (row === undefined) return undefined; const byMinute = await loadResolutions([minuteId]); return toMinute(row, byMinute.get(minuteId) ?? []); }, async save(minute) { // 纪要与其决议同事务落笔:决议的登记状态不得与纪要正文脱节 await db.transaction(async (tx) => { await tx .insert(minutes) .values({ id: minute.id, title: minute.title, body: minute.body, authorId: minute.authorId, participants: [...minute.participants], occurredOn: minute.occurredOn, createdAt: new Date(minute.createdAt), updatedAt: new Date(minute.updatedAt), }) .onConflictDoUpdate({ target: minutes.id, set: { title: minute.title, body: minute.body, updatedAt: new Date(minute.updatedAt) }, }); for (const r of minute.resolutions) { await tx .insert(minuteResolutions) .values({ id: r.id, minuteId: minute.id, text: r.text, ownerId: r.ownerId ?? null, state: r.state, workflowId: r.workflowId ?? null, registeredById: r.registeredById ?? null, registeredAt: r.registeredAt === undefined ? null : new Date(r.registeredAt), }) .onConflictDoUpdate({ target: minuteResolutions.id, set: { state: r.state, workflowId: r.workflowId ?? null, registeredById: r.registeredById ?? null, registeredAt: r.registeredAt === undefined ? null : new Date(r.registeredAt), }, }); } }); }, }; }