nodejs-store 2.3.0 → 2.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/README.md +33 -7
- package/README.zh-CN.md +38 -5
- package/package.json +2 -2
- package/src/crud/exec.js +68 -2
- package/src/crud/mutation.js +3 -7
- package/src/crud/write.js +26 -14
- package/src/datasource.js +652 -30
- package/src/executors/index.js +38 -1
- package/src/executors/mongo.js +61 -13
- package/src/executors/mysql.js +58 -23
- package/src/executors/postgres.js +57 -35
- package/src/executors/sqlite.js +49 -18
- package/src/index.js +76 -4
package/src/datasource.js
CHANGED
|
@@ -30,6 +30,46 @@ let _connections = Object.create(null);
|
|
|
30
30
|
/** 事务作用域的连接覆盖:source → 事务描述符(见 runInTransaction) */
|
|
31
31
|
const _txStore = new AsyncLocalStorage();
|
|
32
32
|
|
|
33
|
+
/** 当前会话:优先于 _txStore(会话内部自持事务连接) */
|
|
34
|
+
const _sessionStore = new AsyncLocalStorage();
|
|
35
|
+
|
|
36
|
+
/** 写命令 kind(与 crud 侧 Command.kind 一致) */
|
|
37
|
+
const WRITE_KINDS = new Set([
|
|
38
|
+
'insertOne',
|
|
39
|
+
'insertMany',
|
|
40
|
+
'updateMany',
|
|
41
|
+
'findOneAndUpdate',
|
|
42
|
+
'deleteMany',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/** SQL 后端 kind 白名单(Mongo 驱动实例 / Mongo 事务视图一律非 SQL) */
|
|
46
|
+
const SQL_KINDS = new Set(['mysql', 'postgres', 'sqlite']);
|
|
47
|
+
|
|
48
|
+
/** Mongo 事务能力缓存:client -> true/false(探测失败不写缓存,下次重探) */
|
|
49
|
+
const _mongoTxCap = new WeakMap();
|
|
50
|
+
|
|
51
|
+
/** 命令是否为写命令(跨源写 fail-closed 判定用) */
|
|
52
|
+
function isWriteCommand(cmd) {
|
|
53
|
+
return !!cmd && WRITE_KINDS.has(cmd.kind);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 会话内写入了多个数据源:跨源写无法原子(fail-closed,绝不静默提交半截) */
|
|
57
|
+
class NonAtomicWriteError extends Error {
|
|
58
|
+
constructor(sources) {
|
|
59
|
+
const list = Array.from(sources).sort();
|
|
60
|
+
super(
|
|
61
|
+
`会话内写入了多个数据源(${list.join(', ')}):跨源写无法原子(Phase 1 未提供分布式事务);请拆分为多个会话,或改用单一数据源`,
|
|
62
|
+
);
|
|
63
|
+
this.name = 'NonAtomicWriteError';
|
|
64
|
+
this.sources = list;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 当前生效会话(无则 null) */
|
|
69
|
+
function currentSession() {
|
|
70
|
+
return _sessionStore.getStore() || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
33
73
|
/**
|
|
34
74
|
* SQL 下推遇到无法安全翻译的组合(core 标记 unsupported)
|
|
35
75
|
*
|
|
@@ -98,11 +138,11 @@ function hasConnection(source) {
|
|
|
98
138
|
return _connections[source] !== undefined;
|
|
99
139
|
}
|
|
100
140
|
|
|
101
|
-
/** 连接是否为 SQL 执行器描述符(`
|
|
141
|
+
/** 连接是否为 SQL 执行器描述符(`kind ∈ SQL_KINDS` 且 `exec` 为函数;Mongo 一律非 SQL) */
|
|
102
142
|
function isSqlConnection(connection) {
|
|
103
143
|
return (
|
|
104
144
|
!!connection &&
|
|
105
|
-
|
|
145
|
+
SQL_KINDS.has(connection.kind) &&
|
|
106
146
|
typeof connection.exec === 'function'
|
|
107
147
|
);
|
|
108
148
|
}
|
|
@@ -122,33 +162,164 @@ function connectionFor(source) {
|
|
|
122
162
|
return getConnection(source);
|
|
123
163
|
}
|
|
124
164
|
|
|
165
|
+
/** 会话感知的连接解析:会话内返回事务覆盖,否则返回全局连接
|
|
166
|
+
*
|
|
167
|
+
* `crud.exec._execOn` 与 `executeRaw` 共用此入口,保证会话内命令
|
|
168
|
+
* (含跨多次调用的 CRUD 与原生 SQL)落到同一事务连接。
|
|
169
|
+
*/
|
|
170
|
+
async function resolveConnection(source, isWrite = false) {
|
|
171
|
+
const session = currentSession();
|
|
172
|
+
if (session !== null) {
|
|
173
|
+
const override = await session.connFor(source, isWrite);
|
|
174
|
+
if (override !== null && override !== undefined) return override;
|
|
175
|
+
}
|
|
176
|
+
return connectionFor(source);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** 在会话上下文内执行 fn(供 store.session 使用) */
|
|
180
|
+
function runWithSession(session, fn) {
|
|
181
|
+
return _sessionStore.run(session, fn);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 嵌套作用域无保存点原语 → 降级并入外层(允许降级,禁止静默) */
|
|
185
|
+
function warnSavepointUnavailable(source) {
|
|
186
|
+
_emitFeedback({
|
|
187
|
+
type: 'nested_savepoint_unsupported',
|
|
188
|
+
code: 'nestedSavepointUnsupported',
|
|
189
|
+
layer: 'datasource',
|
|
190
|
+
message: `数据源 ${source} 的事务句柄未提供保存点原语:嵌套作用域并入外层(该层失败将回滚整个外层事务)`,
|
|
191
|
+
hint: '为执行器 openTransaction 句柄补 savepoint / releaseSavepoint / rollbackToSavepoint',
|
|
192
|
+
source,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 错误路径保存点回滚 / 释放失败:发反馈,绝不掩盖原始错误 */
|
|
197
|
+
function warnSavepointFailed(source, name, exc) {
|
|
198
|
+
_emitFeedback({
|
|
199
|
+
type: 'savepoint_failed',
|
|
200
|
+
code: 'savepointFailed',
|
|
201
|
+
layer: 'datasource',
|
|
202
|
+
message: `保存点回滚/释放失败(${name},数据源 ${source}):${exc}`,
|
|
203
|
+
hint: '检查该数据源连接与事务状态;该嵌套作用域可能未能独立回滚',
|
|
204
|
+
source,
|
|
205
|
+
savepoint: name,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 回滚到保存点并释放;任一失败发 savepoint_failed(不掩盖原始错误) */
|
|
210
|
+
async function rollbackSavepoint(tx, source, name) {
|
|
211
|
+
const errors = [];
|
|
212
|
+
for (const op of ['rollbackToSavepoint', 'releaseSavepoint']) {
|
|
213
|
+
try {
|
|
214
|
+
await tx[op](name);
|
|
215
|
+
} catch (e) {
|
|
216
|
+
errors.push(e);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (errors.length > 0) warnSavepointFailed(source, name, errors[0]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 同源嵌套事务作用域:在已持有的事务连接上开保存点
|
|
224
|
+
* - 外层句柄无 savepoint 原语 → 降级并入外层(同一外层作用域只告警一次);
|
|
225
|
+
* - 成功 RELEASE;失败 ROLLBACK TO + RELEASE 后原样上抛(外层可继续)。
|
|
226
|
+
*/
|
|
227
|
+
async function nestedSavepointScope(source, outer, fn) {
|
|
228
|
+
const tx = outer.tx;
|
|
229
|
+
if (!tx || typeof tx.savepoint !== 'function') {
|
|
230
|
+
if (!outer.spWarned) {
|
|
231
|
+
outer.spWarned = true;
|
|
232
|
+
warnSavepointUnavailable(source);
|
|
233
|
+
}
|
|
234
|
+
return fn();
|
|
235
|
+
}
|
|
236
|
+
const depth = (outer.spDepth || 0) + 1;
|
|
237
|
+
outer.spDepth = depth;
|
|
238
|
+
const name = `sp_${depth}`;
|
|
239
|
+
await tx.savepoint(name); // 创建失败直接上抛(保存点不存在,无需回滚)
|
|
240
|
+
let out;
|
|
241
|
+
try {
|
|
242
|
+
out = await fn();
|
|
243
|
+
} catch (e) {
|
|
244
|
+
await rollbackSavepoint(tx, source, name);
|
|
245
|
+
throw e;
|
|
246
|
+
} finally {
|
|
247
|
+
outer.spDepth = depth - 1;
|
|
248
|
+
}
|
|
249
|
+
await tx.releaseSavepoint(name);
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
125
253
|
/**
|
|
126
|
-
*
|
|
254
|
+
* 事务作用域:在单个源上以「同连接 + 同事务」执行 fn 内的全部命令
|
|
127
255
|
*
|
|
128
|
-
* -
|
|
129
|
-
* -
|
|
130
|
-
*
|
|
256
|
+
* - 会话内调用:并入会话(事务边界由会话统一管理),不另开事务;
|
|
257
|
+
* - SQL 源且执行器实现 withTransaction:包事务;同源嵌套开 SAVEPOINT sp_<n>;
|
|
258
|
+
* - SQL 源且执行器**未**实现 withTransaction:按原样执行并发 `transaction_not_atomic`
|
|
259
|
+
* (降级不静默,与 `store.session` 的 `session_not_atomic` 对称);
|
|
260
|
+
* - Mongo 源:探测可事务(replica set / sharded)→ 包 session 事务;standalone / unknown
|
|
261
|
+
* → 发 `mongo_transaction_unsupported` 并按原样执行(绝不静默假装已事务化);
|
|
262
|
+
* - Mongo 无保存点原语:同源嵌套走既有 `nested_savepoint_unsupported` 降级声明;
|
|
131
263
|
* - 事务体抛错统一 rollback 后原样上抛。
|
|
132
264
|
*/
|
|
133
265
|
async function runInTransaction(source, fn) {
|
|
266
|
+
const session = currentSession();
|
|
267
|
+
if (session !== null) {
|
|
268
|
+
// 会话内:事务边界由会话统一管理;本层作为嵌套作用域开保存点(失败只回滚本层)
|
|
269
|
+
return session.nestedScope(fn);
|
|
270
|
+
}
|
|
134
271
|
const conn = getConnection(source);
|
|
135
|
-
|
|
136
|
-
|
|
272
|
+
const parent = _txStore.getStore() || new Map();
|
|
273
|
+
|
|
274
|
+
if (isSqlConnection(conn)) {
|
|
275
|
+
// ── SQL 分支(事务作用域)──
|
|
276
|
+
if (typeof conn.withTransaction !== 'function') {
|
|
277
|
+
// 降级不静默:与 store.session 的 session_not_atomic 对称,显式声明本事务作用域未生效
|
|
278
|
+
warnTransactionNotAtomic(source, conn.kind);
|
|
279
|
+
return fn();
|
|
280
|
+
}
|
|
281
|
+
if (parent.has(source)) {
|
|
282
|
+
// 同源嵌套事务:在已持有的事务连接上开保存点(内层失败只回滚本层,外层可继续)
|
|
283
|
+
return nestedSavepointScope(source, parent.get(source), fn);
|
|
284
|
+
}
|
|
285
|
+
const txDescriptor = { kind: conn.kind, exec: null, tx: null };
|
|
286
|
+
const store = new Map(parent);
|
|
287
|
+
store.set(source, txDescriptor);
|
|
288
|
+
return _txStore.run(store, () =>
|
|
289
|
+
conn.withTransaction(async (execOnTx, tx) => {
|
|
290
|
+
txDescriptor.exec = execOnTx;
|
|
291
|
+
txDescriptor.tx = tx;
|
|
292
|
+
return fn();
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
137
295
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
//
|
|
296
|
+
|
|
297
|
+
// ── Mongo 分支 ──
|
|
298
|
+
if (parent.has(source)) {
|
|
299
|
+
// 同源嵌套:Mongo 无保存点 → 复用降级声明(内层失败将回滚整个外层事务)
|
|
300
|
+
return nestedSavepointScope(source, parent.get(source), fn);
|
|
301
|
+
}
|
|
302
|
+
const cap = await mongoTransactable(conn);
|
|
303
|
+
if (cap !== true) {
|
|
304
|
+
warnMongoUnsupported(source, cap === false ? 'standalone' : 'unknown');
|
|
142
305
|
return fn();
|
|
143
306
|
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
307
|
+
const tx = await executors.mongo.openTransaction(conn);
|
|
308
|
+
const view = { kind: 'mongo', conn, session: tx.session, tx };
|
|
309
|
+
const store = new Map(parent);
|
|
310
|
+
store.set(source, view);
|
|
311
|
+
return _txStore.run(store, async () => {
|
|
312
|
+
try {
|
|
313
|
+
const out = await fn();
|
|
314
|
+
await tx.commit();
|
|
315
|
+
return out;
|
|
316
|
+
} catch (e) {
|
|
317
|
+
await tx.rollback();
|
|
318
|
+
throw e;
|
|
319
|
+
} finally {
|
|
320
|
+
await tx.release();
|
|
321
|
+
}
|
|
322
|
+
});
|
|
152
323
|
}
|
|
153
324
|
|
|
154
325
|
/**
|
|
@@ -179,6 +350,77 @@ function mongoDb(connection, source, namespace) {
|
|
|
179
350
|
return null;
|
|
180
351
|
}
|
|
181
352
|
|
|
353
|
+
/** MongoClient 形态判别(db 实例有 collection 函数,MongoClient 只有 db) */
|
|
354
|
+
function isMongoClientHandle(x) {
|
|
355
|
+
return !!x && typeof x.db === 'function' && typeof x.collection !== 'function';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Mongo 连接的 client:db 实例取 `.client`;MongoClient 返回自身;其余 null */
|
|
359
|
+
function mongoClientOf(connection) {
|
|
360
|
+
if (isMongoClientHandle(connection)) return connection;
|
|
361
|
+
return (connection && connection.client) || null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 探测 Mongo 部署是否支持多文档事务(四态,绝不猜;结果按 client 缓存)
|
|
366
|
+
* - true : 支持(replica set 的 setName,或 sharded 的 msg === 'isdbgrid')
|
|
367
|
+
* - false: 不支持(standalone)
|
|
368
|
+
* - null : 探测失败 / 无法探测(unknown;不写缓存,下次重探)
|
|
369
|
+
* 经 admin 库的 hello 命令探测(只读、幂等、驱动无关)。
|
|
370
|
+
*/
|
|
371
|
+
async function mongoTransactable(connection) {
|
|
372
|
+
const client = mongoClientOf(connection);
|
|
373
|
+
if (!client) return null;
|
|
374
|
+
if (_mongoTxCap.has(client)) return _mongoTxCap.get(client);
|
|
375
|
+
let hello;
|
|
376
|
+
try {
|
|
377
|
+
hello = await client.db('admin').command({ hello: 1 });
|
|
378
|
+
} catch (_) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
const cap = Boolean(hello.setName) || hello.msg === 'isdbgrid';
|
|
382
|
+
_mongoTxCap.set(client, cap);
|
|
383
|
+
return cap;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** 数据源名是否绑定 Mongo 源(非 SQL 描述符即 Mongo 驱动实例 / Mongo 事务视图) */
|
|
387
|
+
function isMongoSource(source) {
|
|
388
|
+
return !isSqlConnection(getConnection(source));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Mongo 部署不支持事务 → 降级按原样执行(允许降级,禁止静默)
|
|
392
|
+
*
|
|
393
|
+
* deployment: 'standalone'(探测为不支持)| 'unknown'(探测失败/无法探测)。
|
|
394
|
+
*/
|
|
395
|
+
function warnMongoUnsupported(source, deployment) {
|
|
396
|
+
_emitFeedback({
|
|
397
|
+
type: 'mongo_transaction_unsupported',
|
|
398
|
+
code: 'mongoTransactionUnsupported',
|
|
399
|
+
layer: 'datasource',
|
|
400
|
+
deployment,
|
|
401
|
+
message: `数据源 ${source} 的 Mongo 部署不支持多文档事务(${deployment}):本次调用按原样执行(非原子)`,
|
|
402
|
+
hint: '将 MongoDB 部署为 replica set 或 sharded cluster 以启用 session 事务;standalone 无此能力',
|
|
403
|
+
source,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** SQL 执行器未实现 withTransaction → 事务作用域按原样执行(允许降级,禁止静默)
|
|
408
|
+
*
|
|
409
|
+
* 与 `session_not_atomic`(会话路径)对称:同一类降级在两条入口(`store.transaction`
|
|
410
|
+
* 与 `store.session`)都必须显式声明,不留静默口子。
|
|
411
|
+
*/
|
|
412
|
+
function warnTransactionNotAtomic(source, kind) {
|
|
413
|
+
_emitFeedback({
|
|
414
|
+
type: 'transaction_not_atomic',
|
|
415
|
+
code: 'transactionNotAtomic',
|
|
416
|
+
layer: 'datasource',
|
|
417
|
+
message: `数据源 ${source}(${kind}) 未实现 withTransaction:事务作用域内命令按原样执行(非原子)`,
|
|
418
|
+
hint: '为该执行器实现 withTransaction,或将写命令收敛到已支持事务的数据源',
|
|
419
|
+
source,
|
|
420
|
+
kind,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
182
424
|
/** schema 声明的数据源名(缺省 `default`) */
|
|
183
425
|
function sourceOfSchema(name) {
|
|
184
426
|
return _getSchema(name).datasource || DEFAULT_SOURCE;
|
|
@@ -244,30 +486,393 @@ class RawSqlError extends Error {
|
|
|
244
486
|
}
|
|
245
487
|
|
|
246
488
|
/**
|
|
247
|
-
* 在指定 SQL 源上执行原生 SQL(Host
|
|
489
|
+
* 在指定 SQL 源上执行原生 SQL(Host 层逃生口,编译由 core 的 rawStmtCompile 完成)
|
|
490
|
+
*
|
|
491
|
+
* 两档参数风格(core 编译器按 params 类型自动分档):
|
|
492
|
+
* - 位置档:params 为数组(或 null)→ SQL 原样透传,占位符为各后端原生风格
|
|
493
|
+
* (mysql/sqlite 用 `?`,postgres 用 `$1..$n`);
|
|
494
|
+
* - 命名档:params 为对象 → SQL 文本中的 `:name` 编译为方言占位符(同名复用、
|
|
495
|
+
* 跳过 `::` cast / 引号 / 注释边界;缺名 / 多余名显式报错)。
|
|
248
496
|
*
|
|
249
|
-
* -
|
|
250
|
-
* -
|
|
497
|
+
* - 事务 / 会话作用域内经 `resolveConnection` 落到事务专用连接 → 支持 SELECT ... FOR UPDATE;
|
|
498
|
+
* - `isWrite` 缺省时由 SQL 首词推断(SELECT/WITH/EXPLAIN/SHOW/PRAGMA/TABLE 视为读,
|
|
499
|
+
* 其余按写——默认写是安全方向);显式传入则覆盖推断;
|
|
251
500
|
* - 仅支持 SQL 源;Mongo 源显式报错(绝不静默);
|
|
252
|
-
* - `isWrite=false` 视为读(取行);`true` 视为写(取影响行数);
|
|
253
501
|
* - 返回 `{ rows, affectedRows }`。
|
|
254
502
|
* 对齐 py_store/datasource.py#execute_raw。
|
|
255
503
|
*/
|
|
256
|
-
async function executeRaw(source, sql, params =
|
|
257
|
-
|
|
258
|
-
|
|
504
|
+
async function executeRaw(source, sql, params = null, isWrite = null) {
|
|
505
|
+
if (params != null && !Array.isArray(params) && typeof params !== 'object') {
|
|
506
|
+
throw new RawSqlError(`原生 SQL params 仅支持数组(位置档)或对象(命名档),收到 ${typeof params}`);
|
|
507
|
+
}
|
|
508
|
+
const conn0 = connectionFor(source);
|
|
509
|
+
if (!conn0 || !SQL_KINDS.has(conn0.kind)) {
|
|
259
510
|
throw new RawSqlError(
|
|
260
511
|
`数据源 ${source} 不是 SQL 源(原生 SQL 入口仅支持 mysql/postgres/sqlite)`,
|
|
261
512
|
);
|
|
262
513
|
}
|
|
263
|
-
|
|
264
|
-
|
|
514
|
+
let compiled;
|
|
515
|
+
try {
|
|
516
|
+
compiled = _core.rawStmtCompile(conn0.kind, sql, params ?? null, isWrite ?? null);
|
|
517
|
+
} catch (e) {
|
|
518
|
+
throw new RawSqlError((e && e.message) || String(e));
|
|
265
519
|
}
|
|
266
|
-
const
|
|
520
|
+
const conn = await resolveConnection(source, compiled.isWrite);
|
|
521
|
+
if (!conn || typeof conn.exec !== 'function') {
|
|
522
|
+
throw new RawSqlError(`SQL 数据源 ${source}(${conn && conn.kind}) 的执行器未接入`);
|
|
523
|
+
}
|
|
524
|
+
const stmt = { text: compiled.sql, params: compiled.params, isWrite: compiled.isWrite };
|
|
267
525
|
const out = await conn.exec({ stmts: [stmt] });
|
|
268
526
|
return { rows: out.rows ?? null, affectedRows: Number(out.affectedRows || 0) };
|
|
269
527
|
}
|
|
270
528
|
|
|
529
|
+
/** 原生 Mongo 命令入口的显式错误(非 Mongo 源 / 非 Mongo 形态) */
|
|
530
|
+
class NativeCommandError extends Error {
|
|
531
|
+
constructor(message) {
|
|
532
|
+
super(message);
|
|
533
|
+
this.name = 'NativeCommandError';
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* 在指定 Mongo 源上执行原生聚合管道(Host 层逃生口,对标 SQL 侧 executeRaw)
|
|
539
|
+
*
|
|
540
|
+
* - 复用 GQL 路径唯一的 Mongo IO 边界(executors/mongo.js#execMongo 的 aggregate
|
|
541
|
+
* 分支),options 为驱动原生透传项(allowDiskUse/batchSize/hint/maxTimeMS…,
|
|
542
|
+
* 宿主不做白名单);
|
|
543
|
+
* - 事务 / 会话作用域内自动透传 session(session 由事务强制接管,
|
|
544
|
+
* options.session 不可覆盖);统一按读路径解析(isWrite=false),
|
|
545
|
+
* $merge/$out 写管道请自行开事务;
|
|
546
|
+
* - 仅支持 Mongo 源:SQL 源显式报错并指引 executeRaw(绝不静默);
|
|
547
|
+
* MongoClient 形态须经 schema 声明 namespace(mongoDb 既有校验,缺名即报错);
|
|
548
|
+
* - 返回 `{ rows }`。
|
|
549
|
+
* 对齐 py_store/datasource.py#execute_native。
|
|
550
|
+
*/
|
|
551
|
+
async function executeNative(source, collection, pipeline = [], options = null) {
|
|
552
|
+
const conn = await resolveConnection(source, false);
|
|
553
|
+
if (isSqlConnection(conn)) {
|
|
554
|
+
throw new NativeCommandError(
|
|
555
|
+
`数据源 ${source} 是 SQL 源(原生 Mongo 命令入口仅支持 mongo;SQL 源请用 executeRaw)`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
const isTxView = !!conn && conn.kind === 'mongo';
|
|
559
|
+
const db = mongoDb(isTxView ? conn.conn : conn, source, null);
|
|
560
|
+
if (!db) {
|
|
561
|
+
throw new NativeCommandError(
|
|
562
|
+
`数据源 ${source} 不是 Mongo 源(原生 Mongo 命令入口仅支持 mongo)`,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
const cmd = {
|
|
566
|
+
kind: 'aggregate',
|
|
567
|
+
collection,
|
|
568
|
+
pipeline: Array.from(pipeline || []),
|
|
569
|
+
options: { ...(options || {}) },
|
|
570
|
+
};
|
|
571
|
+
// 属性路径调用(monkeypatch 可拦截);session 由事务强制注入(_opts 内覆盖用户值)
|
|
572
|
+
const rows = await executors.mongo.execMongo(db, cmd, isTxView ? conn.session : null);
|
|
573
|
+
return { rows };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* 显式会话(工作单元)
|
|
578
|
+
*
|
|
579
|
+
* - 惰性开事务:命令真正落到某 SQL 源时才 checkout 并 BEGIN(空会话不占连接);
|
|
580
|
+
* - Mongo 源按探测结果事务化;不可事务(standalone/unknown)→ 直通 +
|
|
581
|
+
* `mongo_transaction_unsupported` 声明(绝不静默);
|
|
582
|
+
* - 跨源写 fail-closed:≥2 个源发生写命令 → 退出时全部 rollback 并抛
|
|
583
|
+
* NonAtomicWriteError;
|
|
584
|
+
* - 嵌套会话 / 会话内 transaction:作为嵌套作用域在已有事务上开保存点
|
|
585
|
+
* (内层失败只回滚本层;名字形如 sp_<n>)。
|
|
586
|
+
*
|
|
587
|
+
* 会话上下文由 `store.session` 用 `_sessionStore.run(session, fn)` 整体包裹
|
|
588
|
+
* (见 index.js#session);`Session` 自身不设置上下文,`bindOuter` 只记录父子关系。
|
|
589
|
+
*/
|
|
590
|
+
class Session {
|
|
591
|
+
constructor() {
|
|
592
|
+
this._views = new Map(); // source -> { kind, exec, tx } | Mongo 视图 | null(null = 直通)
|
|
593
|
+
this._txs = new Map(); // source -> 显式事务句柄
|
|
594
|
+
this._opened = []; // 开启顺序
|
|
595
|
+
this._wrote = new Set(); // 发生过写命令的 source
|
|
596
|
+
this._warned = new Set();
|
|
597
|
+
this._outer = null;
|
|
598
|
+
this._spWarned = new Set(); // 无保存点原语的告警去重
|
|
599
|
+
this._scopes = []; // 嵌套作用域栈(仅最外层会话持有)
|
|
600
|
+
this._spSeq = 0; // 保存点命名序号(sp_<n>)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** 绑定外层会话(嵌套时生命周期交外层);由 store.session 调用,不设置上下文 */
|
|
604
|
+
bindOuter(parent) {
|
|
605
|
+
this._outer = parent;
|
|
606
|
+
return this;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** 退出会话:err 非空 → 全部回滚并原样上抛;否则跨源写判定后统一提交 */
|
|
610
|
+
async exit(err) {
|
|
611
|
+
if (this._outer !== null) return;
|
|
612
|
+
if (err !== null && err !== undefined) {
|
|
613
|
+
await this._finalize(false);
|
|
614
|
+
throw err;
|
|
615
|
+
}
|
|
616
|
+
if (this._wrote.size > 1) {
|
|
617
|
+
await this._finalize(false);
|
|
618
|
+
throw new NonAtomicWriteError(this._wrote);
|
|
619
|
+
}
|
|
620
|
+
await this._finalize(true);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async _finalize(commit) {
|
|
624
|
+
const errors = [];
|
|
625
|
+
if (commit) {
|
|
626
|
+
for (const source of this._opened) {
|
|
627
|
+
try {
|
|
628
|
+
await this._txs.get(source).commit();
|
|
629
|
+
} catch (exc) { // 提交失败:其余全部回滚
|
|
630
|
+
errors.push([source, exc]);
|
|
631
|
+
for (const other of this._opened) {
|
|
632
|
+
if (other === source) continue;
|
|
633
|
+
try {
|
|
634
|
+
await this._txs.get(other).rollback();
|
|
635
|
+
} catch (exc2) {
|
|
636
|
+
errors.push([other, exc2]);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
} else {
|
|
643
|
+
for (const source of [...this._opened].reverse()) {
|
|
644
|
+
try {
|
|
645
|
+
await this._txs.get(source).rollback();
|
|
646
|
+
} catch (exc) {
|
|
647
|
+
errors.push([source, exc]);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
for (const source of this._opened) {
|
|
652
|
+
try {
|
|
653
|
+
await this._txs.get(source).release();
|
|
654
|
+
} catch (exc) {
|
|
655
|
+
errors.push([source, exc]);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
this._views.clear();
|
|
659
|
+
this._txs.clear();
|
|
660
|
+
this._opened = [];
|
|
661
|
+
for (const [source, exc] of errors) this._warnFinalizeFailure(source, commit, exc);
|
|
662
|
+
if (errors.length > 0) throw errors[0][1];
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** 命令落到该源时解析连接:事务视图 / null(直通,用原始连接)
|
|
666
|
+
*
|
|
667
|
+
* 嵌套作用域内首次**写**某源时,在该事务连接上开保存点(惰性,只读不开)。
|
|
668
|
+
*/
|
|
669
|
+
async connFor(source, isWrite = false) {
|
|
670
|
+
if (isWrite) this._wrote.add(source);
|
|
671
|
+
if (!this._views.has(source)) this._views.set(source, await this._openView(source));
|
|
672
|
+
const view = this._views.get(source);
|
|
673
|
+
if (isWrite && this._scopes.length > 0) await this._scopeSavepoints(source, view);
|
|
674
|
+
return view;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** 解析并缓存该源的事务视图;返回 { kind, exec, tx } /
|
|
678
|
+
* { kind:'mongo', conn, session, tx } / null(直通) */
|
|
679
|
+
async _openView(source) {
|
|
680
|
+
const override = _txStore.getStore();
|
|
681
|
+
if (override && override.has(source)) {
|
|
682
|
+
const ov = override.get(source);
|
|
683
|
+
if (ov.tx) return ov; // 外层事务已绑定该源 → 复用,不新开事务、不告警
|
|
684
|
+
}
|
|
685
|
+
const connection = getConnection(source);
|
|
686
|
+
if (isSqlConnection(connection)) {
|
|
687
|
+
if (typeof connection.openTransaction === 'function') {
|
|
688
|
+
const tx = await connection.openTransaction();
|
|
689
|
+
this._txs.set(source, tx);
|
|
690
|
+
this._opened.push(source);
|
|
691
|
+
return { kind: connection.kind, exec: tx.exec, tx };
|
|
692
|
+
}
|
|
693
|
+
this._warnNotAtomic(source, connection.kind);
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
// Mongo 源(裸驱动实例)
|
|
697
|
+
const cap = await mongoTransactable(connection);
|
|
698
|
+
if (cap === true) {
|
|
699
|
+
const tx = await executors.mongo.openTransaction(connection);
|
|
700
|
+
this._txs.set(source, tx);
|
|
701
|
+
this._opened.push(source);
|
|
702
|
+
return { kind: 'mongo', conn: connection, session: tx.session, tx };
|
|
703
|
+
}
|
|
704
|
+
this._warnMongo(source, cap);
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ---------- 嵌套作用域(嵌套会话 / 会话内 transaction) ----------
|
|
709
|
+
|
|
710
|
+
/** 进入嵌套作用域(内层 session / 会话内 transaction) */
|
|
711
|
+
pushScope() {
|
|
712
|
+
const scope = { savepoints: new Map(), txs: new Map() };
|
|
713
|
+
this._scopes.push(scope);
|
|
714
|
+
return scope;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** 退出嵌套作用域:按成败回滚到保存点或释放(失败发反馈,不掩盖原异常) */
|
|
718
|
+
async popScope(scope, rollback) {
|
|
719
|
+
try {
|
|
720
|
+
for (const source of [...scope.savepoints.keys()].reverse()) {
|
|
721
|
+
const name = scope.savepoints.get(source);
|
|
722
|
+
if (name === null) continue;
|
|
723
|
+
const tx = scope.txs.get(source);
|
|
724
|
+
try {
|
|
725
|
+
if (rollback) await tx.rollbackToSavepoint(name);
|
|
726
|
+
await tx.releaseSavepoint(name);
|
|
727
|
+
} catch (e) {
|
|
728
|
+
warnSavepointFailed(source, name, e);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
} finally {
|
|
732
|
+
const i = this._scopes.indexOf(scope);
|
|
733
|
+
if (i >= 0) this._scopes.splice(i, 1);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** 会话内以嵌套作用域执行 fn(保存点隔离;失败只回滚本层) */
|
|
738
|
+
async nestedScope(fn) {
|
|
739
|
+
const scope = this.pushScope();
|
|
740
|
+
let out;
|
|
741
|
+
try {
|
|
742
|
+
out = await fn();
|
|
743
|
+
} catch (e) {
|
|
744
|
+
await this.popScope(scope, true);
|
|
745
|
+
throw e;
|
|
746
|
+
}
|
|
747
|
+
await this.popScope(scope, false);
|
|
748
|
+
return out;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** 嵌套作用域首次写到某源时开保存点(惰性;句柄无原语 → 降级 + 告警一次) */
|
|
752
|
+
async _scopeSavepoints(source, view) {
|
|
753
|
+
const tx = view && view.tx ? view.tx : null;
|
|
754
|
+
for (const scope of this._scopes) {
|
|
755
|
+
if (scope.savepoints.has(source)) continue;
|
|
756
|
+
if (!tx || typeof tx.savepoint !== 'function') {
|
|
757
|
+
this._warnScopeNoSavepoint(source);
|
|
758
|
+
scope.savepoints.set(source, null);
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
this._spSeq += 1;
|
|
762
|
+
const name = `sp_${this._spSeq}`;
|
|
763
|
+
await tx.savepoint(name); // 创建失败直接上抛(可见错误)
|
|
764
|
+
scope.savepoints.set(source, name);
|
|
765
|
+
scope.txs.set(source, tx);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
_warnScopeNoSavepoint(source) {
|
|
770
|
+
if (this._spWarned.has(source)) return;
|
|
771
|
+
this._spWarned.add(source);
|
|
772
|
+
warnSavepointUnavailable(source);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ---------- 告警(自动反馈:允许降级、禁止静默) ----------
|
|
776
|
+
|
|
777
|
+
_warnNotAtomic(source, kind) {
|
|
778
|
+
if (this._warned.has(source)) return;
|
|
779
|
+
this._warned.add(source);
|
|
780
|
+
_emitFeedback({
|
|
781
|
+
type: 'session_not_atomic',
|
|
782
|
+
code: 'sessionNotAtomic',
|
|
783
|
+
layer: 'datasource',
|
|
784
|
+
message: `数据源 ${source}(${kind}) 未实现 openTransaction:会话内该源命令按原样执行(非原子)`,
|
|
785
|
+
hint: '为该执行器实现 openTransaction,或将该源的写命令移出会话',
|
|
786
|
+
source,
|
|
787
|
+
kind,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Mongo 部署不支持事务 → 降级声明(同源只声明一次);cap=false→standalone,null→unknown */
|
|
792
|
+
_warnMongo(source, cap) {
|
|
793
|
+
if (this._warned.has(source)) return;
|
|
794
|
+
this._warned.add(source);
|
|
795
|
+
warnMongoUnsupported(source, cap === false ? 'standalone' : 'unknown');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
_warnFinalizeFailure(source, commit, exc) {
|
|
799
|
+
_emitFeedback({
|
|
800
|
+
type: 'session_finalize_failed',
|
|
801
|
+
code: 'sessionFinalizeFailed',
|
|
802
|
+
layer: 'datasource',
|
|
803
|
+
message: `会话收尾失败(${commit ? 'commit' : 'rollback'},数据源 ${source}):${exc}`,
|
|
804
|
+
hint: '检查该数据源连接状态;rollback 失败可能意味着连接已失效',
|
|
805
|
+
source,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ---------- 会话 API(与 Store 同名同形,委托 crud) ----------
|
|
810
|
+
|
|
811
|
+
async query(...a) {
|
|
812
|
+
const crud = require('./crud');
|
|
813
|
+
return crud.query(...a);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async queryOne(...a) {
|
|
817
|
+
const crud = require('./crud');
|
|
818
|
+
return crud.queryOne(...a);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async queryWithCount(...a) {
|
|
822
|
+
const crud = require('./crud');
|
|
823
|
+
return crud.queryWithCount(...a);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
async insert(...a) {
|
|
827
|
+
const crud = require('./crud');
|
|
828
|
+
return crud.insert(...a);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async insertMany(...a) {
|
|
832
|
+
const crud = require('./crud');
|
|
833
|
+
return crud.insertMany(...a);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async update(...a) {
|
|
837
|
+
const crud = require('./crud');
|
|
838
|
+
return crud.update(...a);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async updateMany(...a) {
|
|
842
|
+
const crud = require('./crud');
|
|
843
|
+
return crud.updateMany(...a);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async upsert(...a) {
|
|
847
|
+
const crud = require('./crud');
|
|
848
|
+
return crud.upsert(...a);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async remove(...a) {
|
|
852
|
+
const crud = require('./crud');
|
|
853
|
+
return crud.remove(...a);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async exists(...a) {
|
|
857
|
+
const crud = require('./crud');
|
|
858
|
+
return crud.exists(...a);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
async count(...a) {
|
|
862
|
+
const crud = require('./crud');
|
|
863
|
+
return crud.count(...a);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async mutation(...a) {
|
|
867
|
+
const crud = require('./crud');
|
|
868
|
+
return crud.mutation(...a);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async executeRaw(...a) {
|
|
872
|
+
return executeRaw(...a);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
271
876
|
module.exports = {
|
|
272
877
|
DEFAULT_SOURCE,
|
|
273
878
|
setConnections,
|
|
@@ -276,14 +881,31 @@ module.exports = {
|
|
|
276
881
|
isSqlConnection,
|
|
277
882
|
isSql,
|
|
278
883
|
connectionFor,
|
|
884
|
+
resolveConnection,
|
|
279
885
|
runInTransaction,
|
|
886
|
+
nestedSavepointScope,
|
|
887
|
+
warnSavepointUnavailable,
|
|
888
|
+
warnSavepointFailed,
|
|
889
|
+
warnTransactionNotAtomic,
|
|
280
890
|
mongoDb,
|
|
891
|
+
isMongoClientHandle,
|
|
892
|
+
mongoClientOf,
|
|
893
|
+
mongoTransactable,
|
|
894
|
+
isMongoSource,
|
|
895
|
+
warnMongoUnsupported,
|
|
281
896
|
sourceOfSchema,
|
|
282
897
|
connectionOfSchema,
|
|
283
898
|
dbOfSchema,
|
|
284
899
|
route,
|
|
285
900
|
execSql,
|
|
286
901
|
executeRaw,
|
|
902
|
+
executeNative,
|
|
903
|
+
isWriteCommand,
|
|
904
|
+
currentSession,
|
|
905
|
+
runWithSession,
|
|
906
|
+
Session,
|
|
907
|
+
NonAtomicWriteError,
|
|
287
908
|
PushdownUnsupportedError,
|
|
288
909
|
RawSqlError,
|
|
910
|
+
NativeCommandError,
|
|
289
911
|
};
|