@mldong/jeeflow 1.6.1 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.js CHANGED
@@ -124,16 +124,9 @@ export class EngineImpl {
124
124
  return (await this.repo.findInstanceById(inst.id));
125
125
  }
126
126
  for (const node of followEdges(flow, curNode.id)) {
127
- if (node.type === TypeEnd) {
128
- // 聚合根:流程完成
129
- inst.finish(new Date());
130
- inst.variables = vars;
131
- await this.repo.updateInstance(inst);
132
- await this.fireEvent({ type: EventType.ProcessFinish, instanceId: inst.id, operator });
133
- }
134
- else {
135
- await this.executeNode(flow, inst, node, operator, vars);
136
- }
127
+ // 统一走 executeNode:结束节点也经节点执行链(拦截器/事件完整触发),
128
+ // executeNode 内部 TypeEnd 分支完成聚合根 finish + 事件发布
129
+ await this.executeNode(flow, inst, node, operator, vars);
137
130
  }
138
131
  }
139
132
  return (await this.repo.findInstanceById(inst.id));
package/dist/index.d.ts CHANGED
@@ -4,4 +4,5 @@ export { HandlerRegistry, type IAssignmentHandler, type IDecisionHandler, type H
4
4
  export { enumDict, enumDictKeys, type DictItem } from './metadata.js';
5
5
  export * from './model.js';
6
6
  export type { ProcessRepository, UserProvider, OrgUserProvider, IDGenerator, ExpressionEvaluator } from './spi.js';
7
+ export { SqliteDynamicTableWriter, PersistPostInterceptor, type DynamicTableWriter, type DefineLoader, } from './persist.js';
7
8
  export { registerBuiltinAssignments, OperatorAssignmentHandler, FormFieldAssigneeHandler, DeptLeaderAssignmentHandler, DeptMainLeaderAssignmentHandler, ApplicantDeptLeaderAssignmentHandler, ApplicantDeptMainLeaderAssignmentHandler, TaskRoleAssigneeHandler, HANDLER_OPERATOR_ASSIGNMENT, HANDLER_FORM_FIELD_ASSIGNEE, HANDLER_DEPT_LEADER, HANDLER_DEPT_MAIN_LEADER, HANDLER_APPLICANT_DEPT_LEADER, HANDLER_APPLICANT_DEPT_MAIN_LEADER, HANDLER_TASK_ROLE_ASSIGNEE, } from './builtin.js';
package/dist/index.js CHANGED
@@ -3,4 +3,5 @@ export { MemoryRepository } from './memory.js';
3
3
  export { HandlerRegistry } from './registry.js';
4
4
  export { enumDict, enumDictKeys } from './metadata.js';
5
5
  export * from './model.js';
6
+ export { SqliteDynamicTableWriter, PersistPostInterceptor, } from './persist.js';
6
7
  export { registerBuiltinAssignments, OperatorAssignmentHandler, FormFieldAssigneeHandler, DeptLeaderAssignmentHandler, DeptMainLeaderAssignmentHandler, ApplicantDeptLeaderAssignmentHandler, ApplicantDeptMainLeaderAssignmentHandler, TaskRoleAssigneeHandler, HANDLER_OPERATOR_ASSIGNMENT, HANDLER_FORM_FIELD_ASSIGNEE, HANDLER_DEPT_LEADER, HANDLER_DEPT_MAIN_LEADER, HANDLER_APPLICANT_DEPT_LEADER, HANDLER_APPLICANT_DEPT_MAIN_LEADER, HANDLER_TASK_ROLE_ASSIGNEE, } from './builtin.js';
@@ -0,0 +1,72 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { type ProcessInstance, type ProcessDefine, type FlowNode } from './model.js';
3
+ import type { FlowInterceptor } from './extensions.js';
4
+ /**
5
+ * 动态表写入组件接口——引擎无关,四语言契约一致(Java/Go/Python/Node)
6
+ *
7
+ * 用法:给「表名 + 字段 Map」安全写入任意业务表(列过滤 / 参数化 INSERT /
8
+ * 幂等 / 系统字段)。不依赖工作流引擎。
9
+ */
10
+ export interface DynamicTableWriter {
11
+ /** 按目标表过滤列(表结构探测),返回表内实际存在的列 */
12
+ filterColumns(tableName: string, columns: string[]): string[] | Promise<string[]>;
13
+ /** 参数化 INSERT(按列过滤结果落库),返回生成主键 */
14
+ insert(tableName: string, data: Record<string, unknown>): unknown | Promise<unknown>;
15
+ /** 幂等检查:指定业务键(如 process_instance_id)是否已存在 */
16
+ exists(tableName: string, bizKey: string, bizKeyValue: unknown): boolean | Promise<boolean>;
17
+ /** 按配置列名填充系统字段(未配置的列跳过) */
18
+ fillSystemFields(data: Record<string, unknown>, isInsert: boolean): void;
19
+ }
20
+ /**
21
+ * 动态表写入器默认实现(node:sqlite DatabaseSync)。
22
+ *
23
+ * 注意:node:sqlite 是实验特性(Node 22.5+,无需外部依赖)。
24
+ * MySQL/PG 集成方可自行实现 DynamicTableWriter 接口(契约见上)。
25
+ */
26
+ export declare class SqliteDynamicTableWriter implements DynamicTableWriter {
27
+ private db;
28
+ private cache;
29
+ /** 系统字段列名(null/undefined 禁用) */
30
+ createTimeColumn?: string | null;
31
+ createUserColumn?: string | null;
32
+ updateTimeColumn?: string | null;
33
+ updateUserColumn?: string | null;
34
+ isDeletedColumn?: string | null;
35
+ /** 用户列默认值(issues/19):优先取 data 中已注入的 apply_user_id=流程 operator,
36
+ * 否则用此配置值,缺省 "system"——多数框架业务表 create_user/update_user 为 BIGINT 存 userId */
37
+ defaultUserValue: unknown;
38
+ constructor(db: DatabaseSync);
39
+ private tableColumns;
40
+ filterColumns(tableName: string, columns: string[]): string[];
41
+ insert(tableName: string, data: Record<string, unknown>): unknown;
42
+ exists(tableName: string, bizKey: string, bizKeyValue: unknown): boolean;
43
+ fillSystemFields(data: Record<string, unknown>, isInsert: boolean): void;
44
+ /** 默认用户值(issues/19):优先取 data 中已注入的 apply_user_id
45
+ * (拦截器场景 = 流程 operator,BIGINT 用户列表开箱即用),否则回落配置默认值 */
46
+ private resolveDefaultUser;
47
+ }
48
+ /** 流程定义加载器(用于解析 relTableName / 流程 name),通常透传仓库 findDefineById */
49
+ export type DefineLoader = (defineId: number) => Promise<ProcessDefine | null>;
50
+ /**
51
+ * 工作流业务数据入库适配拦截器——流程结束同意后,f_ 表单数据写入业务表。
52
+ *
53
+ * 语义(spec 契约,四语言一致):
54
+ * - 时机:结束节点执行后 + 实例 Done + submitType=AGREE(不同意/退回不入库)
55
+ * - 字段:实例 Variables 中 f_ 前缀字段,去前缀
56
+ * - 表名:流程定义 content 顶层 relTableName,缺省回落流程 name
57
+ * - 系统字段:writer 通用字段 + 流程上下文(process_instance_id / apply_user_id /
58
+ * apply_dept_id,蛇形列名约定)
59
+ * - 幂等:bizKey = process_instance_id(先查后插,跨请求有效)
60
+ * - 静默跳过:非结束节点 / 非同意 / 未配置表名 / writer 未注入
61
+ * - 表不存在 = 配置错误 → 显性抛错(快速失败)
62
+ */
63
+ export declare class PersistPostInterceptor implements FlowInterceptor {
64
+ private writer;
65
+ private loader;
66
+ order: number;
67
+ fieldPrefix: string;
68
+ constructor(writer: DynamicTableWriter | null, loader: DefineLoader | null);
69
+ preHandle(_node: FlowNode, _inst: ProcessInstance): Promise<boolean>;
70
+ postHandle(node: FlowNode, inst: ProcessInstance): Promise<void>;
71
+ private resolveTableName;
72
+ }
@@ -0,0 +1,197 @@
1
+ import { KeySubmitType, KeyDeptID } from './engine.js';
2
+ import { TypeEnd, InstanceState, SubmitType } from './model.js';
3
+ // ─── 默认实现:node:sqlite(内置零依赖) ──────────────────────────────────────
4
+ const TABLE_NAME_RE = /^[A-Za-z0-9_]+$/;
5
+ function nowText() {
6
+ const d = new Date();
7
+ const p = (n) => String(n).padStart(2, '0');
8
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
9
+ }
10
+ /** 表名安全校验:非空、合法字符、拒绝 sys_ 前缀 */
11
+ function checkTableName(tableName) {
12
+ if (!tableName)
13
+ throw new Error('persist: table name is empty');
14
+ if (tableName.startsWith('sys_'))
15
+ throw new Error(`persist: table ${tableName} with sys_ prefix is not allowed`);
16
+ if (!TABLE_NAME_RE.test(tableName))
17
+ throw new Error(`persist: table ${tableName} contains illegal characters`);
18
+ }
19
+ /**
20
+ * 动态表写入器默认实现(node:sqlite DatabaseSync)。
21
+ *
22
+ * 注意:node:sqlite 是实验特性(Node 22.5+,无需外部依赖)。
23
+ * MySQL/PG 集成方可自行实现 DynamicTableWriter 接口(契约见上)。
24
+ */
25
+ export class SqliteDynamicTableWriter {
26
+ db;
27
+ cache = new Map();
28
+ /** 系统字段列名(null/undefined 禁用) */
29
+ createTimeColumn = 'create_time';
30
+ createUserColumn = 'create_user';
31
+ updateTimeColumn = 'update_time';
32
+ updateUserColumn = 'update_user';
33
+ isDeletedColumn = 'is_deleted';
34
+ /** 用户列默认值(issues/19):优先取 data 中已注入的 apply_user_id=流程 operator,
35
+ * 否则用此配置值,缺省 "system"——多数框架业务表 create_user/update_user 为 BIGINT 存 userId */
36
+ defaultUserValue = 'system';
37
+ constructor(db) {
38
+ this.db = db;
39
+ }
40
+ tableColumns(tableName) {
41
+ const cached = this.cache.get(tableName);
42
+ if (cached)
43
+ return cached;
44
+ // PRAGMA 不支持占位符——表名已过安全校验
45
+ const rows = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
46
+ if (rows.length === 0)
47
+ throw new Error(`persist: table ${tableName} not found`);
48
+ const cols = rows.map(r => r.name.toUpperCase());
49
+ this.cache.set(tableName, cols);
50
+ return cols;
51
+ }
52
+ filterColumns(tableName, columns) {
53
+ checkTableName(tableName);
54
+ const set = new Set(this.tableColumns(tableName));
55
+ return columns.filter(c => set.has(c.toUpperCase()));
56
+ }
57
+ insert(tableName, data) {
58
+ checkTableName(tableName);
59
+ const cols = this.tableColumns(tableName);
60
+ const names = [];
61
+ const values = [];
62
+ // 保持插入顺序稳定(对象键无序,按表列顺序取)
63
+ for (const col of cols) {
64
+ if (Object.prototype.hasOwnProperty.call(data, col)) {
65
+ names.push(col);
66
+ values.push(data[col]);
67
+ }
68
+ else {
69
+ const key = Object.keys(data).find(k => k.toUpperCase() === col);
70
+ if (key !== undefined) {
71
+ names.push(col);
72
+ values.push(data[key]);
73
+ }
74
+ }
75
+ }
76
+ if (names.length === 0)
77
+ throw new Error(`persist: no matching columns for ${tableName}`);
78
+ const placeholders = names.map(() => '?').join(',');
79
+ const stmt = this.db.prepare(`INSERT INTO ${tableName} (${names.join(',')}) VALUES (${placeholders})`);
80
+ const res = stmt.run(...values);
81
+ return res.lastInsertRowid;
82
+ }
83
+ exists(tableName, bizKey, bizKeyValue) {
84
+ checkTableName(tableName);
85
+ this.tableColumns(tableName); // 表不存在提前报错
86
+ const row = this.db.prepare(`SELECT COUNT(1) AS c FROM ${tableName} WHERE ${bizKey} = ?`).get(bizKeyValue);
87
+ return Number(row?.c ?? 0) > 0;
88
+ }
89
+ fillSystemFields(data, isInsert) {
90
+ const now = nowText();
91
+ if (isInsert) {
92
+ if (this.createTimeColumn)
93
+ data[this.createTimeColumn] ??= now;
94
+ if (this.createUserColumn)
95
+ data[this.createUserColumn] ??= this.resolveDefaultUser(data);
96
+ if (this.updateTimeColumn)
97
+ data[this.updateTimeColumn] ??= now;
98
+ if (this.updateUserColumn)
99
+ data[this.updateUserColumn] ??= this.resolveDefaultUser(data);
100
+ if (this.isDeletedColumn)
101
+ data[this.isDeletedColumn] ??= 0;
102
+ }
103
+ else {
104
+ if (this.updateTimeColumn)
105
+ data[this.updateTimeColumn] = now;
106
+ if (this.updateUserColumn)
107
+ data[this.updateUserColumn] ??= this.resolveDefaultUser(data);
108
+ }
109
+ }
110
+ /** 默认用户值(issues/19):优先取 data 中已注入的 apply_user_id
111
+ * (拦截器场景 = 流程 operator,BIGINT 用户列表开箱即用),否则回落配置默认值 */
112
+ resolveDefaultUser(data) {
113
+ return data['apply_user_id'] ?? this.defaultUserValue;
114
+ }
115
+ }
116
+ /**
117
+ * 工作流业务数据入库适配拦截器——流程结束同意后,f_ 表单数据写入业务表。
118
+ *
119
+ * 语义(spec 契约,四语言一致):
120
+ * - 时机:结束节点执行后 + 实例 Done + submitType=AGREE(不同意/退回不入库)
121
+ * - 字段:实例 Variables 中 f_ 前缀字段,去前缀
122
+ * - 表名:流程定义 content 顶层 relTableName,缺省回落流程 name
123
+ * - 系统字段:writer 通用字段 + 流程上下文(process_instance_id / apply_user_id /
124
+ * apply_dept_id,蛇形列名约定)
125
+ * - 幂等:bizKey = process_instance_id(先查后插,跨请求有效)
126
+ * - 静默跳过:非结束节点 / 非同意 / 未配置表名 / writer 未注入
127
+ * - 表不存在 = 配置错误 → 显性抛错(快速失败)
128
+ */
129
+ export class PersistPostInterceptor {
130
+ writer;
131
+ loader;
132
+ order = 0;
133
+ fieldPrefix = 'f_';
134
+ constructor(writer, loader) {
135
+ this.writer = writer;
136
+ this.loader = loader;
137
+ }
138
+ async preHandle(_node, _inst) {
139
+ return true;
140
+ }
141
+ async postHandle(node, inst) {
142
+ if (!this.writer || !this.loader)
143
+ return; // 未注入:静默跳过
144
+ // 时机:仅结束节点 + 流程正常完成(Done)+ 同意
145
+ if (!node || node.type !== TypeEnd)
146
+ return;
147
+ if (!inst || inst.state !== InstanceState.Done)
148
+ return;
149
+ const submitType = Number(inst.variables[KeySubmitType]);
150
+ if (submitType !== SubmitType.Agree)
151
+ return;
152
+ // 同链重复触发防护(issues/19):最后任务节点与结束节点都会触发后置拦截器,
153
+ // 同一执行链(共享 inst.variables)只插一次。标记写入时实例已完成持久化
154
+ // (引擎 executeNode 先 updateInstance 后触发拦截器,repo 存副本)不会落库;
155
+ // exists 保留作为跨请求/重启的幂等兜底(先查后插语义不变)。
156
+ const chainKey = `__persist_executed_${inst.id}`;
157
+ if (inst.variables[chainKey] === true)
158
+ return;
159
+ inst.variables[chainKey] = true;
160
+ // 表名:流程定义顶层 relTableName,缺省回落流程 name
161
+ const tableName = await this.resolveTableName(inst);
162
+ if (!tableName)
163
+ return; // 未配置:静默跳过
164
+ // 幂等:以 process_instance_id 为键,先查后插
165
+ if (await this.writer.exists(tableName, 'process_instance_id', inst.id))
166
+ return;
167
+ // 提取 f_ 前缀字段(去前缀)
168
+ const prefix = this.fieldPrefix || 'f_';
169
+ const data = {};
170
+ for (const [k, v] of Object.entries(inst.variables)) {
171
+ if (k.startsWith(prefix) && k.length > prefix.length)
172
+ data[k.slice(prefix.length)] = v;
173
+ }
174
+ // 流程上下文字段(蛇形列名约定,与 writer 系统字段一致)
175
+ data['process_instance_id'] ??= inst.id;
176
+ data['apply_user_id'] ??= inst.operator;
177
+ data['apply_dept_id'] ??= inst.variables[KeyDeptID] ?? null;
178
+ // 通用系统字段(writer 按配置列填充)
179
+ this.writer.fillSystemFields(data, true);
180
+ await this.writer.insert(tableName, data);
181
+ }
182
+ async resolveTableName(inst) {
183
+ const define = await this.loader(inst.defineId);
184
+ if (!define)
185
+ return '';
186
+ const content = typeof define.content === 'string' ? define.content : new TextDecoder().decode(define.content);
187
+ let meta;
188
+ try {
189
+ meta = JSON.parse(content);
190
+ }
191
+ catch {
192
+ return '';
193
+ }
194
+ const tableName = (meta.relTableName ?? '').trim();
195
+ return tableName || (meta.name ?? '').trim(); // 缺省回落流程 name
196
+ }
197
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mldong/jeeflow",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "description": "jeeflow workflow engine — Node.js / TypeScript implementation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",