@mldong/jeeflow 1.6.2 → 1.6.4

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/persist.d.ts CHANGED
@@ -32,12 +32,25 @@ export declare class SqliteDynamicTableWriter implements DynamicTableWriter {
32
32
  updateTimeColumn?: string | null;
33
33
  updateUserColumn?: string | null;
34
34
  isDeletedColumn?: string | null;
35
+ /** 用户列默认值(issues/19):优先取 data 中已注入的 apply_user_id=流程 operator,
36
+ * 否则用此配置值,缺省 "system"——多数框架业务表 create_user/update_user 为 BIGINT 存 userId */
37
+ defaultUserValue: unknown;
38
+ /** 列匹配(issues/20):默认宽松——驼峰↔下划线归一匹配(表单字段 companyName ↔ 表列 company_name);
39
+ * 需要精确控制列名的集成方显式开启严格模式(忽略大小写精确匹配) */
40
+ strictColumnMatch: boolean;
35
41
  constructor(db: DatabaseSync);
36
42
  private tableColumns;
37
43
  filterColumns(tableName: string, columns: string[]): string[];
38
44
  insert(tableName: string, data: Record<string, unknown>): unknown;
45
+ /** 列匹配(issues/20):严格=忽略大小写精确;宽松(默认)=驼峰↔下划线归一匹配 */
46
+ private findColumn;
47
+ /** 在 data 中找匹配指定表列的 key(宽松模式驼峰 key 匹配下划线列) */
48
+ private findDataKey;
39
49
  exists(tableName: string, bizKey: string, bizKeyValue: unknown): boolean;
40
50
  fillSystemFields(data: Record<string, unknown>, isInsert: boolean): void;
51
+ /** 默认用户值(issues/19):优先取 data 中已注入的 apply_user_id
52
+ * (拦截器场景 = 流程 operator,BIGINT 用户列表开箱即用),否则回落配置默认值 */
53
+ private resolveDefaultUser;
41
54
  }
42
55
  /** 流程定义加载器(用于解析 relTableName / 流程 name),通常透传仓库 findDefineById */
43
56
  export type DefineLoader = (defineId: number) => Promise<ProcessDefine | null>;
package/dist/persist.js CHANGED
@@ -2,6 +2,10 @@ import { KeySubmitType, KeyDeptID } from './engine.js';
2
2
  import { TypeEnd, InstanceState, SubmitType } from './model.js';
3
3
  // ─── 默认实现:node:sqlite(内置零依赖) ──────────────────────────────────────
4
4
  const TABLE_NAME_RE = /^[A-Za-z0-9_]+$/;
