jsql-neo 5.4.2 → 5.5.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/LICENSE +202 -202
- package/README.md +8097 -7971
- package/bin/jsql +320 -320
- package/bin/jsql-server +90 -90
- package/bin/jsql-server-control.js +177 -177
- package/index.d.ts +385 -357
- package/index.js +124 -118
- package/lib/ast.js +214 -0
- package/lib/btree.js +413 -413
- package/lib/cache.js +328 -328
- package/lib/client.js +125 -125
- package/lib/database.js +1744 -1744
- package/lib/date-types.js +182 -182
- package/lib/errors.js +89 -89
- package/lib/jsql_format.js +497 -497
- package/lib/migrate.js +254 -254
- package/lib/mod.js +292 -292
- package/lib/mongo_server.js +607 -607
- package/lib/multiserver.js +140 -140
- package/lib/mysql_compat.js +413 -413
- package/lib/mysql_server.js +1237 -1237
- package/lib/native_client.js +732 -732
- package/lib/nedb_compat.js +506 -506
- package/lib/pg_server.js +864 -864
- package/lib/plugin.js +34 -34
- package/lib/query.js +815 -815
- package/lib/redis_server.js +571 -571
- package/lib/sql.js +3460 -2925
- package/lib/sqlite_compat.js +323 -323
- package/lib/sqlite_worker.js +264 -264
- package/lib/table.js +1304 -1304
- package/lib/tui.js +502 -502
- package/lib/wasm.js +1 -1
- package/lib/wasm_client.js +718 -718
- package/lib/web_ui.js +257 -257
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +455 -455
- package/nativesrc/jsql-neo-core/Cargo.toml +24 -24
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +455 -455
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +168 -168
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +62 -62
- package/nativesrc/jsql-neo-core/src/engine/table.rs +700 -700
- package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
- package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
- package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -85
- package/nativesrc/jsql-neo-core/src/types.rs +93 -93
- package/nativesrc/jsql-neo-native/Cargo.lock +606 -606
- package/nativesrc/jsql-neo-native/Cargo.toml +16 -16
- package/nativesrc/jsql-neo-native/build.rs +3 -3
- package/nativesrc/jsql-neo-native/package.json +7 -7
- package/nativesrc/jsql-neo-native/src/lib.rs +269 -269
- package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -465
- package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -15
- package/nativesrc/jsql-neo-wasm/src/lib.rs +447 -447
- package/package.json +87 -86
- package/postinstall.js +158 -158
- package/test/btree.test.js +128 -128
- package/test/coverage.js +187 -187
- package/test/join.test.js +110 -110
- package/test/native.test.js +158 -158
- package/test/regress-5.1.0.js +271 -271
- package/test/smoke.js +58 -58
- package/test/sql-parser.test.js +273 -0
- package/test/wasm.test.js +110 -110
- package/wasm/browser.d.ts +89 -89
- package/wasm/browser.mjs +569 -569
- package/wasm/browser_bg.mjs +597 -597
- package/wasm/jsql_neo_wasm.d.ts +38 -38
- package/wasm/jsql_neo_wasm.js +520 -520
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
- package/wasm/package.json +11 -11
package/lib/mod.js
CHANGED
|
@@ -1,292 +1,292 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const Module = require('module');
|
|
5
|
-
|
|
6
|
-
const DEFAULT_CONFIG_PATH = path.join(os.homedir(), '.config', 'jsql', 'mod.config');
|
|
7
|
-
const CONFIG_PATH = process.env.JSQL_MOD_CONFIG || DEFAULT_CONFIG_PATH;
|
|
8
|
-
|
|
9
|
-
class ModApi {
|
|
10
|
-
constructor(collector) {
|
|
11
|
-
this._collector = collector;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
nameset(name) {
|
|
15
|
-
if (typeof name !== 'string' || !name) {
|
|
16
|
-
throw new Error('nameset: module name must be a non-empty string');
|
|
17
|
-
}
|
|
18
|
-
if (this._collector.nameSet) {
|
|
19
|
-
throw new Error('nameset: module name already declared');
|
|
20
|
-
}
|
|
21
|
-
this._collector.name = name;
|
|
22
|
-
this._collector.nameSet = true;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
priority(n) {
|
|
26
|
-
this._collector.priority = n;
|
|
27
|
-
return this;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
depends(name) {
|
|
31
|
-
if (typeof name === 'string') name = [name];
|
|
32
|
-
for (const d of name) {
|
|
33
|
-
if (typeof d !== 'string' || !d) {
|
|
34
|
-
throw new Error('depends: dependency names must be non-empty strings');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
this._collector.depends = [...(this._collector.depends || []), ...name];
|
|
38
|
-
return this;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
api(obj) {
|
|
42
|
-
if (typeof obj !== 'object' || obj === null) {
|
|
43
|
-
throw new Error('api: must be an object');
|
|
44
|
-
}
|
|
45
|
-
this._collector.api = obj;
|
|
46
|
-
return this;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
on(hook, fn) {
|
|
50
|
-
if (typeof fn !== 'function') throw new Error('on: hook must be a function');
|
|
51
|
-
if (!this._collector.hooks[hook]) this._collector.hooks[hook] = [];
|
|
52
|
-
this._collector.hooks[hook].push(fn);
|
|
53
|
-
return this;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
onEvent(fn) {
|
|
57
|
-
if (typeof fn !== 'function') throw new Error('onEvent: must be a function');
|
|
58
|
-
this._collector.onEvent = fn;
|
|
59
|
-
return this;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
install(fn) {
|
|
63
|
-
if (typeof fn !== 'function') throw new Error('install: must be a function');
|
|
64
|
-
this._collector.install = fn;
|
|
65
|
-
return this;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
class ModuleManager {
|
|
70
|
-
constructor(configPath = CONFIG_PATH) {
|
|
71
|
-
this._configPath = configPath;
|
|
72
|
-
this._modules = [];
|
|
73
|
-
this._load();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
_load() {
|
|
77
|
-
try {
|
|
78
|
-
if (!fs.existsSync(this._configPath)) return;
|
|
79
|
-
const data = JSON.parse(fs.readFileSync(this._configPath, 'utf8'));
|
|
80
|
-
if (Array.isArray(data)) {
|
|
81
|
-
this._modules = data;
|
|
82
|
-
} else if (Array.isArray(data.modules)) {
|
|
83
|
-
this._modules = data.modules;
|
|
84
|
-
}
|
|
85
|
-
} catch (e) {
|
|
86
|
-
this._modules = [];
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
save() {
|
|
91
|
-
const dir = path.dirname(this._configPath);
|
|
92
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
93
|
-
fs.writeFileSync(this._configPath, JSON.stringify({ modules: this._modules }, null, 2), 'utf8');
|
|
94
|
-
try { fs.chmodSync(this._configPath, 0o600); } catch (e) {}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
list() {
|
|
98
|
-
return this._modules.map(m => ({ ...m }));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
find(name) {
|
|
102
|
-
return this._modules.find(m => m.name === name) || null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
add(address, opts = {}) {
|
|
106
|
-
const resolved = path.resolve(address);
|
|
107
|
-
if (!fs.existsSync(resolved)) {
|
|
108
|
-
throw new Error(`Module file not found: ${resolved}`);
|
|
109
|
-
}
|
|
110
|
-
if (fs.statSync(resolved).isDirectory()) {
|
|
111
|
-
throw new Error(`Expected a module file, got directory: ${resolved}`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const plugin = this._loadModuleFile(resolved);
|
|
115
|
-
const name = plugin.name;
|
|
116
|
-
|
|
117
|
-
if (this.find(name)) {
|
|
118
|
-
throw new Error(`Module already registered: ${name}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const entry = {
|
|
122
|
-
name,
|
|
123
|
-
path: resolved,
|
|
124
|
-
enabled: false,
|
|
125
|
-
priority: plugin.priority || 0,
|
|
126
|
-
depends: plugin.depends || [],
|
|
127
|
-
addedAt: Date.now()
|
|
128
|
-
};
|
|
129
|
-
this._modules.push(entry);
|
|
130
|
-
this.save();
|
|
131
|
-
return { ...entry };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
enable(name, opts = {}) {
|
|
135
|
-
const m = this.find(name);
|
|
136
|
-
if (!m) throw new Error(`Module not found: ${name}. Use 'jsql mod add --address <path>' first.`);
|
|
137
|
-
if (m.enabled) return { ...m, ok: true, message: 'already enabled' };
|
|
138
|
-
|
|
139
|
-
const enabling = this._enablingStack || (this._enablingStack = new Set());
|
|
140
|
-
if (enabling.has(name)) throw new Error(`Circular module dependency: ${[...enabling, name].join(' -> ')}`);
|
|
141
|
-
enabling.add(name);
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
const plugin = this._loadModuleFile(m.path);
|
|
145
|
-
|
|
146
|
-
if (plugin.depends && plugin.depends.length > 0) {
|
|
147
|
-
for (const dep of plugin.depends) {
|
|
148
|
-
const depMod = this.find(dep);
|
|
149
|
-
if (!depMod) throw new Error(`Module '${name}' depends on '${dep}' which is not registered`);
|
|
150
|
-
if (!depMod.enabled && opts.withDeps !== false) {
|
|
151
|
-
this.enable(dep, opts);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
m.enabled = true;
|
|
157
|
-
m.priority = plugin.priority || m.priority || 0;
|
|
158
|
-
this.save();
|
|
159
|
-
return { ...m, ok: true };
|
|
160
|
-
} finally {
|
|
161
|
-
enabling.delete(name);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
disable(name) {
|
|
166
|
-
const m = this.find(name);
|
|
167
|
-
if (!m) throw new Error(`Module not found: ${name}`);
|
|
168
|
-
if (!m.enabled) return { ...m, ok: true, message: 'already disabled' };
|
|
169
|
-
for (const other of this._modules) {
|
|
170
|
-
const plugin = this._loadModuleFile(other.path);
|
|
171
|
-
if (plugin.depends && plugin.depends.includes(name) && other.enabled) {
|
|
172
|
-
this.disable(other.name);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
m.enabled = false;
|
|
176
|
-
this.save();
|
|
177
|
-
return { ...m, ok: true };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
enableAll() {
|
|
181
|
-
const names = this._modules.map(m => m.name);
|
|
182
|
-
for (const name of names) {
|
|
183
|
-
try { this.enable(name); } catch (e) { /* skip failed */ }
|
|
184
|
-
}
|
|
185
|
-
this.save();
|
|
186
|
-
return this.list().filter(m => m.enabled).length;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
disableAll() {
|
|
190
|
-
for (const m of this._modules) m.enabled = false;
|
|
191
|
-
this.save();
|
|
192
|
-
return this._modules.length;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
applyTo(db) {
|
|
196
|
-
const enabled = this._sortEnabled();
|
|
197
|
-
for (const entry of enabled) {
|
|
198
|
-
const plugin = this._loadModuleFile(entry.path);
|
|
199
|
-
db.use(plugin);
|
|
200
|
-
}
|
|
201
|
-
return db;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
api(name) {
|
|
205
|
-
const m = this.find(name);
|
|
206
|
-
if (!m) throw new Error(`Module not found: ${name}`);
|
|
207
|
-
const plugin = this._loadModuleFile(m.path);
|
|
208
|
-
return plugin.api || null;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
_sortEnabled() {
|
|
212
|
-
const enabled = this._modules.filter(m => m.enabled).map(m => ({ ...m }));
|
|
213
|
-
const resolved = new Set();
|
|
214
|
-
const order = [];
|
|
215
|
-
const cache = new Map();
|
|
216
|
-
|
|
217
|
-
const resolve = (name, chain) => {
|
|
218
|
-
if (resolved.has(name)) return 0;
|
|
219
|
-
const entry = enabled.find(m => m.name === name);
|
|
220
|
-
if (!entry) return 0;
|
|
221
|
-
if (chain.has(name)) throw new Error(`Circular module dependency: ${[...chain, name].join(' -> ')}`);
|
|
222
|
-
|
|
223
|
-
let plugin;
|
|
224
|
-
if (cache.has(entry.path)) {
|
|
225
|
-
plugin = cache.get(entry.path);
|
|
226
|
-
} else {
|
|
227
|
-
plugin = this._loadModuleFile(entry.path);
|
|
228
|
-
cache.set(entry.path, plugin);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
let maxDepDepth = -1;
|
|
232
|
-
for (const dep of (plugin.depends || [])) {
|
|
233
|
-
const d = resolve(dep, new Set([...chain, name]));
|
|
234
|
-
maxDepDepth = Math.max(maxDepDepth, d);
|
|
235
|
-
}
|
|
236
|
-
resolved.add(name);
|
|
237
|
-
order.push({ entry, depth: maxDepDepth + 1 });
|
|
238
|
-
return maxDepDepth + 1;
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
for (const entry of enabled) resolve(entry.name, new Set());
|
|
242
|
-
|
|
243
|
-
// 依赖深度优先,同层按 priority 排序:保证被依赖方始终先应用
|
|
244
|
-
order.sort((a, b) => a.depth - b.depth || (a.entry.priority || 0) - (b.entry.priority || 0));
|
|
245
|
-
return order.map(o => o.entry);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
remove(name) {
|
|
249
|
-
const idx = this._modules.findIndex(m => m.name === name);
|
|
250
|
-
if (idx === -1) throw new Error(`Module not found: ${name}`);
|
|
251
|
-
const [removed] = this._modules.splice(idx, 1);
|
|
252
|
-
this.save();
|
|
253
|
-
return removed;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
get(name) {
|
|
257
|
-
const m = this.find(name);
|
|
258
|
-
if (!m) throw new Error(`Module not found: ${name}`);
|
|
259
|
-
return this._loadModuleFile(m.path);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
_loadModuleFile(filePath) {
|
|
263
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
264
|
-
|
|
265
|
-
if (ext === '.json') {
|
|
266
|
-
const obj = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
267
|
-
return {
|
|
268
|
-
name: obj.name || path.basename(filePath, ext),
|
|
269
|
-
...obj
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// JS 模块使用 Node.js require 直接加载(信任模型——模块由管理员通过 CLI 管理,
|
|
274
|
-
// 与 npm 包模型相同,无安全隔离假设)。
|
|
275
|
-
// 如需防止非预期代码执行,请使用 JSON-only 模块(只声明 hooks/api 无代码执行能力)。
|
|
276
|
-
const resolved = path.isAbsolute(filePath) ? filePath : require.resolve(filePath);
|
|
277
|
-
delete require.cache[resolved];
|
|
278
|
-
let plugin;
|
|
279
|
-
try {
|
|
280
|
-
plugin = require(filePath);
|
|
281
|
-
} catch (e) {
|
|
282
|
-
throw new Error(`Failed to load module ${filePath}: ${e.message}`);
|
|
283
|
-
}
|
|
284
|
-
if (typeof plugin === 'function') {
|
|
285
|
-
return { install: plugin, name: path.basename(filePath, ext) };
|
|
286
|
-
}
|
|
287
|
-
if (!plugin.name) plugin.name = path.basename(filePath, ext);
|
|
288
|
-
return plugin;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
module.exports = { ModuleManager, ModApi, CONFIG_PATH };
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const Module = require('module');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONFIG_PATH = path.join(os.homedir(), '.config', 'jsql', 'mod.config');
|
|
7
|
+
const CONFIG_PATH = process.env.JSQL_MOD_CONFIG || DEFAULT_CONFIG_PATH;
|
|
8
|
+
|
|
9
|
+
class ModApi {
|
|
10
|
+
constructor(collector) {
|
|
11
|
+
this._collector = collector;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
nameset(name) {
|
|
15
|
+
if (typeof name !== 'string' || !name) {
|
|
16
|
+
throw new Error('nameset: module name must be a non-empty string');
|
|
17
|
+
}
|
|
18
|
+
if (this._collector.nameSet) {
|
|
19
|
+
throw new Error('nameset: module name already declared');
|
|
20
|
+
}
|
|
21
|
+
this._collector.name = name;
|
|
22
|
+
this._collector.nameSet = true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
priority(n) {
|
|
26
|
+
this._collector.priority = n;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
depends(name) {
|
|
31
|
+
if (typeof name === 'string') name = [name];
|
|
32
|
+
for (const d of name) {
|
|
33
|
+
if (typeof d !== 'string' || !d) {
|
|
34
|
+
throw new Error('depends: dependency names must be non-empty strings');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
this._collector.depends = [...(this._collector.depends || []), ...name];
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
api(obj) {
|
|
42
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
43
|
+
throw new Error('api: must be an object');
|
|
44
|
+
}
|
|
45
|
+
this._collector.api = obj;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
on(hook, fn) {
|
|
50
|
+
if (typeof fn !== 'function') throw new Error('on: hook must be a function');
|
|
51
|
+
if (!this._collector.hooks[hook]) this._collector.hooks[hook] = [];
|
|
52
|
+
this._collector.hooks[hook].push(fn);
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onEvent(fn) {
|
|
57
|
+
if (typeof fn !== 'function') throw new Error('onEvent: must be a function');
|
|
58
|
+
this._collector.onEvent = fn;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
install(fn) {
|
|
63
|
+
if (typeof fn !== 'function') throw new Error('install: must be a function');
|
|
64
|
+
this._collector.install = fn;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class ModuleManager {
|
|
70
|
+
constructor(configPath = CONFIG_PATH) {
|
|
71
|
+
this._configPath = configPath;
|
|
72
|
+
this._modules = [];
|
|
73
|
+
this._load();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_load() {
|
|
77
|
+
try {
|
|
78
|
+
if (!fs.existsSync(this._configPath)) return;
|
|
79
|
+
const data = JSON.parse(fs.readFileSync(this._configPath, 'utf8'));
|
|
80
|
+
if (Array.isArray(data)) {
|
|
81
|
+
this._modules = data;
|
|
82
|
+
} else if (Array.isArray(data.modules)) {
|
|
83
|
+
this._modules = data.modules;
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
this._modules = [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
save() {
|
|
91
|
+
const dir = path.dirname(this._configPath);
|
|
92
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
93
|
+
fs.writeFileSync(this._configPath, JSON.stringify({ modules: this._modules }, null, 2), 'utf8');
|
|
94
|
+
try { fs.chmodSync(this._configPath, 0o600); } catch (e) {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
list() {
|
|
98
|
+
return this._modules.map(m => ({ ...m }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
find(name) {
|
|
102
|
+
return this._modules.find(m => m.name === name) || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
add(address, opts = {}) {
|
|
106
|
+
const resolved = path.resolve(address);
|
|
107
|
+
if (!fs.existsSync(resolved)) {
|
|
108
|
+
throw new Error(`Module file not found: ${resolved}`);
|
|
109
|
+
}
|
|
110
|
+
if (fs.statSync(resolved).isDirectory()) {
|
|
111
|
+
throw new Error(`Expected a module file, got directory: ${resolved}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const plugin = this._loadModuleFile(resolved);
|
|
115
|
+
const name = plugin.name;
|
|
116
|
+
|
|
117
|
+
if (this.find(name)) {
|
|
118
|
+
throw new Error(`Module already registered: ${name}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const entry = {
|
|
122
|
+
name,
|
|
123
|
+
path: resolved,
|
|
124
|
+
enabled: false,
|
|
125
|
+
priority: plugin.priority || 0,
|
|
126
|
+
depends: plugin.depends || [],
|
|
127
|
+
addedAt: Date.now()
|
|
128
|
+
};
|
|
129
|
+
this._modules.push(entry);
|
|
130
|
+
this.save();
|
|
131
|
+
return { ...entry };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
enable(name, opts = {}) {
|
|
135
|
+
const m = this.find(name);
|
|
136
|
+
if (!m) throw new Error(`Module not found: ${name}. Use 'jsql mod add --address <path>' first.`);
|
|
137
|
+
if (m.enabled) return { ...m, ok: true, message: 'already enabled' };
|
|
138
|
+
|
|
139
|
+
const enabling = this._enablingStack || (this._enablingStack = new Set());
|
|
140
|
+
if (enabling.has(name)) throw new Error(`Circular module dependency: ${[...enabling, name].join(' -> ')}`);
|
|
141
|
+
enabling.add(name);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const plugin = this._loadModuleFile(m.path);
|
|
145
|
+
|
|
146
|
+
if (plugin.depends && plugin.depends.length > 0) {
|
|
147
|
+
for (const dep of plugin.depends) {
|
|
148
|
+
const depMod = this.find(dep);
|
|
149
|
+
if (!depMod) throw new Error(`Module '${name}' depends on '${dep}' which is not registered`);
|
|
150
|
+
if (!depMod.enabled && opts.withDeps !== false) {
|
|
151
|
+
this.enable(dep, opts);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
m.enabled = true;
|
|
157
|
+
m.priority = plugin.priority || m.priority || 0;
|
|
158
|
+
this.save();
|
|
159
|
+
return { ...m, ok: true };
|
|
160
|
+
} finally {
|
|
161
|
+
enabling.delete(name);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
disable(name) {
|
|
166
|
+
const m = this.find(name);
|
|
167
|
+
if (!m) throw new Error(`Module not found: ${name}`);
|
|
168
|
+
if (!m.enabled) return { ...m, ok: true, message: 'already disabled' };
|
|
169
|
+
for (const other of this._modules) {
|
|
170
|
+
const plugin = this._loadModuleFile(other.path);
|
|
171
|
+
if (plugin.depends && plugin.depends.includes(name) && other.enabled) {
|
|
172
|
+
this.disable(other.name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
m.enabled = false;
|
|
176
|
+
this.save();
|
|
177
|
+
return { ...m, ok: true };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
enableAll() {
|
|
181
|
+
const names = this._modules.map(m => m.name);
|
|
182
|
+
for (const name of names) {
|
|
183
|
+
try { this.enable(name); } catch (e) { /* skip failed */ }
|
|
184
|
+
}
|
|
185
|
+
this.save();
|
|
186
|
+
return this.list().filter(m => m.enabled).length;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
disableAll() {
|
|
190
|
+
for (const m of this._modules) m.enabled = false;
|
|
191
|
+
this.save();
|
|
192
|
+
return this._modules.length;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
applyTo(db) {
|
|
196
|
+
const enabled = this._sortEnabled();
|
|
197
|
+
for (const entry of enabled) {
|
|
198
|
+
const plugin = this._loadModuleFile(entry.path);
|
|
199
|
+
db.use(plugin);
|
|
200
|
+
}
|
|
201
|
+
return db;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
api(name) {
|
|
205
|
+
const m = this.find(name);
|
|
206
|
+
if (!m) throw new Error(`Module not found: ${name}`);
|
|
207
|
+
const plugin = this._loadModuleFile(m.path);
|
|
208
|
+
return plugin.api || null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_sortEnabled() {
|
|
212
|
+
const enabled = this._modules.filter(m => m.enabled).map(m => ({ ...m }));
|
|
213
|
+
const resolved = new Set();
|
|
214
|
+
const order = [];
|
|
215
|
+
const cache = new Map();
|
|
216
|
+
|
|
217
|
+
const resolve = (name, chain) => {
|
|
218
|
+
if (resolved.has(name)) return 0;
|
|
219
|
+
const entry = enabled.find(m => m.name === name);
|
|
220
|
+
if (!entry) return 0;
|
|
221
|
+
if (chain.has(name)) throw new Error(`Circular module dependency: ${[...chain, name].join(' -> ')}`);
|
|
222
|
+
|
|
223
|
+
let plugin;
|
|
224
|
+
if (cache.has(entry.path)) {
|
|
225
|
+
plugin = cache.get(entry.path);
|
|
226
|
+
} else {
|
|
227
|
+
plugin = this._loadModuleFile(entry.path);
|
|
228
|
+
cache.set(entry.path, plugin);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let maxDepDepth = -1;
|
|
232
|
+
for (const dep of (plugin.depends || [])) {
|
|
233
|
+
const d = resolve(dep, new Set([...chain, name]));
|
|
234
|
+
maxDepDepth = Math.max(maxDepDepth, d);
|
|
235
|
+
}
|
|
236
|
+
resolved.add(name);
|
|
237
|
+
order.push({ entry, depth: maxDepDepth + 1 });
|
|
238
|
+
return maxDepDepth + 1;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
for (const entry of enabled) resolve(entry.name, new Set());
|
|
242
|
+
|
|
243
|
+
// 依赖深度优先,同层按 priority 排序:保证被依赖方始终先应用
|
|
244
|
+
order.sort((a, b) => a.depth - b.depth || (a.entry.priority || 0) - (b.entry.priority || 0));
|
|
245
|
+
return order.map(o => o.entry);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
remove(name) {
|
|
249
|
+
const idx = this._modules.findIndex(m => m.name === name);
|
|
250
|
+
if (idx === -1) throw new Error(`Module not found: ${name}`);
|
|
251
|
+
const [removed] = this._modules.splice(idx, 1);
|
|
252
|
+
this.save();
|
|
253
|
+
return removed;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
get(name) {
|
|
257
|
+
const m = this.find(name);
|
|
258
|
+
if (!m) throw new Error(`Module not found: ${name}`);
|
|
259
|
+
return this._loadModuleFile(m.path);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_loadModuleFile(filePath) {
|
|
263
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
264
|
+
|
|
265
|
+
if (ext === '.json') {
|
|
266
|
+
const obj = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
267
|
+
return {
|
|
268
|
+
name: obj.name || path.basename(filePath, ext),
|
|
269
|
+
...obj
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// JS 模块使用 Node.js require 直接加载(信任模型——模块由管理员通过 CLI 管理,
|
|
274
|
+
// 与 npm 包模型相同,无安全隔离假设)。
|
|
275
|
+
// 如需防止非预期代码执行,请使用 JSON-only 模块(只声明 hooks/api 无代码执行能力)。
|
|
276
|
+
const resolved = path.isAbsolute(filePath) ? filePath : require.resolve(filePath);
|
|
277
|
+
delete require.cache[resolved];
|
|
278
|
+
let plugin;
|
|
279
|
+
try {
|
|
280
|
+
plugin = require(filePath);
|
|
281
|
+
} catch (e) {
|
|
282
|
+
throw new Error(`Failed to load module ${filePath}: ${e.message}`);
|
|
283
|
+
}
|
|
284
|
+
if (typeof plugin === 'function') {
|
|
285
|
+
return { install: plugin, name: path.basename(filePath, ext) };
|
|
286
|
+
}
|
|
287
|
+
if (!plugin.name) plugin.name = path.basename(filePath, ext);
|
|
288
|
+
return plugin;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
module.exports = { ModuleManager, ModApi, CONFIG_PATH };
|