@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 +13 -0
- package/dist/persist.js +55 -17
- package/package.json +1 -1
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
|
|
52
|
-
return columns.filter(c =>
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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] ??=
|
|
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] ??=
|
|
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] ??=
|
|
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)
|