@mldong/jeeflow 1.6.5 → 1.7.0
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/meta.d.ts +78 -0
- package/dist/meta.js +268 -0
- package/package.json +1 -1
package/dist/meta.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import type { DynamicTableWriter } from './persist.js';
|
|
3
|
+
/** 字段存储类型(对齐 mldong dev_schema_field 1-5 语义) */
|
|
4
|
+
export declare enum StorageType {
|
|
5
|
+
Normal = 1,// 直写列
|
|
6
|
+
Expand = 2,// 对象展开为多列(expandFields 定义子字段列映射)
|
|
7
|
+
Json = 3,// 对象/数组序列化为 JSON 串写列
|
|
8
|
+
One2One = 4,// 子表单条(外键=主表主键,同事务)
|
|
9
|
+
One2Many = 5
|
|
10
|
+
}
|
|
11
|
+
/** 解析 storageType:支持名称("EXPAND")与数字(2,mldong 语义) */
|
|
12
|
+
export declare function parseStorageType(v: unknown): StorageType;
|
|
13
|
+
/** 字段元数据——表单字段 → 存储语义映射 */
|
|
14
|
+
export interface FieldMeta {
|
|
15
|
+
name: string;
|
|
16
|
+
columnName?: string;
|
|
17
|
+
storageType?: StorageType;
|
|
18
|
+
expandFields?: Record<string, string>;
|
|
19
|
+
targetTable?: string;
|
|
20
|
+
foreignKey?: string;
|
|
21
|
+
}
|
|
22
|
+
/** 表元数据——一张业务表的字段存储规范 */
|
|
23
|
+
export interface TableMeta {
|
|
24
|
+
tableName: string;
|
|
25
|
+
primaryKey?: string;
|
|
26
|
+
fields: FieldMeta[];
|
|
27
|
+
}
|
|
28
|
+
export declare function columnOf(f: FieldMeta): string;
|
|
29
|
+
export declare function pkOf(m: TableMeta): string;
|
|
30
|
+
export declare function findField(m: TableMeta, name: string): FieldMeta | undefined;
|
|
31
|
+
export declare function findFieldByColumn(m: TableMeta, columnName: string): FieldMeta | undefined;
|
|
32
|
+
/** 驼峰转下划线(companyName → company_name) */
|
|
33
|
+
export declare function toUnderline(name: string): string;
|
|
34
|
+
/** 元数据提供者 SPI——集成方只实现这一件事;未定义返回 null(回落表结构探测) */
|
|
35
|
+
export interface IDynamicMetaProvider {
|
|
36
|
+
loadTableMeta(tableName: string): TableMeta | null;
|
|
37
|
+
}
|
|
38
|
+
/** 内置 JSON 配置加载器——从目录加载(文件名 = 表名,如 biz_leave.json) */
|
|
39
|
+
export declare class JsonMetaProvider implements IDynamicMetaProvider {
|
|
40
|
+
private dir;
|
|
41
|
+
private cache;
|
|
42
|
+
constructor(dir: string);
|
|
43
|
+
loadTableMeta(tableName: string): TableMeta | null;
|
|
44
|
+
}
|
|
45
|
+
/** 元数据驱动的动态写入器——按 TableMeta.storageType 语义执行插入;
|
|
46
|
+
* 无元数据时完全委托基础 writer(回落现状,零破坏)。读侧由 MetaTableReader 提供。 */
|
|
47
|
+
export declare class MetaTableWriter implements DynamicTableWriter {
|
|
48
|
+
private base;
|
|
49
|
+
private provider;
|
|
50
|
+
constructor(base: DynamicTableWriter, provider: IDynamicMetaProvider);
|
|
51
|
+
filterColumns(tableName: string, columns: string[]): string[] | Promise<string[]>;
|
|
52
|
+
insert(tableName: string, data: Record<string, unknown>): unknown;
|
|
53
|
+
exists(tableName: string, bizKey: string, bizKeyValue: unknown): boolean | Promise<boolean>;
|
|
54
|
+
fillSystemFields(data: Record<string, unknown>, isInsert: boolean): void;
|
|
55
|
+
private expandInto;
|
|
56
|
+
private insertSubTable;
|
|
57
|
+
private insertSubRow;
|
|
58
|
+
}
|
|
59
|
+
/** 业务表查询器——按列等值查询原始行(与写入器职责分离) */
|
|
60
|
+
export declare class JdbcTableReader {
|
|
61
|
+
private db;
|
|
62
|
+
constructor(db: DatabaseSync);
|
|
63
|
+
queryFirst(tableName: string, whereColumn: string, value: unknown): Record<string, unknown> | null;
|
|
64
|
+
queryList(tableName: string, whereColumn: string, value: unknown, limit: number): Record<string, unknown>[];
|
|
65
|
+
}
|
|
66
|
+
/** 元数据驱动的动态读取器——按流程实例回显业务数据。
|
|
67
|
+
* 边界(不做):通用条件分页 / 动态条件语法 / 数据权限 / 排序。 */
|
|
68
|
+
export declare class MetaTableReader {
|
|
69
|
+
private reader;
|
|
70
|
+
private provider;
|
|
71
|
+
constructor(reader: JdbcTableReader, provider: IDynamicMetaProvider);
|
|
72
|
+
readByProcessInstance(tableName: string, processInstanceId: unknown): Record<string, unknown> | null;
|
|
73
|
+
assemble(meta: TableMeta, row: Record<string, unknown>): Record<string, unknown>;
|
|
74
|
+
private expandFrom;
|
|
75
|
+
private readSubTable;
|
|
76
|
+
}
|
|
77
|
+
/** 按列名取值(宽松:忽略大小写) */
|
|
78
|
+
export declare function findRowValue(row: Record<string, unknown>, columnName: string): unknown;
|
package/dist/meta.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// ═══ 元数据驱动的动态写入/读取规范(issues/23)— 与 Java/Go/Python 契约一致 ═══
|
|
2
|
+
/** 字段存储类型(对齐 mldong dev_schema_field 1-5 语义) */
|
|
3
|
+
export var StorageType;
|
|
4
|
+
(function (StorageType) {
|
|
5
|
+
StorageType[StorageType["Normal"] = 1] = "Normal";
|
|
6
|
+
StorageType[StorageType["Expand"] = 2] = "Expand";
|
|
7
|
+
StorageType[StorageType["Json"] = 3] = "Json";
|
|
8
|
+
StorageType[StorageType["One2One"] = 4] = "One2One";
|
|
9
|
+
StorageType[StorageType["One2Many"] = 5] = "One2Many";
|
|
10
|
+
})(StorageType || (StorageType = {}));
|
|
11
|
+
/** 解析 storageType:支持名称("EXPAND")与数字(2,mldong 语义) */
|
|
12
|
+
export function parseStorageType(v) {
|
|
13
|
+
if (typeof v === 'number')
|
|
14
|
+
return v;
|
|
15
|
+
if (typeof v === 'string')
|
|
16
|
+
return StorageType[v] ?? StorageType.Normal;
|
|
17
|
+
return StorageType.Normal;
|
|
18
|
+
}
|
|
19
|
+
export function columnOf(f) {
|
|
20
|
+
return f.columnName || toUnderline(f.name);
|
|
21
|
+
}
|
|
22
|
+
export function pkOf(m) {
|
|
23
|
+
return m.primaryKey || 'id';
|
|
24
|
+
}
|
|
25
|
+
export function findField(m, name) {
|
|
26
|
+
return m.fields.find(f => f.name.toLowerCase() === name.toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
export function findFieldByColumn(m, columnName) {
|
|
29
|
+
return m.fields.find(f => columnOf(f).toLowerCase() === columnName.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
/** 驼峰转下划线(companyName → company_name) */
|
|
32
|
+
export function toUnderline(name) {
|
|
33
|
+
return name.replace(/([A-Z])/g, (c, _, i) => (i > 0 ? '_' : '') + c.toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
/** 内置 JSON 配置加载器——从目录加载(文件名 = 表名,如 biz_leave.json) */
|
|
36
|
+
export class JsonMetaProvider {
|
|
37
|
+
dir;
|
|
38
|
+
cache = new Map();
|
|
39
|
+
constructor(dir) {
|
|
40
|
+
this.dir = dir;
|
|
41
|
+
}
|
|
42
|
+
loadTableMeta(tableName) {
|
|
43
|
+
if (this.cache.has(tableName))
|
|
44
|
+
return this.cache.get(tableName);
|
|
45
|
+
const data = readFileSyncSafe(`${this.dir}/${tableName}.json`);
|
|
46
|
+
if (data === null)
|
|
47
|
+
return null;
|
|
48
|
+
const raw = JSON.parse(data);
|
|
49
|
+
const meta = {
|
|
50
|
+
tableName: raw.tableName ?? tableName,
|
|
51
|
+
primaryKey: raw.primaryKey ?? 'id',
|
|
52
|
+
fields: (raw.fields ?? []).map((f) => ({
|
|
53
|
+
name: f.name,
|
|
54
|
+
columnName: f.columnName,
|
|
55
|
+
storageType: parseStorageType(f.storageType ?? 1),
|
|
56
|
+
expandFields: f.expandFields,
|
|
57
|
+
targetTable: f.targetTable,
|
|
58
|
+
foreignKey: f.foreignKey,
|
|
59
|
+
})),
|
|
60
|
+
};
|
|
61
|
+
this.cache.set(tableName, meta);
|
|
62
|
+
return meta;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
import { readFileSync } from 'node:fs';
|
|
66
|
+
function readFileSyncSafe(path) {
|
|
67
|
+
try {
|
|
68
|
+
return readFileSync(path, 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// ═══ MetaTableWriter(写,纯写职责) ═══════════════════════════════════════════
|
|
75
|
+
/** 元数据驱动的动态写入器——按 TableMeta.storageType 语义执行插入;
|
|
76
|
+
* 无元数据时完全委托基础 writer(回落现状,零破坏)。读侧由 MetaTableReader 提供。 */
|
|
77
|
+
export class MetaTableWriter {
|
|
78
|
+
base;
|
|
79
|
+
provider;
|
|
80
|
+
constructor(base, provider) {
|
|
81
|
+
this.base = base;
|
|
82
|
+
this.provider = provider;
|
|
83
|
+
}
|
|
84
|
+
filterColumns(tableName, columns) {
|
|
85
|
+
return this.base.filterColumns(tableName, columns);
|
|
86
|
+
}
|
|
87
|
+
insert(tableName, data) {
|
|
88
|
+
const meta = this.provider.loadTableMeta(tableName);
|
|
89
|
+
if (!meta)
|
|
90
|
+
return this.base.insert(tableName, data); // 无元数据:回落现状
|
|
91
|
+
const subData = new Map();
|
|
92
|
+
const row = {};
|
|
93
|
+
for (const f of meta.fields) {
|
|
94
|
+
const v = data[f.name];
|
|
95
|
+
if (v === undefined || v === null)
|
|
96
|
+
continue;
|
|
97
|
+
const st = f.storageType ?? StorageType.Normal;
|
|
98
|
+
if (st === StorageType.Json) {
|
|
99
|
+
row[columnOf(f)] = JSON.stringify(v);
|
|
100
|
+
}
|
|
101
|
+
else if (st === StorageType.Expand) {
|
|
102
|
+
this.expandInto(f, v, row);
|
|
103
|
+
}
|
|
104
|
+
else if (st === StorageType.One2One || st === StorageType.One2Many) {
|
|
105
|
+
subData.set(f.name, v);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
row[columnOf(f)] = v;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 未消费字段(流程上下文 process_instance_id 等)直通基础 writer
|
|
112
|
+
for (const [k, v] of Object.entries(data)) {
|
|
113
|
+
if (!findField(meta, k))
|
|
114
|
+
row[k] ??= v;
|
|
115
|
+
}
|
|
116
|
+
this.base.fillSystemFields(row, true);
|
|
117
|
+
let pk = this.base.insert(tableName, row); // 主表插入(自增/生成器返回主键)
|
|
118
|
+
if (pk === undefined || pk === null)
|
|
119
|
+
pk = findRowValue(row, pkOf(meta));
|
|
120
|
+
// 子表递归插入(外键=主表主键)
|
|
121
|
+
for (const [name, v] of subData) {
|
|
122
|
+
this.insertSubTable(meta, findField(meta, name), v, pk);
|
|
123
|
+
}
|
|
124
|
+
return pk;
|
|
125
|
+
}
|
|
126
|
+
exists(tableName, bizKey, bizKeyValue) {
|
|
127
|
+
return this.base.exists(tableName, bizKey, bizKeyValue);
|
|
128
|
+
}
|
|
129
|
+
fillSystemFields(data, isInsert) {
|
|
130
|
+
this.base.fillSystemFields(data, isInsert);
|
|
131
|
+
}
|
|
132
|
+
expandInto(f, v, row) {
|
|
133
|
+
if (typeof v !== 'object' || v === null)
|
|
134
|
+
return;
|
|
135
|
+
const obj = v;
|
|
136
|
+
for (const [sub, col] of Object.entries(f.expandFields ?? {})) {
|
|
137
|
+
if (obj[sub] !== undefined && obj[sub] !== null)
|
|
138
|
+
row[col] = obj[sub];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
insertSubTable(parentMeta, f, v, parentPk) {
|
|
142
|
+
if (parentPk === undefined || parentPk === null) {
|
|
143
|
+
throw new Error(`persist: parent primary key missing, cannot insert sub table ${f.name}`);
|
|
144
|
+
}
|
|
145
|
+
const fk = f.foreignKey || pkOf(parentMeta);
|
|
146
|
+
const st = f.storageType ?? StorageType.Normal;
|
|
147
|
+
if (st === StorageType.One2One && typeof v === 'object' && v !== null) {
|
|
148
|
+
this.insertSubRow(f, v, fk, parentPk);
|
|
149
|
+
}
|
|
150
|
+
else if (st === StorageType.One2Many && Array.isArray(v)) {
|
|
151
|
+
for (const item of v) {
|
|
152
|
+
if (typeof item === 'object' && item !== null) {
|
|
153
|
+
this.insertSubRow(f, item, fk, parentPk);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
insertSubRow(f, subData, fk, parentPk) {
|
|
159
|
+
const row = { ...subData, [fk]: parentPk };
|
|
160
|
+
this.insert(f.targetTable, row); // 递归走子表自身元数据
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ═══ JdbcTableReader(读侧底层) ═══════════════════════════════════════════════
|
|
164
|
+
/** 业务表查询器——按列等值查询原始行(与写入器职责分离) */
|
|
165
|
+
export class JdbcTableReader {
|
|
166
|
+
db;
|
|
167
|
+
constructor(db) {
|
|
168
|
+
this.db = db;
|
|
169
|
+
}
|
|
170
|
+
queryFirst(tableName, whereColumn, value) {
|
|
171
|
+
const rows = this.queryList(tableName, whereColumn, value, 1);
|
|
172
|
+
return rows.length > 0 ? rows[0] : null;
|
|
173
|
+
}
|
|
174
|
+
queryList(tableName, whereColumn, value, limit) {
|
|
175
|
+
const sql = `SELECT * FROM ${tableName} WHERE ${whereColumn} = ?${limit > 0 ? ` LIMIT ${limit}` : ''}`;
|
|
176
|
+
return this.db.prepare(sql).all(value);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ═══ MetaTableReader(读,流程回显最小闭环) ═══════════════════════════════════
|
|
180
|
+
/** 元数据驱动的动态读取器——按流程实例回显业务数据。
|
|
181
|
+
* 边界(不做):通用条件分页 / 动态条件语法 / 数据权限 / 排序。 */
|
|
182
|
+
export class MetaTableReader {
|
|
183
|
+
reader;
|
|
184
|
+
provider;
|
|
185
|
+
constructor(reader, provider) {
|
|
186
|
+
this.reader = reader;
|
|
187
|
+
this.provider = provider;
|
|
188
|
+
}
|
|
189
|
+
readByProcessInstance(tableName, processInstanceId) {
|
|
190
|
+
const row = this.reader.queryFirst(tableName, 'process_instance_id', processInstanceId);
|
|
191
|
+
if (!row)
|
|
192
|
+
return null;
|
|
193
|
+
const meta = this.provider.loadTableMeta(tableName);
|
|
194
|
+
if (!meta)
|
|
195
|
+
return row; // 无元数据:原样返回(列名→值)
|
|
196
|
+
return this.assemble(meta, row);
|
|
197
|
+
}
|
|
198
|
+
assemble(meta, row) {
|
|
199
|
+
const result = {};
|
|
200
|
+
for (const f of meta.fields) {
|
|
201
|
+
const v = findRowValue(row, columnOf(f));
|
|
202
|
+
const st = f.storageType ?? StorageType.Normal;
|
|
203
|
+
if (st === StorageType.Json) {
|
|
204
|
+
if (v !== undefined && v !== null) {
|
|
205
|
+
try {
|
|
206
|
+
result[f.name] = JSON.parse(String(v));
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
result[f.name] = v;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (st === StorageType.Expand) {
|
|
214
|
+
const obj = this.expandFrom(row, f);
|
|
215
|
+
if (Object.keys(obj).length > 0)
|
|
216
|
+
result[f.name] = obj;
|
|
217
|
+
}
|
|
218
|
+
else if (st === StorageType.One2One || st === StorageType.One2Many) {
|
|
219
|
+
const sub = this.readSubTable(meta, f, row);
|
|
220
|
+
if (sub !== undefined && sub !== null)
|
|
221
|
+
result[f.name] = sub;
|
|
222
|
+
}
|
|
223
|
+
else if (v !== undefined && v !== null) {
|
|
224
|
+
result[f.name] = v;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// 未在元数据中的列带出(key 统一小写)
|
|
228
|
+
for (const [k, v] of Object.entries(row)) {
|
|
229
|
+
if (!findFieldByColumn(meta, k))
|
|
230
|
+
result[k.toLowerCase()] ??= v;
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
expandFrom(row, f) {
|
|
235
|
+
const obj = {};
|
|
236
|
+
for (const [sub, col] of Object.entries(f.expandFields ?? {})) {
|
|
237
|
+
const v = findRowValue(row, col);
|
|
238
|
+
if (v !== undefined && v !== null)
|
|
239
|
+
obj[sub] = v;
|
|
240
|
+
}
|
|
241
|
+
return obj;
|
|
242
|
+
}
|
|
243
|
+
readSubTable(parentMeta, f, row) {
|
|
244
|
+
const parentPk = findRowValue(row, pkOf(parentMeta));
|
|
245
|
+
if (parentPk === undefined || parentPk === null)
|
|
246
|
+
return null;
|
|
247
|
+
const fk = f.foreignKey || pkOf(parentMeta);
|
|
248
|
+
const subMeta = this.provider.loadTableMeta(f.targetTable);
|
|
249
|
+
const st = f.storageType ?? StorageType.Normal;
|
|
250
|
+
if (st === StorageType.One2One) {
|
|
251
|
+
const sub = this.reader.queryFirst(f.targetTable, fk, parentPk);
|
|
252
|
+
if (!sub)
|
|
253
|
+
return null;
|
|
254
|
+
return subMeta ? this.assemble(subMeta, sub) : sub;
|
|
255
|
+
}
|
|
256
|
+
// ONE2MANY
|
|
257
|
+
const subs = this.reader.queryList(f.targetTable, fk, parentPk, 0);
|
|
258
|
+
return subs.map(sub => (subMeta ? this.assemble(subMeta, sub) : sub));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** 按列名取值(宽松:忽略大小写) */
|
|
262
|
+
export function findRowValue(row, columnName) {
|
|
263
|
+
for (const [k, v] of Object.entries(row)) {
|
|
264
|
+
if (k.toLowerCase() === columnName.toLowerCase())
|
|
265
|
+
return v;
|
|
266
|
+
}
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|