jsql-neo 5.4.2 → 6.0.0-beta2
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/README.md +198 -14
- package/index.d.ts +29 -0
- package/index.js +12 -2
- package/lib/ast.js +214 -0
- package/lib/database.js +26 -60
- package/lib/mongo_server.js +1 -1
- package/lib/native_client.js +43 -66
- package/lib/plugin.js +227 -3
- package/lib/plugins/audit.js +37 -0
- package/lib/plugins/index.js +72 -0
- package/lib/plugins/logger.js +35 -0
- package/lib/plugins/metrics.js +38 -0
- package/lib/plugins/timestamps.js +43 -0
- package/lib/plugins/validation.js +128 -0
- package/lib/sql.js +1212 -84
- package/lib/wasm_client.js +41 -57
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +8 -1
- package/nativesrc/jsql-neo-core/Cargo.toml +3 -1
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +50 -13
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +46 -23
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +13 -1
- package/nativesrc/jsql-neo-core/src/engine/table.rs +338 -42
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +245 -32
- package/nativesrc/jsql-neo-native/Cargo.lock +9 -2
- package/nativesrc/jsql-neo-native/Cargo.toml +9 -1
- package/nativesrc/jsql-neo-native/src/lib.rs +14 -2
- package/nativesrc/jsql-neo-wasm/Cargo.lock +9 -2
- package/nativesrc/jsql-neo-wasm/Cargo.toml +8 -1
- package/nativesrc/jsql-neo-wasm/src/lib.rs +18 -2
- package/package.json +4 -3
- package/test/sql-parser.test.js +450 -0
- package/wasm/browser.mjs +12 -0
- package/wasm/browser_bg.mjs +269 -248
- package/wasm/jsql_neo_wasm.d.ts +6 -0
- package/wasm/jsql_neo_wasm.js +248 -127
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +22 -22
- package/wasm/package.json +1 -1
package/lib/database.js
CHANGED
|
@@ -11,6 +11,10 @@ const crypto = require('crypto');
|
|
|
11
11
|
const Table = require('./table');
|
|
12
12
|
const { createError } = require('./errors');
|
|
13
13
|
const JSQLFormat = require('./jsql_format');
|
|
14
|
+
const {
|
|
15
|
+
createHookRegistry, applyPlugin, buildPluginContext, registerHook,
|
|
16
|
+
runHooks, getPluginApis
|
|
17
|
+
} = require('./plugin');
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* 解析字段简写类型字符串,如 'integer primary key'、'string unique'、
|
|
@@ -91,17 +95,7 @@ class Database {
|
|
|
91
95
|
this._versions = {}; // tableName -> [{ rows, timestamp }]
|
|
92
96
|
this._maxVersions = options.maxVersions || 50;
|
|
93
97
|
this._plugins = [];
|
|
94
|
-
this._hooks =
|
|
95
|
-
beforeInsert: [], afterInsert: [],
|
|
96
|
-
beforeUpdate: [], afterUpdate: [],
|
|
97
|
-
beforeDelete: [], afterDelete: [],
|
|
98
|
-
beforeFind: [], afterFind: [],
|
|
99
|
-
beforeCreateTable: [], afterCreateTable: [],
|
|
100
|
-
beforeDropTable: [], afterDropTable: [],
|
|
101
|
-
beforeFlush: [], afterFlush: [],
|
|
102
|
-
beforeCount: [], afterCount: [],
|
|
103
|
-
onStart: [], onStop: []
|
|
104
|
-
};
|
|
98
|
+
this._hooks = createHookRegistry();
|
|
105
99
|
this._eventListeners = [];
|
|
106
100
|
this._tableNames = new Set(Object.keys(this._tables));
|
|
107
101
|
this._migrations = [];
|
|
@@ -1082,53 +1076,18 @@ class Database {
|
|
|
1082
1076
|
|
|
1083
1077
|
/**
|
|
1084
1078
|
* 注册插件
|
|
1085
|
-
* @param {object|function} plugin - { name, install(db, ctx), hooks, onEvent } 或函数 (作为 install)
|
|
1079
|
+
* @param {object|function} plugin - { name, version, install(db, ctx), hooks, api, onEvent } 或函数 (作为 install)
|
|
1086
1080
|
*/
|
|
1087
1081
|
use(plugin) {
|
|
1088
|
-
|
|
1089
|
-
plugin = { install: plugin };
|
|
1090
|
-
}
|
|
1091
|
-
if (plugin.hooks) {
|
|
1092
|
-
for (const [event, fn] of Object.entries(plugin.hooks)) {
|
|
1093
|
-
if (this._hooks[event]) {
|
|
1094
|
-
this._hooks[event].push(fn);
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
if (typeof plugin.install === 'function') {
|
|
1099
|
-
plugin.install(this, this._buildCtx(plugin));
|
|
1100
|
-
}
|
|
1101
|
-
if (typeof plugin.onEvent === 'function') {
|
|
1102
|
-
this._eventListeners.push(plugin.onEvent);
|
|
1103
|
-
}
|
|
1104
|
-
this._plugins.push(plugin);
|
|
1105
|
-
return this;
|
|
1082
|
+
return applyPlugin(this, plugin);
|
|
1106
1083
|
}
|
|
1107
1084
|
|
|
1108
1085
|
_buildCtx(plugin) {
|
|
1109
|
-
|
|
1110
|
-
return {
|
|
1111
|
-
name: plugin.name || 'anonymous',
|
|
1112
|
-
engine: this,
|
|
1113
|
-
plugin,
|
|
1114
|
-
on(hook, fn) { return self.on(hook, fn); },
|
|
1115
|
-
onEvent(fn) { return self.onEvent(fn); },
|
|
1116
|
-
emit(eventName, data) { self._emit(eventName, data); },
|
|
1117
|
-
tables() { return Array.from(self._tableNames); },
|
|
1118
|
-
hasTable(name) { return self._tableNames.has(name); },
|
|
1119
|
-
getTableSchema(name) {
|
|
1120
|
-
const t = self._tables[name];
|
|
1121
|
-
return t ? t._schema : null;
|
|
1122
|
-
},
|
|
1123
|
-
table(name) { return self._tables[name] || null; }
|
|
1124
|
-
};
|
|
1086
|
+
return buildPluginContext(this, plugin);
|
|
1125
1087
|
}
|
|
1126
1088
|
|
|
1127
1089
|
on(event, fn) {
|
|
1128
|
-
|
|
1129
|
-
this._hooks[event].push(fn);
|
|
1130
|
-
}
|
|
1131
|
-
return this;
|
|
1090
|
+
return registerHook(this, event, fn);
|
|
1132
1091
|
}
|
|
1133
1092
|
|
|
1134
1093
|
onEvent(fn) {
|
|
@@ -1136,6 +1095,22 @@ class Database {
|
|
|
1136
1095
|
return this;
|
|
1137
1096
|
}
|
|
1138
1097
|
|
|
1098
|
+
/** 广播事件(插件上下文 / 外部均可调用) */
|
|
1099
|
+
emit(eventName, data) {
|
|
1100
|
+
this._emit(eventName, data);
|
|
1101
|
+
return this;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** 主动触发一个钩子(含插件自定义钩子) */
|
|
1105
|
+
runHook(hookName, ...args) {
|
|
1106
|
+
return this._runHooks(hookName, args);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/** 取插件对外暴露的 API(由 plugin.api 声明) */
|
|
1110
|
+
plugin(name) {
|
|
1111
|
+
return getPluginApis(this).get(name) || null;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1139
1114
|
_emit(eventName, data) {
|
|
1140
1115
|
for (const fn of this._eventListeners) {
|
|
1141
1116
|
try { fn(eventName, data); } catch (e) { /* ignore */ }
|
|
@@ -1143,16 +1118,7 @@ class Database {
|
|
|
1143
1118
|
}
|
|
1144
1119
|
|
|
1145
1120
|
_runHooks(hookName, args) {
|
|
1146
|
-
|
|
1147
|
-
if (!hooks || hooks.length === 0) return true;
|
|
1148
|
-
for (const fn of hooks) {
|
|
1149
|
-
const r = fn(...args);
|
|
1150
|
-
if (r === false) return false;
|
|
1151
|
-
if (r !== undefined && args.length > 0) {
|
|
1152
|
-
args[0] = r;
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
return true;
|
|
1121
|
+
return runHooks(this, hookName, args);
|
|
1156
1122
|
}
|
|
1157
1123
|
|
|
1158
1124
|
// ============================================================
|
package/lib/mongo_server.js
CHANGED
|
@@ -322,7 +322,7 @@ class MongoServer {
|
|
|
322
322
|
localTime: new Date(), logicalSessionTimeoutMinutes: 30, connectionId: 1,
|
|
323
323
|
};
|
|
324
324
|
case 'ping': return { ok: 1 };
|
|
325
|
-
case 'buildInfo': return { ok: 1, version: '5.
|
|
325
|
+
case 'buildInfo': return { ok: 1, version: '5.6.0-jsql-neo', gitVersion: 'jsql-neo', versionArray: [5, 6, 0, 0] };
|
|
326
326
|
case 'getParameter': return { ok: 1 };
|
|
327
327
|
case 'endSessions': return { ok: 1 };
|
|
328
328
|
case 'listDatabases':
|
package/lib/native_client.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const { executeSQL } = require('./sql');
|
|
4
|
+
const {
|
|
5
|
+
createHookRegistry, applyPlugin, buildPluginContext, registerHook,
|
|
6
|
+
runHooks, getPluginApis
|
|
7
|
+
} = require('./plugin');
|
|
4
8
|
const native = require(path.join(__dirname, '..', 'native', 'jsql-neo-native.node'));
|
|
5
9
|
const INT64_TAG = 1;
|
|
6
10
|
const FLOAT_TAG = 2;
|
|
@@ -229,17 +233,9 @@ class JSQL {
|
|
|
229
233
|
this._evictTimer = null;
|
|
230
234
|
|
|
231
235
|
this._plugins = [];
|
|
232
|
-
this._hooks =
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
beforeDelete: [], afterDelete: [],
|
|
236
|
-
beforeFind: [], afterFind: [],
|
|
237
|
-
beforeCreateTable: [], afterCreateTable: [],
|
|
238
|
-
beforeDropTable: [], afterDropTable: [],
|
|
239
|
-
beforeFlush: [], afterFlush: [],
|
|
240
|
-
beforeCount: [], afterCount: [],
|
|
241
|
-
onStart: [], onStop: []
|
|
242
|
-
};
|
|
236
|
+
this._hooks = createHookRegistry();
|
|
237
|
+
// native 引擎:钩子内再次调用引擎方法会触发 N-API 重入崩溃,需显式防护
|
|
238
|
+
this._forbidReentry = true;
|
|
243
239
|
this._eventListeners = [];
|
|
244
240
|
if (this._autoPlugins) {
|
|
245
241
|
const { ModuleManager } = require('./mod');
|
|
@@ -247,47 +243,16 @@ class JSQL {
|
|
|
247
243
|
}
|
|
248
244
|
}
|
|
249
245
|
|
|
250
|
-
use(plugin) {
|
|
251
|
-
|
|
252
|
-
plugin = { install: plugin };
|
|
253
|
-
}
|
|
254
|
-
if (plugin.hooks) {
|
|
255
|
-
for (const [event, fn] of Object.entries(plugin.hooks)) {
|
|
256
|
-
if (this._hooks[event]) {
|
|
257
|
-
this._hooks[event].push(fn);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
if (typeof plugin.install === 'function') {
|
|
262
|
-
plugin.install(this, this._buildCtx(plugin));
|
|
263
|
-
}
|
|
264
|
-
if (typeof plugin.onEvent === 'function') {
|
|
265
|
-
this._eventListeners.push(plugin.onEvent);
|
|
266
|
-
}
|
|
267
|
-
this._plugins.push(plugin);
|
|
268
|
-
return this;
|
|
246
|
+
use(plugin, opts) {
|
|
247
|
+
return applyPlugin(this, plugin, opts);
|
|
269
248
|
}
|
|
270
249
|
|
|
271
250
|
_buildCtx(plugin) {
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
name: plugin.name || 'anonymous',
|
|
275
|
-
engine: this,
|
|
276
|
-
plugin,
|
|
277
|
-
on(hook, fn) { return self.on(hook, fn); },
|
|
278
|
-
onEvent(fn) { return self.onEvent(fn); },
|
|
279
|
-
emit(eventName, data) { self._emit(eventName, data); },
|
|
280
|
-
tables() { return Array.from(self._tableNames); },
|
|
281
|
-
hasTable(name) { return self._tableNames.has(name); },
|
|
282
|
-
getTableSchema(name) { return self._schemas[name] || null; }
|
|
283
|
-
};
|
|
251
|
+
return buildPluginContext(this, plugin);
|
|
284
252
|
}
|
|
285
253
|
|
|
286
254
|
on(event, fn) {
|
|
287
|
-
|
|
288
|
-
this._hooks[event].push(fn);
|
|
289
|
-
}
|
|
290
|
-
return this;
|
|
255
|
+
return registerHook(this, event, fn);
|
|
291
256
|
}
|
|
292
257
|
|
|
293
258
|
onEvent(fn) {
|
|
@@ -295,6 +260,22 @@ class JSQL {
|
|
|
295
260
|
return this;
|
|
296
261
|
}
|
|
297
262
|
|
|
263
|
+
/** 广播事件(插件上下文 / 外部均可调用) */
|
|
264
|
+
emit(eventName, data) {
|
|
265
|
+
this._emit(eventName, data);
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** 主动触发一个钩子(含插件自定义钩子) */
|
|
270
|
+
runHook(hookName, ...args) {
|
|
271
|
+
return this._runHooks(hookName, args);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** 取插件对外暴露的 API(由 plugin.api 声明) */
|
|
275
|
+
plugin(name) {
|
|
276
|
+
return getPluginApis(this).get(name) || null;
|
|
277
|
+
}
|
|
278
|
+
|
|
298
279
|
_emit(eventName, data) {
|
|
299
280
|
for (const fn of this._eventListeners) {
|
|
300
281
|
try { fn(eventName, data); } catch (e) { /* ignore */ }
|
|
@@ -302,25 +283,7 @@ class JSQL {
|
|
|
302
283
|
}
|
|
303
284
|
|
|
304
285
|
_runHooks(hookName, args) {
|
|
305
|
-
|
|
306
|
-
if (!hooks || hooks.length === 0) return true;
|
|
307
|
-
if (this._inHook) {
|
|
308
|
-
// 防止插件 hook 内重入 native 调用导致 N-API 崩溃(段错误)
|
|
309
|
-
throw new Error('ER_PLUGIN_REENTRY: plugin hook "' + hookName + '" re-entered native call; plugin must not call engine methods inside its own hook');
|
|
310
|
-
}
|
|
311
|
-
this._inHook = true;
|
|
312
|
-
try {
|
|
313
|
-
for (const fn of hooks) {
|
|
314
|
-
const r = fn(...args);
|
|
315
|
-
if (r === false) return false;
|
|
316
|
-
if (r !== undefined && args.length > 0) {
|
|
317
|
-
args[0] = r;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
return true;
|
|
321
|
-
} finally {
|
|
322
|
-
this._inHook = false;
|
|
323
|
-
}
|
|
286
|
+
return runHooks(this, hookName, args);
|
|
324
287
|
}
|
|
325
288
|
|
|
326
289
|
async start() {
|
|
@@ -551,6 +514,17 @@ class JSQL {
|
|
|
551
514
|
return r;
|
|
552
515
|
}
|
|
553
516
|
|
|
517
|
+
async addColumn(table, name, def) {
|
|
518
|
+
await this._flush();
|
|
519
|
+
const mapped = mapNativeSchema({ [name]: def })[name];
|
|
520
|
+
const r = safeParse(native.jsqlAddColumn(table, name, JSON.stringify(mapped)));
|
|
521
|
+
if (r && r.ok === false) throw new Error(r.error || 'add column failed');
|
|
522
|
+
if (!this._schemas[table]) this._schemas[table] = {};
|
|
523
|
+
this._schemas[table][name] = mapped;
|
|
524
|
+
this._emit('addColumn', { table, name, def: mapped });
|
|
525
|
+
return r;
|
|
526
|
+
}
|
|
527
|
+
|
|
554
528
|
async dropTable(name) {
|
|
555
529
|
await this._flush();
|
|
556
530
|
if (!this._runHooks('beforeDropTable', [name])) return null;
|
|
@@ -676,7 +650,10 @@ class JSQL {
|
|
|
676
650
|
* await db.query("UPDATE users SET age = age + 1 WHERE name = 'A'");
|
|
677
651
|
*/
|
|
678
652
|
async query(sql, paramsOrOpts, opts) {
|
|
679
|
-
|
|
653
|
+
if (!this._runHooks('beforeQuery', [sql, paramsOrOpts, opts])) return null;
|
|
654
|
+
const result = await executeSQL(this, sql, paramsOrOpts, opts);
|
|
655
|
+
this._runHooks('afterQuery', [sql, paramsOrOpts, opts, result]);
|
|
656
|
+
return result;
|
|
680
657
|
}
|
|
681
658
|
|
|
682
659
|
async stop() {
|
package/lib/plugin.js
CHANGED
|
@@ -1,7 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSQL-NEO 可编程插件系统
|
|
3
|
+
*
|
|
4
|
+
* 统一的插件运行时,被 native / wasm / js 三个引擎共用,开放四类扩展点:
|
|
5
|
+
* 1. 生命周期钩子(HOOKS)——在引擎操作前后触发,返回 false 可中止操作;
|
|
6
|
+
* 2. 事件监听(onEvent / emit)——操作完成后广播;
|
|
7
|
+
* 3. 上下文接口(ctx)——插件拿到引擎能力、schema、日志、配置与私有存储;
|
|
8
|
+
* 4. 插件 API(api / db.plugin(name))——插件对外暴露可编程接口。
|
|
9
|
+
*
|
|
10
|
+
* 约定:钩子内不要调用会再次触发钩子的引擎方法(native 引擎会抛
|
|
11
|
+
* ER_PLUGIN_REENTRY 保护 N-API 不崩溃);需要修改数据时直接原地修改传入的
|
|
12
|
+
* 数组 / 对象,返回 undefined;返回 false 表示中止本次操作。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** 内置生命周期钩子名(自定义钩子名也会被自动登记,无需在此声明) */
|
|
16
|
+
const HOOKS = Object.freeze([
|
|
17
|
+
'beforeInsert', 'afterInsert',
|
|
18
|
+
'beforeUpdate', 'afterUpdate',
|
|
19
|
+
'beforeDelete', 'afterDelete',
|
|
20
|
+
'beforeFind', 'afterFind',
|
|
21
|
+
'beforeCreateTable', 'afterCreateTable',
|
|
22
|
+
'beforeDropTable', 'afterDropTable',
|
|
23
|
+
'beforeFlush', 'afterFlush',
|
|
24
|
+
'beforeCount', 'afterCount',
|
|
25
|
+
'beforeQuery', 'afterQuery',
|
|
26
|
+
'onStart', 'onStop'
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/** 创建一个预置了全部内置钩子的空注册表 */
|
|
30
|
+
function createHookRegistry() {
|
|
31
|
+
const reg = {};
|
|
32
|
+
for (const h of HOOKS) reg[h] = [];
|
|
33
|
+
return reg;
|
|
34
|
+
}
|
|
35
|
+
|
|
1
36
|
class Plugin {
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} name 插件名(用于 db.plugin(name) 取 API、私有存储隔离)
|
|
39
|
+
* @param {object} [opts]
|
|
40
|
+
* @param {string} [opts.version]
|
|
41
|
+
* @param {object} [opts.hooks] { hookName: fn | fn[] }
|
|
42
|
+
* @param {string[]} [opts.depends] 依赖的其它插件名
|
|
43
|
+
* @param {number} [opts.priority]
|
|
44
|
+
* @param {object} [opts.config] 传给插件的配置
|
|
45
|
+
* @param {object} [opts.api] 插件对外暴露的接口对象
|
|
46
|
+
* @param {function} [opts.onEvent]
|
|
47
|
+
* @param {function} [opts.install] install(engine, ctx)
|
|
48
|
+
*/
|
|
2
49
|
constructor(name, opts = {}) {
|
|
3
50
|
this.name = name;
|
|
51
|
+
this.version = opts.version || null;
|
|
4
52
|
this.hooks = opts.hooks || {};
|
|
53
|
+
this.depends = opts.depends || [];
|
|
54
|
+
this.priority = opts.priority || 0;
|
|
55
|
+
this.config = opts.config || {};
|
|
56
|
+
this.api = opts.api || null;
|
|
5
57
|
this._onEvent = opts.onEvent || null;
|
|
6
58
|
this._install = opts.install || null;
|
|
7
59
|
}
|
|
@@ -11,8 +63,11 @@ class Plugin {
|
|
|
11
63
|
return this;
|
|
12
64
|
}
|
|
13
65
|
|
|
66
|
+
/** 注册钩子;同名钩子可多次注册,按注册顺序执行 */
|
|
14
67
|
on(hook, fn) {
|
|
15
|
-
this.hooks[hook] =
|
|
68
|
+
if (!this.hooks[hook]) this.hooks[hook] = [];
|
|
69
|
+
if (!Array.isArray(this.hooks[hook])) this.hooks[hook] = [this.hooks[hook]];
|
|
70
|
+
this.hooks[hook].push(fn);
|
|
16
71
|
return this;
|
|
17
72
|
}
|
|
18
73
|
|
|
@@ -21,15 +76,184 @@ class Plugin {
|
|
|
21
76
|
return this;
|
|
22
77
|
}
|
|
23
78
|
|
|
79
|
+
/** 声明插件对外暴露的接口 */
|
|
80
|
+
expose(api) {
|
|
81
|
+
this.api = api;
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
24
85
|
build() {
|
|
25
86
|
const inst = {
|
|
26
87
|
name: this.name,
|
|
27
|
-
|
|
88
|
+
version: this.version,
|
|
89
|
+
hooks: this.hooks,
|
|
90
|
+
depends: this.depends,
|
|
91
|
+
priority: this.priority,
|
|
92
|
+
config: this.config
|
|
28
93
|
};
|
|
94
|
+
if (this.api) inst.api = this.api;
|
|
29
95
|
if (this._onEvent) inst.onEvent = this._onEvent;
|
|
30
96
|
if (this._install) inst.install = this._install;
|
|
31
97
|
return inst;
|
|
32
98
|
}
|
|
33
99
|
}
|
|
34
100
|
|
|
35
|
-
|
|
101
|
+
/** 以函数式风格定义插件:definePlugin('name', { hooks, api, install }) */
|
|
102
|
+
function definePlugin(name, def = {}) {
|
|
103
|
+
return new Plugin(name, def).build();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 登记单个钩子;未知钩子名会自动创建,便于插件自定义扩展点 */
|
|
107
|
+
function registerHook(engine, hook, fn) {
|
|
108
|
+
if (typeof fn !== 'function') return engine;
|
|
109
|
+
if (!engine._hooks) engine._hooks = createHookRegistry();
|
|
110
|
+
if (!engine._hooks[hook]) engine._hooks[hook] = [];
|
|
111
|
+
engine._hooks[hook].push(fn);
|
|
112
|
+
return engine;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 把一个插件声明的所有钩子登记到引擎上 */
|
|
116
|
+
function registerHooks(engine, plugin) {
|
|
117
|
+
if (!plugin || !plugin.hooks) return engine;
|
|
118
|
+
for (const [hook, val] of Object.entries(plugin.hooks)) {
|
|
119
|
+
const fns = Array.isArray(val) ? val : [val];
|
|
120
|
+
for (const fn of fns) registerHook(engine, hook, fn);
|
|
121
|
+
}
|
|
122
|
+
return engine;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 引擎 → 插件 API 注册表 */
|
|
126
|
+
function getPluginApis(engine) {
|
|
127
|
+
if (!engine._pluginApis) engine._pluginApis = new Map();
|
|
128
|
+
return engine._pluginApis;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 引擎 → 插件私有存储(按插件名隔离) */
|
|
132
|
+
function getPluginStore(engine, name) {
|
|
133
|
+
if (!engine._pluginStores) engine._pluginStores = new Map();
|
|
134
|
+
if (!engine._pluginStores.has(name)) engine._pluginStores.set(name, new Map());
|
|
135
|
+
return engine._pluginStores.get(name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 构建插件上下文。优先走引擎自身的公开方法,缺失时回退到约定的内部字段,
|
|
140
|
+
* 使三个引擎共享同一套插件 API。
|
|
141
|
+
*/
|
|
142
|
+
function buildPluginContext(engine, plugin) {
|
|
143
|
+
const name = plugin.name || 'anonymous';
|
|
144
|
+
const version = plugin.version || null;
|
|
145
|
+
const store = getPluginStore(engine, name);
|
|
146
|
+
const ctx = {
|
|
147
|
+
name,
|
|
148
|
+
version,
|
|
149
|
+
engine,
|
|
150
|
+
plugin,
|
|
151
|
+
config: plugin.config || {},
|
|
152
|
+
store,
|
|
153
|
+
/** 注册生命周期钩子 / 自定义钩子 */
|
|
154
|
+
on(hook, fn) { return engine.on(hook, fn); },
|
|
155
|
+
/** 注册事件监听 */
|
|
156
|
+
onEvent(fn) { return engine.onEvent(fn); },
|
|
157
|
+
/** 广播事件 */
|
|
158
|
+
emit(eventName, data) { return engine.emit(eventName, data); },
|
|
159
|
+
/** 主动触发一个钩子(含自定义钩子) */
|
|
160
|
+
hook(hookName, ...args) { return engine.runHook(hookName, ...args); },
|
|
161
|
+
/** 组合:加载另一个插件 */
|
|
162
|
+
use(other) { return engine.use(other); },
|
|
163
|
+
/** 表清单(同步) */
|
|
164
|
+
tables() {
|
|
165
|
+
if (engine._tableNames) return Array.from(engine._tableNames);
|
|
166
|
+
if (typeof engine.getTables === 'function') return engine.getTables();
|
|
167
|
+
return [];
|
|
168
|
+
},
|
|
169
|
+
/** 表是否存在 */
|
|
170
|
+
hasTable(t) {
|
|
171
|
+
if (typeof engine.hasTable === 'function') return engine.hasTable(t);
|
|
172
|
+
return !!(engine._tableNames && engine._tableNames.has(t));
|
|
173
|
+
},
|
|
174
|
+
/** 表 schema(同步,可能为 null) */
|
|
175
|
+
getTableSchema(t) {
|
|
176
|
+
if (engine._schemas && engine._schemas[t] !== undefined) return engine._schemas[t];
|
|
177
|
+
if (typeof engine.getTableSchema === 'function') return engine.getTableSchema(t);
|
|
178
|
+
return null;
|
|
179
|
+
},
|
|
180
|
+
/** 带插件名前缀的日志 */
|
|
181
|
+
log(...args) { console.log(`[jsql:${name}]`, ...args); },
|
|
182
|
+
/** 运行期声明插件 API */
|
|
183
|
+
expose(api) {
|
|
184
|
+
plugin.api = api;
|
|
185
|
+
getPluginApis(engine).set(name, api);
|
|
186
|
+
return api;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
return ctx;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 把一个插件应用到引擎:登记钩子、调用 install、挂载事件与 API。
|
|
194
|
+
* 引擎的 `use()` 只需 `return applyPlugin(this, plugin)`。
|
|
195
|
+
* @returns {object} engine(便于链式调用)
|
|
196
|
+
*/
|
|
197
|
+
function applyPlugin(engine, plugin, opts) {
|
|
198
|
+
// 字符串 → 内置插件名(懒加载,避免与 plugins/index 形成加载期循环依赖)
|
|
199
|
+
if (typeof plugin === 'string') {
|
|
200
|
+
plugin = require('./plugins').resolve(plugin, opts);
|
|
201
|
+
}
|
|
202
|
+
if (typeof plugin === 'function') plugin = { install: plugin };
|
|
203
|
+
if (!plugin || typeof plugin !== 'object') {
|
|
204
|
+
throw new Error('use: plugin must be an object, a function, or a builtin plugin name');
|
|
205
|
+
}
|
|
206
|
+
if (!engine._plugins) engine._plugins = [];
|
|
207
|
+
if (!engine._eventListeners) engine._eventListeners = [];
|
|
208
|
+
|
|
209
|
+
registerHooks(engine, plugin);
|
|
210
|
+
|
|
211
|
+
const ctx = buildPluginContext(engine, plugin);
|
|
212
|
+
if (typeof plugin.install === 'function') plugin.install(engine, ctx);
|
|
213
|
+
if (typeof plugin.onEvent === 'function') engine._eventListeners.push(plugin.onEvent);
|
|
214
|
+
|
|
215
|
+
engine._plugins.push(plugin);
|
|
216
|
+
if (plugin.name && plugin.api) getPluginApis(engine).set(plugin.name, plugin.api);
|
|
217
|
+
return engine;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 运行某个钩子链。
|
|
222
|
+
* - 任一钩子返回 false → 中止(返回 false),引擎据此跳过本次操作;
|
|
223
|
+
* - native 引擎置 `_forbidReentry`,钩子内再次触发引擎操作会抛 ER_PLUGIN_REENTRY。
|
|
224
|
+
*/
|
|
225
|
+
function runHooks(engine, hookName, args) {
|
|
226
|
+
const hooks = engine._hooks && engine._hooks[hookName];
|
|
227
|
+
if (!hooks || hooks.length === 0) return true;
|
|
228
|
+
|
|
229
|
+
const guard = !!engine._forbidReentry;
|
|
230
|
+
if (guard) {
|
|
231
|
+
if (engine._inHook) {
|
|
232
|
+
throw new Error('ER_PLUGIN_REENTRY: plugin hook "' + hookName +
|
|
233
|
+
'" re-entered native call; plugin must not call engine methods inside its own hook');
|
|
234
|
+
}
|
|
235
|
+
engine._inHook = true;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
for (const fn of hooks) {
|
|
239
|
+
if (fn(...args) === false) return false;
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
} finally {
|
|
243
|
+
if (guard) engine._inHook = false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = {
|
|
248
|
+
HOOKS,
|
|
249
|
+
Plugin,
|
|
250
|
+
definePlugin,
|
|
251
|
+
createHookRegistry,
|
|
252
|
+
registerHook,
|
|
253
|
+
registerHooks,
|
|
254
|
+
buildPluginContext,
|
|
255
|
+
applyPlugin,
|
|
256
|
+
runHooks,
|
|
257
|
+
getPluginApis,
|
|
258
|
+
getPluginStore
|
|
259
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* audit —— 记录数据变更审计日志(内存环形缓冲)。
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* db.use(require('jsql-neo/lib/plugins').audit({ limit: 500 }));
|
|
6
|
+
* db.plugin('audit').entries(); // [{ at, op, table, count }]
|
|
7
|
+
*/
|
|
8
|
+
const { definePlugin } = require('../plugin');
|
|
9
|
+
|
|
10
|
+
function createAudit(opts = {}) {
|
|
11
|
+
const limit = opts.limit || 1000;
|
|
12
|
+
const entries = [];
|
|
13
|
+
|
|
14
|
+
const push = e => {
|
|
15
|
+
entries.push({ at: Date.now(), ...e });
|
|
16
|
+
if (entries.length > limit) entries.splice(0, entries.length - limit);
|
|
17
|
+
};
|
|
18
|
+
const rowCount = rows => (Array.isArray(rows) ? rows.length : 1);
|
|
19
|
+
|
|
20
|
+
return definePlugin('audit', {
|
|
21
|
+
config: { limit },
|
|
22
|
+
api: {
|
|
23
|
+
entries: () => entries.map(e => ({ ...e })),
|
|
24
|
+
size: () => entries.length,
|
|
25
|
+
clear: () => { entries.length = 0; }
|
|
26
|
+
},
|
|
27
|
+
hooks: {
|
|
28
|
+
afterInsert(table, rows) { push({ op: 'insert', table, count: rowCount(rows) }); },
|
|
29
|
+
afterUpdate(table) { push({ op: 'update', table }); },
|
|
30
|
+
afterDelete(table) { push({ op: 'delete', table }); },
|
|
31
|
+
afterCreateTable(name) { push({ op: 'createTable', table: name }); },
|
|
32
|
+
afterDropTable(name) { push({ op: 'dropTable', table: name }); }
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { createAudit, audit: createAudit };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSQL-NEO 内置插件集
|
|
3
|
+
*
|
|
4
|
+
* 每个插件都是「工厂函数」,调用后返回可交给 `db.use()` 的插件对象。
|
|
5
|
+
* 也可以直接用名字加载:`db.use('logger')` / `db.use('timestamps', { ... })`。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const plugins = require('jsql-neo/lib/plugins');
|
|
9
|
+
* db.use(plugins.timestamps());
|
|
10
|
+
* db.use(plugins.logger({ logger: myLog }));
|
|
11
|
+
* db.use(plugins.metrics());
|
|
12
|
+
* const audit = db.plugin('audit');
|
|
13
|
+
*/
|
|
14
|
+
const { createTimestamps } = require('./timestamps');
|
|
15
|
+
const { createLogger } = require('./logger');
|
|
16
|
+
const { createMetrics } = require('./metrics');
|
|
17
|
+
const { createAudit } = require('./audit');
|
|
18
|
+
const { createValidation } = require('./validation');
|
|
19
|
+
|
|
20
|
+
/** 名称 → 插件工厂 */
|
|
21
|
+
const builtins = {
|
|
22
|
+
timestamps: createTimestamps,
|
|
23
|
+
logger: createLogger,
|
|
24
|
+
metrics: createMetrics,
|
|
25
|
+
audit: createAudit,
|
|
26
|
+
validation: createValidation
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** 内置插件名清单 */
|
|
30
|
+
function list() {
|
|
31
|
+
return Object.keys(builtins);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 按名字构造内置插件。
|
|
36
|
+
* @param {string} name
|
|
37
|
+
* @param {object} [opts]
|
|
38
|
+
* @returns {object} 插件对象,可直接 db.use()
|
|
39
|
+
*/
|
|
40
|
+
function create(name, opts = {}) {
|
|
41
|
+
const factory = builtins[name];
|
|
42
|
+
if (!factory) {
|
|
43
|
+
throw new Error(`Unknown builtin plugin: ${name}. Available: ${list().join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
return factory(opts);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 名称或插件对象 → 插件对象。供引擎的 `use()` 解析字符串用。
|
|
50
|
+
* 插件对象原样返回;字符串走内置注册表。
|
|
51
|
+
*/
|
|
52
|
+
function resolve(nameOrPlugin, opts) {
|
|
53
|
+
if (typeof nameOrPlugin === 'string') return create(nameOrPlugin, opts);
|
|
54
|
+
return nameOrPlugin;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
createTimestamps,
|
|
59
|
+
createLogger,
|
|
60
|
+
createMetrics,
|
|
61
|
+
createAudit,
|
|
62
|
+
createValidation,
|
|
63
|
+
timestamps: createTimestamps,
|
|
64
|
+
logger: createLogger,
|
|
65
|
+
metrics: createMetrics,
|
|
66
|
+
audit: createAudit,
|
|
67
|
+
validation: createValidation,
|
|
68
|
+
builtins,
|
|
69
|
+
list,
|
|
70
|
+
create,
|
|
71
|
+
resolve
|
|
72
|
+
};
|