5
+ /** 列名归一(issues/20):转小写 + 去下划线(companyName / company_name / COMPANY_NAME 等价) */
6
+ function normalizeColumn(name) {
7
+ return name.toLowerCase().replace(/_/g, '');
8
+ }
5
9
  function nowText() {
6
10
  const d = new Date();
7
11
  const p = (n) => String(n).padStart(2, '0');
@@ -31,6 +35,12 @@ export class SqliteDynamicTableWriter {
31
35
  updateTimeColumn = 'update_time';
32
36
  updateUserColumn = 'update_user';
33
37
  isDeletedColumn = 'is_deleted';
38
+ /** 用户列默认值(issues/19):优先取 data 中已注入的 apply_user_id=流程 operator,
39
+ * 否则用此配置值,缺省 "system"——多数框架业务表 create_user/update_user 为 BIGINT 存 userId */
40
+ defaultUserValue = 'system';
41
+ /** 列匹配(issues/20):默认宽松——驼峰↔下划线归一匹配(表单字段 companyName ↔ 表列 company_name);
42
+ * 需要精确控制列名的集成方显式开启严格模式(忽略大小写精确匹配) */
43
+ strictColumnMatch = false;
34
44
  constructor(db) {
35
45
  this.db = db;
36
46
  }
@@ -48,27 +58,21 @@ export class SqliteDynamicTableWriter {
48
58
  }
49
59
  filterColumns(tableName, columns) {
50
60
  checkTableName(tableName);
51
- const set = new Set(this.tableColumns(tableName));
52
- return columns.filter(c => set.has(c.toUpperCase()));
61
+ const cols = this.tableColumns(tableName);
62
+ return columns.filter(c => this.findColumn(cols, c) !== '');
53
63
  }
54
64
  insert(tableName, data) {
55
65
  checkTableName(tableName);
56
66
  const cols = this.tableColumns(tableName);
57
67
  const names = [];
58
68
  const values = [];
59
- // 保持插入顺序稳定(对象键无序,按表列顺序取)
69
+ // 保持插入顺序稳定(对象键无序,按表列顺序取);写入用表列原名(issues/20)
60
70
  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
- }
71
+ const key = this.findDataKey(data, col);
72
+ if (key === '')
73
+ continue;
74
+ names.push(col);
75
+ values.push(data[key]);
72
76
  }
73
77
  if (names.length === 0)
74
78
  throw new Error(`persist: no matching columns for ${tableName}`);
@@ -77,6 +81,27 @@ export class SqliteDynamicTableWriter {
77
81
  const res = stmt.run(...values);
78
82
  return res.lastInsertRowid;
79
83
  }
84
+ /** 列匹配(issues/20):严格=忽略大小写精确;宽松(默认)=驼峰↔下划线归一匹配 */
85
+ findColumn(cols, key) {
86
+ for (const col of cols) {
87
+ if (this.strictColumnMatch) {
88
+ if (col.toUpperCase() === key.toUpperCase())
89
+ return col;
90
+ }
91
+ else if (normalizeColumn(col) === normalizeColumn(key)) {
92
+ return col;
93
+ }
94
+ }
95
+ return '';
96
+ }
97
+ /** 在 data 中找匹配指定表列的 key(宽松模式驼峰 key 匹配下划线列) */
98
+ findDataKey(data, col) {
99
+ for (const k of Object.keys(data)) {
100
+ if (this.findColumn([col], k) !== '')
101
+ return k;
102
+ }
103
+ return '';
104
+ }
80
105
  exists(tableName, bizKey, bizKeyValue) {
81
106
  checkTableName(tableName);
82
107
  this.tableColumns(tableName); // 表不存在提前报错
@@ -89,11 +114,11 @@ export class SqliteDynamicTableWriter {
89
114
  if (this.createTimeColumn)
90
115
  data[this.createTimeColumn] ??= now;
91
116
  if (this.createUserColumn)
92
- data[this.createUserColumn] ??= 'system';
117
+ data[this.createUserColumn] ??= this.resolveDefaultUser(data);
93
118
  if (this.updateTimeColumn)
94
119
  data[this.updateTimeColumn] ??= now;
95
120
  if (this.updateUserColumn)
96
- data[this.updateUserColumn] ??= 'system';
121
+ data[this.updateUserColumn] ??= this.resolveDefaultUser(data);
97
122
  if (this.isDeletedColumn)
98
123
  data[this.isDeletedColumn] ??= 0;
99
124
  }
@@ -101,9 +126,14 @@ export class SqliteDynamicTableWriter {
101
126
  if (this.updateTimeColumn)
102
127
  data[this.updateTimeColumn] = now;
103
128
  if (this.updateUserColumn)
104
- data[this.updateUserColumn] ??= 'system';
129
+ data[this.updateUserColumn] ??= this.resolveDefaultUser(data);
105
130
  }
106
131
  }
132
+ /** 默认用户值(issues/19):优先取 data 中已注入的 apply_user_id
133
+ * (拦截器场景 = 流程 operator,BIGINT 用户列表开箱即用),否则回落配置默认值 */
134
+ resolveDefaultUser(data) {
135
+ return data['apply_user_id'] ?? this.defaultUserValue;
136
+ }
107
137
  }
108
138
  /**
109
139
  * 工作流业务数据入库适配拦截器——流程结束同意后,f_ 表单数据写入业务表。
@@ -141,6 +171,14 @@ export class PersistPostInterceptor {
141
171
  const submitType = Number(inst.variables[KeySubmitType]);
142
172
  if (submitType !== SubmitType.Agree)
143
173
  return;
174
+ // 同链重复触发防护(issues/19):最后任务节点与结束节点都会触发后置拦截器,
175
+ // 同一执行链(共享 inst.variables)只插一次。标记写入时实例已完成持久化
176
+ // (引擎 executeNode 先 updateInstance 后触发拦截器,repo 存副本)不会落库;
177
+ // exists 保留作为跨请求/重启的幂等兜底(先查后插语义不变)。
178
+ const chainKey = `__persist_executed_${inst.id}`;
179
+ if (inst.variables[chainKey] === true)
180
+ return;
181
+ inst.variables[chainKey] = true;
144
182
  // 表名:流程定义顶层 relTableName,缺省回落流程 name
145
183
  const tableName = await this.resolveTableName(inst);
146
184
  if (!tableName)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mldong/jeeflow",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "jeeflow workflow engine — Node.js / TypeScript implementation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",