@mldong/jeeflow 1.6.0 → 1.6.2

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,66 @@
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
+ constructor(db: DatabaseSync);
36
+ private tableColumns;
37
+ filterColumns(tableName: string, columns: string[]): string[];
38
+ insert(tableName: string, data: Record<string, unknown>): unknown;
39
+ exists(tableName: string, bizKey: string, bizKeyValue: unknown): boolean;
40
+ fillSystemFields(data: Record<string, unknown>, isInsert: boolean): void;
41
+ }
42
+ /** 流程定义加载器(用于解析 relTableName / 流程 name),通常透传仓库 findDefineById */
43
+ export type DefineLoader = (defineId: number) => Promise<ProcessDefine | null>;
44
+ /**
45
+ * 工作流业务数据入库适配拦截器——流程结束同意后,f_ 表单数据写入业务表。
46
+ *
47
+ * 语义(spec 契约,四语言一致):
48
+ * - 时机:结束节点执行后 + 实例 Done + submitType=AGREE(不同意/退回不入库)
49
+ * - 字段:实例 Variables 中 f_ 前缀字段,去前缀
50
+ * - 表名:流程定义 content 顶层 relTableName,缺省回落流程 name
51
+ * - 系统字段:writer 通用字段 + 流程上下文(process_instance_id / apply_user_id /
52
+ * apply_dept_id,蛇形列名约定)
53
+ * - 幂等:bizKey = process_instance_id(先查后插,跨请求有效)
54
+ * - 静默跳过:非结束节点 / 非同意 / 未配置表名 / writer 未注入
55
+ * - 表不存在 = 配置错误 → 显性抛错(快速失败)
56
+ */
57
+ export declare class PersistPostInterceptor implements FlowInterceptor {
58
+ private writer;
59
+ private loader;
60
+ order: number;
61
+ fieldPrefix: string;
62
+ constructor(writer: DynamicTableWriter | null, loader: DefineLoader | null);
63
+ preHandle(_node: FlowNode, _inst: ProcessInstance): Promise<boolean>;
64
+ postHandle(node: FlowNode, inst: ProcessInstance): Promise<void>;
65
+ private resolveTableName;
66
+ }
@@ -0,0 +1,181 @@
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
+ constructor(db) {
35
+ this.db = db;
36
+ }
37
+ tableColumns(tableName) {
38
+ const cached = this.cache.get(tableName);
39
+ if (cached)
40
+ return cached;
41
+ // PRAGMA 不支持占位符——表名已过安全校验
42
+ const rows = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
43
+ if (rows.length === 0)
44
+ throw new Error(`persist: table ${tableName} not found`);
45
+ const cols = rows.map(r => r.name.toUpperCase());
46
+ this.cache.set(tableName, cols);
47
+ return cols;
48
+ }
49
+ filterColumns(tableName, columns) {
50
+ checkTableName(tableName);
51
+ const set = new Set(this.tableColumns(tableName));
52
+ return columns.filter(c => set.has(c.toUpperCase()));
53
+ }
54
+ insert(tableName, data) {
55
+ checkTableName(tableName);
56
+ const cols = this.tableColumns(tableName);
57
+ const names = [];
58
+ const values = [];
59
+ // 保持插入顺序稳定(对象键无序,按表列顺序取)
60
+ for (const col of cols) {
61
+ if (Object.prototype.hasOwnProperty.call(data, col)) {
62
+ names.push(col);
63
+ values.push(data[col]);
64
+ }
65
+ else {
66
+ const key = Object.keys(data).find(k => k.toUpperCase() === col);
67
+ if (key !== undefined) {
68
+ names.push(col);
69
+ values.push(data[key]);
70
+ }
71
+ }
72
+ }
73
+ if (names.length === 0)
74
+ throw new Error(`persist: no matching columns for ${tableName}`);
75
+ const placeholders = names.map(() => '?').join(',');
76
+ const stmt = this.db.prepare(`INSERT INTO ${tableName} (${names.join(',')}) VALUES (${placeholders})`);
77
+ const res = stmt.run(...values);
78
+ return res.lastInsertRowid;
79
+ }
80
+ exists(tableName, bizKey, bizKeyValue) {
81
+ checkTableName(tableName);
82
+ this.tableColumns(tableName); // 表不存在提前报错
83
+ const row = this.db.prepare(`SELECT COUNT(1) AS c FROM ${tableName} WHERE ${bizKey} = ?`).get(bizKeyValue);
84
+ return Number(row?.c ?? 0) > 0;
85
+ }
86
+ fillSystemFields(data, isInsert) {
87
+ const now = nowText();
88
+ if (isInsert) {
89
+ if (this.createTimeColumn)
90
+ data[this.createTimeColumn] ??= now;
91
+ if (this.createUserColumn)
92
+ data[this.createUserColumn] ??= 'system';
93
+ if (this.updateTimeColumn)
94
+ data[this.updateTimeColumn] ??= now;
95
+ if (this.updateUserColumn)
96
+ data[this.updateUserColumn] ??= 'system';
97
+ if (this.isDeletedColumn)
98
+ data[this.isDeletedColumn] ??= 0;
99
+ }
100
+ else {
101
+ if (this.updateTimeColumn)
102
+ data[this.updateTimeColumn] = now;
103
+ if (this.updateUserColumn)
104
+ data[this.updateUserColumn] ??= 'system';
105
+ }
106
+ }
107
+ }
108
+ /**
109
+ * 工作流业务数据入库适配拦截器——流程结束同意后,f_ 表单数据写入业务表。
110
+ *
111
+ * 语义(spec 契约,四语言一致):
112
+ * - 时机:结束节点执行后 + 实例 Done + submitType=AGREE(不同意/退回不入库)
113
+ * - 字段:实例 Variables 中 f_ 前缀字段,去前缀
114
+ * - 表名:流程定义 content 顶层 relTableName,缺省回落流程 name
115
+ * - 系统字段:writer 通用字段 + 流程上下文(process_instance_id / apply_user_id /
116
+ * apply_dept_id,蛇形列名约定)
117
+ * - 幂等:bizKey = process_instance_id(先查后插,跨请求有效)
118
+ * - 静默跳过:非结束节点 / 非同意 / 未配置表名 / writer 未注入
119
+ * - 表不存在 = 配置错误 → 显性抛错(快速失败)
120
+ */
121
+ export class PersistPostInterceptor {
122
+ writer;
123
+ loader;
124
+ order = 0;
125
+ fieldPrefix = 'f_';
126
+ constructor(writer, loader) {
127
+ this.writer = writer;
128
+ this.loader = loader;
129
+ }
130
+ async preHandle(_node, _inst) {
131
+ return true;
132
+ }
133
+ async postHandle(node, inst) {
134
+ if (!this.writer || !this.loader)
135
+ return; // 未注入:静默跳过
136
+ // 时机:仅结束节点 + 流程正常完成(Done)+ 同意
137
+ if (!node || node.type !== TypeEnd)
138
+ return;
139
+ if (!inst || inst.state !== InstanceState.Done)
140
+ return;
141
+ const submitType = Number(inst.variables[KeySubmitType]);
142
+ if (submitType !== SubmitType.Agree)
143
+ return;
144
+ // 表名:流程定义顶层 relTableName,缺省回落流程 name
145
+ const tableName = await this.resolveTableName(inst);
146
+ if (!tableName)
147
+ return; // 未配置:静默跳过
148
+ // 幂等:以 process_instance_id 为键,先查后插
149
+ if (await this.writer.exists(tableName, 'process_instance_id', inst.id))
150
+ return;
151
+ // 提取 f_ 前缀字段(去前缀)
152
+ const prefix = this.fieldPrefix || 'f_';
153
+ const data = {};
154
+ for (const [k, v] of Object.entries(inst.variables)) {
155
+ if (k.startsWith(prefix) && k.length > prefix.length)
156
+ data[k.slice(prefix.length)] = v;
157
+ }
158
+ // 流程上下文字段(蛇形列名约定,与 writer 系统字段一致)
159
+ data['process_instance_id'] ??= inst.id;
160
+ data['apply_user_id'] ??= inst.operator;
161
+ data['apply_dept_id'] ??= inst.variables[KeyDeptID] ?? null;
162
+ // 通用系统字段(writer 按配置列填充)
163
+ this.writer.fillSystemFields(data, true);
164
+ await this.writer.insert(tableName, data);
165
+ }
166
+ async resolveTableName(inst) {
167
+ const define = await this.loader(inst.defineId);
168
+ if (!define)
169
+ return '';
170
+ const content = typeof define.content === 'string' ? define.content : new TextDecoder().decode(define.content);
171
+ let meta;
172
+ try {
173
+ meta = JSON.parse(content);
174
+ }
175
+ catch {
176
+ return '';
177
+ }
178
+ const tableName = (meta.relTableName ?? '').trim();
179
+ return tableName || (meta.name ?? '').trim(); // 缺省回落流程 name
180
+ }
181
+ }
@@ -19,11 +19,15 @@ export interface HandlerMeta {
19
19
  /** 分组(拦截器 pre/post 显式声明;其余可为空) */
20
20
  group?: string;
21
21
  }
