/** * trigger 存储 adapter —— Drizzle 只准出现在 adapter(编码规范 §5.3)。 */ import { eq, inArray, isNull, and } from 'drizzle-orm'; import type { TriggerConfig } from '@contracts'; import { triggerConfigSchema } from '@contracts'; import type { Db } from '@shared/db.ts'; import type { PendingFire } from '../../contract.ts'; import type { TriggerStore } from '../../port.ts'; import { triggerClaimTable, triggerPendingTable, triggerTable } from './trigger-schema.ts'; type Row = typeof triggerTable.$inferSelect; function toConfig(row: Row): TriggerConfig { return triggerConfigSchema.parse({ id: row.id, name: row.name, enabled: row.enabled, processPackageId: row.processPackageId, mapping: row.mapping, spec: row.spec, confirm: { required: row.confirmRequired, ...(row.confirmStaffId === null ? {} : { staffId: row.confirmStaffId }) }, }); } type PendingRow = typeof triggerPendingTable.$inferSelect; function toPending(row: PendingRow): PendingFire { return { id: row.id, triggerId: row.triggerId, packageId: row.packageId, staffId: row.staffId, subject: row.subject, data: row.data as Record, source: row.source as PendingFire['source'], preview: row.preview as Record, createdAt: row.createdAt.toISOString(), ...(row.consumedAt === null ? {} : { consumedAt: row.consumedAt.toISOString() }), ...(row.workflowId === null ? {} : { workflowId: row.workflowId }), }; } export function createTriggerStore(db: Db): TriggerStore { return { async list() { const rows = await db.select().from(triggerTable).orderBy(triggerTable.name); return rows.map(toConfig); }, async get(id) { const rows = await db.select().from(triggerTable).where(eq(triggerTable.id, id)).limit(1); return rows[0] === undefined ? undefined : toConfig(rows[0]); }, async upsert(config) { // 设计面的 upsert **不含 confirm 两列** —— 运营的勾只归 setConfirm 动 const values = { id: config.id, name: config.name, enabled: config.enabled, processPackageId: config.processPackageId, mapping: config.mapping, spec: config.spec, }; const [row] = await db .insert(triggerTable) .values(values) .onConflictDoUpdate({ target: triggerTable.id, set: values }) .returning(); if (row === undefined) throw new Error('触发器写入未返回行'); return toConfig(row); }, async setEnabled(id, enabled) { await db.update(triggerTable).set({ enabled }).where(eq(triggerTable.id, id)); }, async remove(id) { await db.delete(triggerTable).where(eq(triggerTable.id, id)); }, async claim(key) { const inserted = await db .insert(triggerClaimTable) .values({ key }) .onConflictDoNothing() .returning({ key: triggerClaimTable.key }); return inserted.length > 0; }, async lastFiredAt(id) { const rows = await db.select({ at: triggerTable.lastFiredAt }).from(triggerTable).where(eq(triggerTable.id, id)).limit(1); return rows[0]?.at ?? undefined; }, async markFired(id, at) { await db.update(triggerTable).set({ lastFiredAt: at }).where(eq(triggerTable.id, id)); }, async setConfirm(id, confirm) { await db .update(triggerTable) .set({ confirmRequired: confirm.required, confirmStaffId: confirm.staffId ?? null }) .where(eq(triggerTable.id, id)); }, async createPending(pending) { await db.insert(triggerPendingTable).values({ id: pending.id, triggerId: pending.triggerId, packageId: pending.packageId, staffId: pending.staffId, subject: pending.subject, data: pending.data, source: pending.source, preview: pending.preview, }); }, async getPending(id) { const rows = await db.select().from(triggerPendingTable).where(eq(triggerPendingTable.id, id)).limit(1); return rows[0] === undefined ? undefined : toPending(rows[0]); }, async pendingByIds(ids) { if (ids.length === 0) return []; const rows = await db.select().from(triggerPendingTable).where(inArray(triggerPendingTable.id, [...ids])); return rows.map(toPending); }, async claimPending(id, at) { // 只认 consumed_at 还空着的 —— 两人同时点确认,数据库替我们选出唯一赢家 const rows = await db .update(triggerPendingTable) .set({ consumedAt: at }) .where(and(eq(triggerPendingTable.id, id), isNull(triggerPendingTable.consumedAt))) .returning({ id: triggerPendingTable.id }); return rows.length > 0; }, async setPendingWorkflow(id, workflowId) { await db.update(triggerPendingTable).set({ workflowId }).where(eq(triggerPendingTable.id, id)); }, async releasePending(id) { await db.update(triggerPendingTable).set({ consumedAt: null }).where(eq(triggerPendingTable.id, id)); }, }; }