jsql-neo 4.0.2 → 4.1.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/lib/mod.js CHANGED
@@ -2,7 +2,6 @@ const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
4
  const Module = require('module');
5
- const vm = require('vm');
6
5
 
7
6
  const DEFAULT_CONFIG_PATH = path.join(os.homedir(), '.config', 'jsql', 'mod.config');
8
7
  const CONFIG_PATH = process.env.JSQL_MOD_CONFIG || DEFAULT_CONFIG_PATH;
@@ -90,8 +89,9 @@ class ModuleManager {
90
89
 
91
90
  save() {
92
91
  const dir = path.dirname(this._configPath);
93
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
92
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
94
93
  fs.writeFileSync(this._configPath, JSON.stringify({ modules: this._modules }, null, 2), 'utf8');
94
+ try { fs.chmodSync(this._configPath, 0o600); } catch (e) {}
95
95
  }
96
96
 
97
97
  list() {
@@ -102,7 +102,7 @@ class ModuleManager {
102
102
  return this._modules.find(m => m.name === name) || null;
103
103
  }
104
104
 
105
- add(address) {
105
+ add(address, opts = {}) {
106
106
  const resolved = path.resolve(address);
107
107
  if (!fs.existsSync(resolved)) {
108
108
  throw new Error(`Module file not found: ${resolved}`);
@@ -136,22 +136,30 @@ class ModuleManager {
136
136
  if (!m) throw new Error(`Module not found: ${name}. Use 'jsql mod add --address <path>' first.`);
137
137
  if (m.enabled) return { ...m, ok: true, message: 'already enabled' };
138
138
 
139
- const plugin = this._loadModuleFile(m.path);
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);
140
142
 
141
- if (plugin.depends && plugin.depends.length > 0) {
142
- for (const dep of plugin.depends) {
143
- const depMod = this.find(dep);
144
- if (!depMod) throw new Error(`Module '${name}' depends on '${dep}' which is not registered`);
145
- if (!depMod.enabled && opts.withDeps !== false) {
146
- this.enable(dep, opts);
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
+ }
147
153
  }
148
154
  }
149
- }
150
155
 
151
- m.enabled = true;
152
- m.priority = plugin.priority || m.priority || 0;
153
- this.save();
154
- return { ...m, ok: true };
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
+ }
155
163
  }
156
164
 
157
165
  disable(name) {
@@ -207,9 +215,9 @@ class ModuleManager {
207
215
  const cache = new Map();
208
216
 
209
217
  const resolve = (name, chain) => {
210
- if (resolved.has(name)) return;
218
+ if (resolved.has(name)) return 0;
211
219
  const entry = enabled.find(m => m.name === name);
212
- if (!entry) return;
220
+ if (!entry) return 0;
213
221
  if (chain.has(name)) throw new Error(`Circular module dependency: ${[...chain, name].join(' -> ')}`);
214
222
 
215
223
  let plugin;
@@ -220,17 +228,21 @@ class ModuleManager {
220
228
  cache.set(entry.path, plugin);
221
229
  }
222
230
 
231
+ let maxDepDepth = -1;
223
232
  for (const dep of (plugin.depends || [])) {
224
- resolve(dep, new Set([...chain, name]));
233
+ const d = resolve(dep, new Set([...chain, name]));
234
+ maxDepDepth = Math.max(maxDepDepth, d);
225
235
  }
226
236
  resolved.add(name);
227
- order.push(entry);
237
+ order.push({ entry, depth: maxDepDepth + 1 });
238
+ return maxDepDepth + 1;
228
239
  };
229
240
 
230
241
  for (const entry of enabled) resolve(entry.name, new Set());
231
242
 
232
- order.sort((a, b) => (a.priority || 0) - (b.priority || 0));
233
- return order;
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);
234
246
  }
235
247
 
236
248
  remove(name) {
@@ -258,57 +270,21 @@ class ModuleManager {
258
270
  };
259
271
  }
260
272
 
261
- const code = fs.readFileSync(filePath, 'utf8');
262
- const collector = { name: null, nameSet: false, hooks: {} };
263
- const sandboxModule = new Module(filePath, module);
264
- sandboxModule.filename = filePath;
265
- sandboxModule.paths = Module._nodeModulePaths(path.dirname(filePath));
266
- const sandboxRequire = Module.createRequire(filePath);
267
-
268
- const jsqlMod = new ModApi(collector);
269
- const context = vm.createContext({
270
- module: sandboxModule,
271
- exports: sandboxModule.exports,
272
- require: sandboxRequire,
273
- __filename: filePath,
274
- __dirname: path.dirname(filePath),
275
- console,
276
- process,
277
- Buffer,
278
- setTimeout,
279
- clearTimeout,
280
- setInterval,
281
- clearInterval,
282
- URL,
283
- URLSearchParams,
284
- TextEncoder,
285
- TextDecoder,
286
- jsql: { mod: jsqlMod }
287
- });
288
-
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;
289
279
  try {
290
- vm.runInContext(code, context, { filename: filePath });
280
+ plugin = require(filePath);
291
281
  } catch (e) {
292
282
  throw new Error(`Failed to load module ${filePath}: ${e.message}`);
293
283
  }
294
-
295
- const sandboxExports = sandboxModule.exports;
296
- const hasExports = sandboxExports && Object.keys(sandboxExports).length > 0;
297
- const plugin = hasExports ? sandboxExports : {
298
- hooks: collector.hooks,
299
- onEvent: collector.onEvent,
300
- install: collector.install
301
- };
302
-
303
- const meta = {
304
- name: collector.name || plugin.name || path.basename(filePath, ext),
305
- priority: collector.priority !== undefined ? collector.priority : plugin.priority,
306
- depends: collector.depends || plugin.depends,
307
- api: collector.api || plugin.api
308
- };
309
- for (const [k, v] of Object.entries(meta)) {
310
- if (v !== undefined && v !== null) plugin[k] = v;
284
+ if (typeof plugin === 'function') {
285
+ return { install: plugin, name: path.basename(filePath, ext) };
311
286
  }
287
+ if (!plugin.name) plugin.name = path.basename(filePath, ext);
312
288
  return plugin;
313
289
  }
314
290
  }
@@ -7,6 +7,10 @@
7
7
  const { executeSQL, applyParams, escapeValue, escapeId } = require('./sql');
8
8
  const Database = require('./database');
9
9
 
10
+ function isQueryResult(r) {
11
+ return !!r && typeof r === 'object' && Array.isArray(r.rows) && Array.isArray(r.columns);
12
+ }
13
+
10
14
  function toResultPacket(r, table) {
11
15
  if (isQueryResult(r)) {
12
16
  const columns = r.columns || [];
@@ -174,36 +178,149 @@ class Connection {
174
178
  class Pool {
175
179
  constructor(options = {}) {
176
180
  this.config = options;
177
- this._connection = null;
181
+ this.connectionLimit = options.connectionLimit || 10;
182
+ this.queueLimit = options.queueLimit || 0;
183
+ this.idleTimeout = options.idleTimeout != null ? options.idleTimeout : 300000;
184
+ this._connections = [];
185
+ this._waiters = [];
186
+ this._closed = false;
187
+ this._sharedEngine = null;
188
+ this._reaper = setInterval(() => this._reapIdle(), Math.max(1000, Math.floor(this.idleTimeout / 10) || 10000));
189
+ this._reaper.unref();
178
190
  }
179
191
 
180
- async _getConnection() {
181
- if (!this._connection) {
182
- this._connection = new Connection(this.config);
183
- await this._connection._getEngine();
192
+ _createConnection() {
193
+ const conn = new Connection(this.config);
194
+ conn._pool = this;
195
+ if (!conn.database && !conn.engine && !conn.filename) {
196
+ conn.database = this._sharedEngine;
184
197
  }
185
- return this._connection;
198
+ const origDestroy = conn.destroy.bind(conn);
199
+ conn.destroy = () => {
200
+ this._remove(conn);
201
+ origDestroy();
202
+ };
203
+ return conn;
186
204
  }
187
205
 
188
- query(...args) {
189
- return this._getConnection().then(conn => conn.query(...args));
206
+ async _ensureSharedEngine() {
207
+ if (!this._sharedEngine) {
208
+ this._sharedEngine = new Database(':memory:');
209
+ if (typeof this._sharedEngine.start === 'function') await this._sharedEngine.start();
210
+ }
211
+ return this._sharedEngine;
190
212
  }
191
213
 
192
- getConnection(cb) {
193
- const p = this._getConnection().then(conn => {
194
- const release = conn.release.bind(conn);
195
- return { connection: conn, release };
214
+ _remove(conn) {
215
+ const idx = this._connections.indexOf(conn);
216
+ if (idx !== -1) this._connections.splice(idx, 1);
217
+ }
218
+
219
+ _reapIdle() {
220
+ if (this.idleTimeout <= 0) return;
221
+ const now = Date.now();
222
+ for (const conn of this._connections.slice()) {
223
+ if (conn.state === 'released' && !conn._inUse && now - (conn._releasedAt || 0) > this.idleTimeout) {
224
+ this._remove(conn);
225
+ if (conn._ownEngine && conn.engine && typeof conn.engine.stop === 'function') {
226
+ conn.engine.stop().catch(() => {});
227
+ }
228
+ conn.state = 'destroyed';
229
+ }
230
+ }
231
+ }
232
+
233
+ _acquire() {
234
+ if (this._closed) return Promise.reject(new Error('Pool is closed'));
235
+ const free = this._connections.find(c => c.state === 'released' && !c._inUse);
236
+ if (free) {
237
+ free._inUse = true;
238
+ free.state = 'acquired';
239
+ return Promise.resolve(free);
240
+ }
241
+ if (this._connections.length < this.connectionLimit) {
242
+ return this._ensureSharedEngine().then(() => {
243
+ if (this._connections.length >= this.connectionLimit) {
244
+ return this._waitForConnection();
245
+ }
246
+ const conn = this._createConnection();
247
+ this._connections.push(conn);
248
+ conn._inUse = true;
249
+ conn.state = 'acquired';
250
+ return conn._getEngine().then(
251
+ () => conn,
252
+ err => {
253
+ this._remove(conn);
254
+ throw err;
255
+ }
256
+ );
257
+ });
258
+ }
259
+ return this._waitForConnection();
260
+ }
261
+
262
+ _waitForConnection() {
263
+ return new Promise((resolve, reject) => {
264
+ this._waiters.push({ resolve, reject });
265
+ if (this.queueLimit > 0 && this._waiters.length > this.queueLimit) {
266
+ const w = this._waiters.shift();
267
+ w.reject(new Error('Pool queue limit exceeded'));
268
+ }
196
269
  });
270
+ }
271
+
272
+ _release(conn) {
273
+ conn._inUse = false;
274
+ conn.state = 'released';
275
+ conn._releasedAt = Date.now();
276
+ const w = this._waiters.shift();
277
+ if (w) {
278
+ conn._inUse = true;
279
+ conn.state = 'acquired';
280
+ w.resolve(conn);
281
+ }
282
+ }
283
+
284
+ async _getConnection() {
285
+ return this._acquire();
286
+ }
287
+
288
+ async query(...args) {
289
+ const conn = await this._acquire();
290
+ try {
291
+ return await conn.query(...args);
292
+ } finally {
293
+ this._release(conn);
294
+ }
295
+ }
296
+
297
+ async getConnection(cb) {
298
+ const p = (async () => {
299
+ const conn = await this._acquire();
300
+ let released = false;
301
+ const release = () => {
302
+ if (released) return;
303
+ released = true;
304
+ if (conn.state === 'destroyed') {
305
+ this._remove(conn);
306
+ return;
307
+ }
308
+ this._release(conn);
309
+ };
310
+ return { connection: conn, release };
311
+ })();
197
312
  if (cb) p.then(r => cb(null, r.connection, r.release), err => cb(err));
198
313
  return p;
199
314
  }
200
315
 
201
- end(cb) {
316
+ async end(cb) {
202
317
  const p = (async () => {
203
- if (this._connection) {
204
- await this._connection.end();
205
- this._connection = null;
206
- }
318
+ this._closed = true;
319
+ if (this._reaper) { clearInterval(this._reaper); this._reaper = null; }
320
+ const err = new Error('Pool is closed');
321
+ for (const w of this._waiters.splice(0)) w.reject(err);
322
+ const conns = this._connections.splice(0);
323
+ await Promise.allSettled(conns.map(c => c.end()));
207
324
  return undefined;
208
325
  })();
209
326
  if (cb) p.then(() => cb(null), err => cb(err));