22
+ /** 内置通用 AssignmentHandler 元数据(v1.6.0 issues/16,注册名与 Java 类全限定名一致,四语言通用) */
23
+ export declare const BUILTIN_ASSIGNMENT_METAS: HandlerMeta[];
22
24
  /** 处理器注册表——按名称注册/解析 + 元数据清单(对标 Spring IoC / Java HandlerRegistry) */
23
25
  export declare class HandlerRegistry {
24
26
  private assignments;
25
27
  private decisions;
26
28
  private metas;
29
+ /** 构造即内置 7 个通用 AssignmentHandler 元数据(v1.6.0 issues/16,注册名与 Java 类全限定名一致) */
30
+ constructor();
27
31
  registerAssignment(name: string, handler: IAssignmentHandler, meta?: Omit<HandlerMeta, 'name'>): void;
28
32
  registerDecision(name: string, handler: IDecisionHandler, meta?: Omit<HandlerMeta, 'name'>): void;
29
33
  resolveAssignment(name: string): IAssignmentHandler | undefined;
package/dist/registry.js CHANGED
@@ -1,8 +1,24 @@
1
+ /** 内置通用 AssignmentHandler 元数据(v1.6.0 issues/16,注册名与 Java 类全限定名一致,四语言通用) */
2
+ export const BUILTIN_ASSIGNMENT_METAS = [
3
+ { name: 'com.mldong.jeeflow.interceptor.impl.OperatorAssignmentHandler', displayName: '流程发起人', order: -9999 },
4
+ { name: 'com.mldong.jeeflow.interceptor.impl.OrgUserAssignmentHandlers$ApplicantDeptLeaderAssignmentHandler', displayName: '发起人所属部门经理', order: 10 },
5
+ { name: 'com.mldong.jeeflow.interceptor.impl.OrgUserAssignmentHandlers$ApplicantDeptMainLeaderAssignmentHandler', displayName: '发起人所属部门分管领导', order: 20 },
6
+ { name: 'com.mldong.jeeflow.interceptor.impl.OrgUserAssignmentHandlers$DeptLeaderAssignmentHandler', displayName: '当前用户所属部门经理', order: 30 },
7
+ { name: 'com.mldong.jeeflow.interceptor.impl.OrgUserAssignmentHandlers$DeptMainLeaderAssignmentHandler', displayName: '当前用户所属部门分管领导', order: 40 },
8
+ { name: 'com.mldong.jeeflow.interceptor.impl.FormFieldAssigneeHandler', displayName: '根据表单字段值分配参与者', order: 50 },
9
+ { name: 'com.mldong.jeeflow.interceptor.impl.OrgUserAssignmentHandlers$TaskRoleAssigneeHandler', displayName: '根据任务节点唯一编码关联角色分配参与者', order: 60 },
10
+ ];
1
11
  /** 处理器注册表——按名称注册/解析 + 元数据清单(对标 Spring IoC / Java HandlerRegistry) */
2
12
  export class HandlerRegistry {
3
13
  assignments = new Map();
4
14
  decisions = new Map();
5
15
  metas = new Map();
16
+ /** 构造即内置 7 个通用 AssignmentHandler 元数据(v1.6.0 issues/16,注册名与 Java 类全限定名一致) */
17
+ constructor() {
18
+ for (const meta of BUILTIN_ASSIGNMENT_METAS) {
19
+ this.metas.set(meta.name, meta);
20
+ }
21
+ }
6
22
  registerAssignment(name, handler, meta) {
7
23
  this.assignments.set(name, handler);
8
24
  if (meta)
@@ -23,9 +39,11 @@ export class HandlerRegistry {
23
39
  /** 按处理器类型列出可用实现的元数据(typeName: AssignmentHandler / DecisionHandler,按 order 升序) */
24
40
  listHandlers(typeName) {
25
41
  const names = typeName === 'AssignmentHandler'
26
- ? [...this.assignments.keys()]
27
- : [...this.decisions.keys()];
28
- return names
42
+ ? [...this.assignments.keys(), ...this.metas.keys()]
43
+ : [...this.decisions.keys(), ...this.metas.keys()];
44
+ // 内置 meta 属于 AssignmentHandler 类型——DecisionHandler 只列注册过的
45
+ const unique = typeName === 'AssignmentHandler' ? names : this.decisions.keys();
46
+ return [...new Set(unique)]
29
47
  .map(n => this.metas.get(n) ?? { name: n })
30
48
  .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
31
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mldong/jeeflow",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "jeeflow workflow engine — Node.js / TypeScript implementation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",