/** * process 存储 adapter —— Drizzle 只准出现在 adapter(编码规范 §5.3)。 */ import { and, asc, desc, eq, isNull } from 'drizzle-orm'; import type { ProcessPackage, TerminalState, WorkflowState } from '@contracts'; import { approvalTemplateSchema, processPackageSchema } from '@contracts'; import type { Db } from '@shared/db.ts'; import type { ProcessStore, WorkflowRecord } from '../../port.ts'; import type { NodeInstance } from '../domain/owner-resolution.ts'; import { approvalTemplateTable, nodeInstanceTable, processPackageTable, workflowTable, nodeOwnerTable } from './process-schema.ts'; type WorkflowRow = typeof workflowTable.$inferSelect; function toWorkflow(row: WorkflowRow): WorkflowRecord { return { id: row.id, packageId: row.packageId, packageVersion: row.packageVersion, subject: row.subject, state: row.state as WorkflowState, source: row.source as WorkflowRecord['source'], initiation: row.initiation as Record, ...(row.workGroup === null ? {} : { workGroup: row.workGroup as Record }), ...(row.assignments === null ? {} : { assignments: row.assignments as Record }), ports: row.ports as Record>, vars: row.vars as Record, startedAt: row.startedAt.toISOString(), ...(row.finishedAt === null ? {} : { finishedAt: row.finishedAt.toISOString() }), }; } export function createProcessStore(db: Db): ProcessStore { return { /* * **只插不改**:版本一旦落库就是不可变的历史。 * 版本号由用例层算(恒新增),所以这里不该撞键;真撞了就让唯一约束报出来 —— * 静默覆盖会改掉在途任务正在读的定义,那是最不该无声发生的事。 */ async savePackage(pkg: ProcessPackage) { await db .insert(processPackageTable) .values({ id: pkg.id, version: pkg.version, name: pkg.name, state: pkg.state, definition: pkg }); }, async setPackageState(id, version, state) { const rows = await db .select() .from(processPackageTable) .where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version))) .limit(1); const row = rows[0]; if (row === undefined) return; // state 在列上与 definition(jsonb)里各有一份 —— 读包走 definition,列供查询;两处必须一起改 const definition = { ...(row.definition as Record), state }; await db .update(processPackageTable) .set({ state, definition }) .where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version))); }, async deletePackage(id, version) { await db .delete(processPackageTable) .where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version))); }, async renameVersion(id, version, name) { // 名字在列上与 definition(jsonb)里各有一份 —— 与 setPackageState 同一口径,两处一起改 const rows = await db .select() .from(processPackageTable) .where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version))); const row = rows[0]; if (row === undefined) return; // 用例层已挡「版本不存在」,这里不静默造行 const definition = { ...(row.definition as Record), name }; await db .update(processPackageTable) .set({ name, definition }) .where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version))); }, async renumberVersion(id, from, to) { const rows = await db .select() .from(processPackageTable) .where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, from))); const row = rows[0]; if (row === undefined) return; // 用例层已挡「版本不存在」 const definition = { ...(row.definition as Record), version: to }; await db .update(processPackageTable) .set({ version: to, definition }) .where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, from))); // 跑过的单跟着改指 —— 同一份定义,只是名牌换了号;不搬引用才会让留痕指错 await db .update(workflowTable) .set({ packageVersion: to }) .where(and(eq(workflowTable.packageId, id), eq(workflowTable.packageVersion, from))); }, async listApprovalTemplates() { const rows = await db.select().from(approvalTemplateTable).orderBy(asc(approvalTemplateTable.name)); return rows.map((r) => approvalTemplateSchema.parse({ id: r.id, name: r.name, basis: r.basis, ...(r.nudge === null ? {} : { nudge: r.nudge }), ...(r.message === null ? {} : { message: r.message }), })); }, async upsertApprovalTemplate(template) { const values = { id: template.id, name: template.name, basis: template.basis, nudge: template.nudge ?? null, message: template.message ?? null, updatedAt: new Date(), }; await db.insert(approvalTemplateTable).values(values).onConflictDoUpdate({ target: approvalTemplateTable.id, set: { name: values.name, basis: values.basis, nudge: values.nudge, message: values.message, updatedAt: values.updatedAt }, }); }, async deleteApprovalTemplate(id) { await db.delete(approvalTemplateTable).where(eq(approvalTemplateTable.id, id)); }, async getPackage(id, version) { const conditions = [eq(processPackageTable.id, id)]; if (version !== undefined) conditions.push(eq(processPackageTable.version, version)); const rows = await db .select() .from(processPackageTable) .where(and(...conditions)) .orderBy(desc(processPackageTable.version)) .limit(1); return rows[0] === undefined ? undefined : processPackageSchema.parse(rows[0].definition); }, async listPackages() { const rows = await db.select().from(processPackageTable).orderBy(asc(processPackageTable.name)); return rows.map((r) => processPackageSchema.parse(r.definition)); }, async updatePackageDefinition(id, version, pkg) { const hit = await db .update(processPackageTable) .set({ definition: pkg }) .where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version))) .returning({ version: processPackageTable.version }); if (hit.length === 0) throw new Error(`就地写回失败:流程包版本不存在 ${id}@${version}`); }, async countPinnedInFlight(id, version) { const rows = await db .select({ id: workflowTable.id }) .from(workflowTable) .where( and( eq(workflowTable.packageId, id), eq(workflowTable.packageVersion, version), isNull(workflowTable.finishedAt), ), ); return rows.length; }, async insertWorkflow(record) { await db.insert(workflowTable).values({ id: record.id, packageId: record.packageId, packageVersion: record.packageVersion, subject: record.subject, state: record.state, source: record.source, initiation: record.initiation, workGroup: record.workGroup ?? null, assignments: record.assignments ?? null, ports: record.ports, vars: record.vars, startedAt: new Date(record.startedAt), }); }, async getWorkflow(id) { const rows = await db.select().from(workflowTable).where(eq(workflowTable.id, id)).limit(1); return rows[0] === undefined ? undefined : toWorkflow(rows[0]); }, async listWorkflows(limit) { const rows = await db.select().from(workflowTable).orderBy(desc(workflowTable.startedAt)).limit(limit); return rows.map(toWorkflow); }, async updateWorkflow(id, patch) { const values: Record = {}; if (patch.state !== undefined) values['state'] = patch.state; if (patch.ports !== undefined) values['ports'] = patch.ports; if (patch.vars !== undefined) values['vars'] = patch.vars; if (patch.finishedAt !== undefined) values['finishedAt'] = new Date(patch.finishedAt); const [row] = await db.update(workflowTable).set(values).where(eq(workflowTable.id, id)).returning(); if (row === undefined) throw new Error(`工作流不存在:${id}`); return toWorkflow(row); }, async listInstances(workflowId) { const rows = await db .select() .from(nodeInstanceTable) .where(eq(nodeInstanceTable.workflowId, workflowId)) .orderBy(asc(nodeInstanceTable.createdAt)); return rows.map( (r): NodeInstance => ({ nodeId: r.nodeId, instance: r.instance, running: r.running, waiting: r.waiting, activated: r.activated, ...(r.terminal === null ? {} : { terminal: r.terminal as TerminalState }), ...(r.failureDigested === null ? {} : { failureDigested: r.failureDigested }), ...(r.taskId === null ? {} : { taskId: r.taskId }), ...(r.approvalId === null ? {} : { approvalId: r.approvalId }), // 派给谁:审批箱收件、员工面链条、治理列表全按它认人 —— 写了不读回来等于没写 ...(r.assigneeId === null ? {} : { assigneeId: r.assigneeId }), }), ); }, async insertInstance(workflowId, instance) { await db.insert(nodeInstanceTable).values({ workflowId, nodeId: instance.nodeId, instance: instance.instance, running: instance.running, waiting: instance.waiting, activated: instance.activated, terminal: instance.terminal ?? null, failureDigested: instance.failureDigested ?? null, taskId: instance.taskId ?? null, approvalId: instance.approvalId ?? null, assigneeId: instance.assigneeId ?? null, }); }, async updateInstance(workflowId, nodeId, instance, patch) { const values: Record = {}; if (patch.running !== undefined) values['running'] = patch.running; if (patch.waiting !== undefined) values['waiting'] = patch.waiting; if (patch.terminal !== undefined) values['terminal'] = patch.terminal; if (patch.failureDigested !== undefined) values['failureDigested'] = patch.failureDigested; if (patch.taskId !== undefined) values['taskId'] = patch.taskId; if (Object.keys(values).length === 0) return; await db .update(nodeInstanceTable) .set(values) .where( and( eq(nodeInstanceTable.workflowId, workflowId), eq(nodeInstanceTable.nodeId, nodeId), eq(nodeInstanceTable.instance, instance), ), ); }, async nodeOwners(packageId) { const rows = await db.select().from(nodeOwnerTable).where(eq(nodeOwnerTable.packageId, packageId)); return Object.fromEntries(rows.map((r) => [r.nodeId, r.staffId])); }, async setNodeOwner(packageId, nodeId, staffId) { // 一节点至多一位运营负责人:先清后写;undefined = 撤掉,回到轮值 await db.delete(nodeOwnerTable).where(and(eq(nodeOwnerTable.packageId, packageId), eq(nodeOwnerTable.nodeId, nodeId))); if (staffId !== undefined) await db.insert(nodeOwnerTable).values({ packageId, nodeId, staffId }); }, async findInstanceByTask(taskId) { const rows = await db .select() .from(nodeInstanceTable) .where(eq(nodeInstanceTable.taskId, taskId)) .limit(1); const row = rows[0]; return row === undefined ? undefined : { workflowId: row.workflowId, nodeId: row.nodeId, instance: row.instance }; }, }; }