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/executors/index.js
CHANGED
|
@@ -38,6 +38,43 @@ function _scalar(rows) {
|
|
|
38
38
|
return typeof v === 'string' ? Number(v) : v;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* 显式 checkout:返回 `{ conn, release }`;`release` 为 async 幂等函数。
|
|
43
|
+
*
|
|
44
|
+
* 池形态(pg Pool → `connect()` / mysql2 Pool → `getConnection()`)失败**直接上抛**,
|
|
45
|
+
* 禁「静默退回 driver 本体」——退回本体可能落到池上另一连接,使事务语义错乱;
|
|
46
|
+
* 单连接形态直用 driver,release 为 no-op。
|
|
47
|
+
* pg / mysql 执行器的显式事务句柄均以此为基础(禁第二套 checkout 路径)。
|
|
48
|
+
*/
|
|
49
|
+
async function openAcquire(driver) {
|
|
50
|
+
if (driver && typeof driver.connect === 'function') {
|
|
51
|
+
const conn = await driver.connect(); // pg Pool:失败直接上抛
|
|
52
|
+
let released = false;
|
|
53
|
+
return {
|
|
54
|
+
conn,
|
|
55
|
+
release: async () => {
|
|
56
|
+
if (released) return;
|
|
57
|
+
released = true;
|
|
58
|
+
// pg Client(connect() 返回自身)无 release(),属单连接形态
|
|
59
|
+
if (typeof conn.release === 'function') conn.release();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (driver && typeof driver.getConnection === 'function') {
|
|
64
|
+
const conn = await driver.getConnection(); // mysql2 Pool:失败直接上抛
|
|
65
|
+
let released = false;
|
|
66
|
+
return {
|
|
67
|
+
conn,
|
|
68
|
+
release: async () => {
|
|
69
|
+
if (released) return;
|
|
70
|
+
released = true;
|
|
71
|
+
conn.release();
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { conn: driver, release: async () => {} }; // 单连接形态
|
|
76
|
+
}
|
|
77
|
+
|
|
41
78
|
/** 中立包络 → Mongo 驱动等价返回值 */
|
|
42
79
|
function shapeResult(cmd, out) {
|
|
43
80
|
switch (cmd.kind) {
|
|
@@ -62,4 +99,4 @@ function shapeResult(cmd, out) {
|
|
|
62
99
|
}
|
|
63
100
|
}
|
|
64
101
|
|
|
65
|
-
module.exports = { createConnection, shapeResult, mongo, mysql, postgres, sqlite };
|
|
102
|
+
module.exports = { createConnection, openAcquire, shapeResult, mongo, mysql, postgres, sqlite };
|
package/src/executors/mongo.js
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* 同时命中「值为 null」与「字段缺失」两类文档,而本 store 的三态契约(F-07/H-09)
|
|
12
12
|
* 要求 `null` 只命中显式 null、`$exists:false` 才命中缺失 —— 这与 SQL 侧
|
|
13
13
|
* `col IS NULL`(显式 null)语义对齐。运算对象(`$ne:null`/`$exists`/`$gt`…)不改写。
|
|
14
|
+
*
|
|
15
|
+
* Mongo session 事务:本模块只提供原语 `openTransaction`(`startSession` +
|
|
16
|
+
* `startTransaction`)与 `execMongo(..., session)` 透传;事务的编排(提交/回滚/
|
|
17
|
+
* 降级声明)由 datasource 层负责。Mongo **无** `SAVEPOINT` 原语,故不提供保存点系列。
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
20
|
/** 递归改写 filter:`field: null`(标量等值)→ `field: {$eq: null, $exists: true}`。 */
|
|
@@ -51,52 +55,96 @@ function _normPipeline(cmd) {
|
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
58
|
+
/** Mongo 连接的 client:db 实例取 .client;MongoClient 返回自身 */
|
|
59
|
+
function _clientOf(connection) {
|
|
60
|
+
if (connection && typeof connection.db === 'function' && typeof connection.collection !== 'function') {
|
|
61
|
+
return connection; // MongoClient
|
|
62
|
+
}
|
|
63
|
+
return (connection && connection.client) || null; // Db.client
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 合并 session 到 options(session 为空时返回原 options 语义,零回归) */
|
|
67
|
+
function _opts(session, base) {
|
|
68
|
+
const o = base ? { ...base } : {};
|
|
69
|
+
if (session) o.session = session;
|
|
70
|
+
return o;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Mongo 事务句柄:startSession + startTransaction
|
|
75
|
+
* - commit/rollback 幂等;release 结束 session;
|
|
76
|
+
* - 无保存点原语(Mongo 不支持 SAVEPOINT),嵌套由 datasource 层降级声明。
|
|
77
|
+
*/
|
|
78
|
+
async function openTransaction(connection) {
|
|
79
|
+
const client = _clientOf(connection);
|
|
80
|
+
const session = client.startSession();
|
|
81
|
+
session.startTransaction();
|
|
82
|
+
let closed = false;
|
|
83
|
+
return {
|
|
84
|
+
session,
|
|
85
|
+
async commit() {
|
|
86
|
+
if (closed) return;
|
|
87
|
+
closed = true;
|
|
88
|
+
await session.commitTransaction();
|
|
89
|
+
},
|
|
90
|
+
async rollback() {
|
|
91
|
+
if (closed) return;
|
|
92
|
+
closed = true;
|
|
93
|
+
await session.abortTransaction();
|
|
94
|
+
},
|
|
95
|
+
async release() {
|
|
96
|
+
await session.endSession();
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Command JSON → MongoDB 原生驱动调用(session 非空时全部操作携带该 session) */
|
|
102
|
+
async function execMongo(db, cmd, session) {
|
|
56
103
|
const coll = db.collection(cmd.collection);
|
|
57
104
|
switch (cmd.kind) {
|
|
58
105
|
case 'find': {
|
|
59
106
|
_normFilter(cmd);
|
|
60
|
-
const opts = cmd.projection ? { projection: cmd.projection } : undefined;
|
|
107
|
+
const opts = _opts(session, cmd.projection ? { projection: cmd.projection } : undefined);
|
|
61
108
|
return coll.find(cmd.filter, opts).toArray();
|
|
62
109
|
}
|
|
63
110
|
case 'aggregate':
|
|
111
|
+
// cmd.options 为原生聚合透传项(executeNative 注入;GQL 路径无此键,零回归)
|
|
64
112
|
_normPipeline(cmd);
|
|
65
|
-
return coll.aggregate(cmd.pipeline).toArray();
|
|
113
|
+
return coll.aggregate(cmd.pipeline, _opts(session, cmd.options)).toArray();
|
|
66
114
|
case 'countDocuments':
|
|
67
115
|
_normFilter(cmd);
|
|
68
|
-
return coll.countDocuments(cmd.filter);
|
|
116
|
+
return coll.countDocuments(cmd.filter, _opts(session));
|
|
69
117
|
case 'findOne': {
|
|
70
118
|
_normFilter(cmd);
|
|
71
|
-
const opts = cmd.projection ? { projection: cmd.projection } : undefined;
|
|
119
|
+
const opts = _opts(session, cmd.projection ? { projection: cmd.projection } : undefined);
|
|
72
120
|
return coll.findOne(cmd.filter, opts);
|
|
73
121
|
}
|
|
74
122
|
case 'insertOne':
|
|
75
|
-
await coll.insertOne(cmd.doc);
|
|
123
|
+
await coll.insertOne(cmd.doc, _opts(session));
|
|
76
124
|
return cmd.doc;
|
|
77
125
|
case 'insertMany':
|
|
78
126
|
if (cmd.upsertById) {
|
|
79
127
|
// 归档幂等(core planArchiveDocs):按 _id 逐条覆盖 —— 「归档成功但删除失败」
|
|
80
128
|
// 的重试不再因 _id 冲突整批失败。SQL 侧由 dialect 的 ON CONFLICT/REPLACE 承接。
|
|
81
129
|
for (const doc of cmd.docs) {
|
|
82
|
-
await coll.replaceOne({ _id: doc._id }, doc, { upsert: true });
|
|
130
|
+
await coll.replaceOne({ _id: doc._id }, doc, _opts(session, { upsert: true }));
|
|
83
131
|
}
|
|
84
132
|
return { insertedCount: cmd.docs.length };
|
|
85
133
|
}
|
|
86
|
-
await coll.insertMany(cmd.docs);
|
|
134
|
+
await coll.insertMany(cmd.docs, _opts(session));
|
|
87
135
|
return { insertedCount: cmd.docs.length };
|
|
88
136
|
case 'findOneAndUpdate':
|
|
89
137
|
_normFilter(cmd);
|
|
90
|
-
return coll.findOneAndUpdate(cmd.filter, cmd.update, cmd.options);
|
|
138
|
+
return coll.findOneAndUpdate(cmd.filter, cmd.update, _opts(session, cmd.options));
|
|
91
139
|
case 'updateMany':
|
|
92
140
|
_normFilter(cmd);
|
|
93
|
-
return coll.updateMany(cmd.filter, cmd.update);
|
|
141
|
+
return coll.updateMany(cmd.filter, cmd.update, _opts(session));
|
|
94
142
|
case 'deleteMany':
|
|
95
143
|
_normFilter(cmd);
|
|
96
|
-
return coll.deleteMany(cmd.filter);
|
|
144
|
+
return coll.deleteMany(cmd.filter, _opts(session));
|
|
97
145
|
default:
|
|
98
146
|
throw new Error(`未支持的命令: ${cmd.kind}`);
|
|
99
147
|
}
|
|
100
148
|
}
|
|
101
149
|
|
|
102
|
-
module.exports = { execMongo };
|
|
150
|
+
module.exports = { execMongo, openTransaction };
|
package/src/executors/mysql.js
CHANGED
|
@@ -42,32 +42,67 @@ function create(driver, _options = {}) {
|
|
|
42
42
|
return { docs, rows, affectedRows };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* 显式事务句柄:池 checkout 专用连接(失败直接上抛,无静默兜底)
|
|
47
|
+
* + beginTransaction + 幂等 commit/rollback;release 归还连接(单连接为 no-op)。
|
|
48
|
+
*/
|
|
49
|
+
async function openTransaction() {
|
|
50
|
+
const { openAcquire } = require('./index'); // 延迟导入:避免与 index 的循环依赖
|
|
51
|
+
const { conn, release } = await openAcquire(driver);
|
|
52
|
+
await conn.beginTransaction();
|
|
53
|
+
let closed = false;
|
|
54
|
+
return {
|
|
55
|
+
exec: (plan) => runStmts(conn, plan),
|
|
56
|
+
async savepoint(name) {
|
|
57
|
+
/* 保存点(嵌套事务用);name 由 Host 生成(sp_<n>),非用户输入。
|
|
58
|
+
MySQL 预备语句协议不支持 SAVEPOINT → 必须走 conn.query */
|
|
59
|
+
await conn.query(`SAVEPOINT ${name}`);
|
|
60
|
+
},
|
|
61
|
+
async releaseSavepoint(name) {
|
|
62
|
+
await conn.query(`RELEASE SAVEPOINT ${name}`);
|
|
63
|
+
},
|
|
64
|
+
async rollbackToSavepoint(name) {
|
|
65
|
+
await conn.query(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
66
|
+
},
|
|
67
|
+
async commit() {
|
|
68
|
+
if (closed) return;
|
|
69
|
+
closed = true;
|
|
70
|
+
await conn.commit();
|
|
71
|
+
},
|
|
72
|
+
async rollback() {
|
|
73
|
+
if (closed) return;
|
|
74
|
+
closed = true;
|
|
75
|
+
await conn.rollback();
|
|
76
|
+
},
|
|
77
|
+
release,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 事务执行:基于 openTransaction(无第二套事务路径),任一失败整体回滚;
|
|
82
|
+
* body(exec, tx) 第二参数为事务句柄(供上层读保存点原语),可选——旧单参写法继续可用 */
|
|
83
|
+
async function withTransaction(body) {
|
|
84
|
+
const tx = await openTransaction();
|
|
85
|
+
try {
|
|
86
|
+
const out = await body(tx.exec, tx);
|
|
87
|
+
await tx.commit();
|
|
88
|
+
return out;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
try {
|
|
91
|
+
await tx.rollback();
|
|
92
|
+
} catch (_) {
|
|
93
|
+
/* rollback 失败不掩盖原始错误 */
|
|
94
|
+
}
|
|
95
|
+
throw e;
|
|
96
|
+
} finally {
|
|
97
|
+
await tx.release();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
45
101
|
return {
|
|
46
102
|
kind: 'mysql',
|
|
47
103
|
exec: (plan) => runStmts(driver, plan),
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* 成功 commit / 失败 rollback。池自动取专用连接(结束归还)。
|
|
51
|
-
*/
|
|
52
|
-
async withTransaction(body) {
|
|
53
|
-
const conn =
|
|
54
|
-
typeof driver.getConnection === 'function' ? await driver.getConnection() : driver;
|
|
55
|
-
try {
|
|
56
|
-
await conn.beginTransaction();
|
|
57
|
-
const out = await body((plan) => runStmts(conn, plan));
|
|
58
|
-
await conn.commit();
|
|
59
|
-
return out;
|
|
60
|
-
} catch (e) {
|
|
61
|
-
try {
|
|
62
|
-
await conn.rollback();
|
|
63
|
-
} catch (_) {
|
|
64
|
-
/* rollback 失败不掩盖原始错误 */
|
|
65
|
-
}
|
|
66
|
-
throw e;
|
|
67
|
-
} finally {
|
|
68
|
-
if (conn !== driver && typeof conn.release === 'function') conn.release();
|
|
69
|
-
}
|
|
70
|
-
},
|
|
104
|
+
withTransaction,
|
|
105
|
+
openTransaction,
|
|
71
106
|
};
|
|
72
107
|
}
|
|
73
108
|
|
|
@@ -32,44 +32,66 @@ function create(driver, _options = {}) {
|
|
|
32
32
|
return { docs, rows, affectedRows };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 显式事务句柄:池 checkout 专用 client(失败直接上抛,无静默兜底)
|
|
37
|
+
* + BEGIN + 幂等 commit/rollback;release 归还连接(单连接为 no-op)。
|
|
38
|
+
*/
|
|
39
|
+
async function openTransaction() {
|
|
40
|
+
const { openAcquire } = require('./index'); // 延迟导入:避免与 index 的循环依赖
|
|
41
|
+
const { conn, release } = await openAcquire(driver);
|
|
42
|
+
await conn.query('BEGIN');
|
|
43
|
+
let closed = false;
|
|
44
|
+
return {
|
|
45
|
+
exec: (plan) => runStmts(conn, plan),
|
|
46
|
+
async savepoint(name) {
|
|
47
|
+
/* 保存点(嵌套事务用);name 由 Host 生成(sp_<n>),非用户输入 */
|
|
48
|
+
await conn.query(`SAVEPOINT ${name}`);
|
|
49
|
+
},
|
|
50
|
+
async releaseSavepoint(name) {
|
|
51
|
+
await conn.query(`RELEASE SAVEPOINT ${name}`);
|
|
52
|
+
},
|
|
53
|
+
async rollbackToSavepoint(name) {
|
|
54
|
+
await conn.query(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
55
|
+
},
|
|
56
|
+
async commit() {
|
|
57
|
+
if (closed) return;
|
|
58
|
+
closed = true;
|
|
59
|
+
await conn.query('COMMIT');
|
|
60
|
+
},
|
|
61
|
+
async rollback() {
|
|
62
|
+
if (closed) return;
|
|
63
|
+
closed = true;
|
|
64
|
+
await conn.query('ROLLBACK');
|
|
65
|
+
},
|
|
66
|
+
release,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 事务执行:基于 openTransaction(无第二套事务路径),任一失败整体回滚;
|
|
71
|
+
* body(exec, tx) 第二参数为事务句柄(供上层读保存点原语),可选——旧单参写法继续可用 */
|
|
72
|
+
async function withTransaction(body) {
|
|
73
|
+
const tx = await openTransaction();
|
|
74
|
+
try {
|
|
75
|
+
const out = await body(tx.exec, tx);
|
|
76
|
+
await tx.commit();
|
|
77
|
+
return out;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
try {
|
|
80
|
+
await tx.rollback();
|
|
81
|
+
} catch (_) {
|
|
82
|
+
/* rollback 失败不掩盖原始错误 */
|
|
83
|
+
}
|
|
84
|
+
throw e;
|
|
85
|
+
} finally {
|
|
86
|
+
await tx.release();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
35
90
|
return {
|
|
36
91
|
kind: 'postgres',
|
|
37
92
|
exec: (plan) => runStmts(driver, plan),
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* Pool 自动 checkout 专用 client(`release()` 归还);Client 直连直接用。
|
|
41
|
-
*/
|
|
42
|
-
async withTransaction(body) {
|
|
43
|
-
let conn = driver;
|
|
44
|
-
let release = null;
|
|
45
|
-
if (typeof driver.connect === 'function') {
|
|
46
|
-
try {
|
|
47
|
-
const c = await driver.connect();
|
|
48
|
-
// Pool.connect() → 专用 Client(带 release);Client.connect() → 自身
|
|
49
|
-
if (c && typeof c.query === 'function') {
|
|
50
|
-
conn = c;
|
|
51
|
-
if (c !== driver && typeof c.release === 'function') release = () => c.release();
|
|
52
|
-
}
|
|
53
|
-
} catch (_) {
|
|
54
|
-
/* checkout 失败退回 driver 本体,事务语义由 BEGIN/COMMIT 保证 */
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
await conn.query('BEGIN');
|
|
59
|
-
const out = await body((plan) => runStmts(conn, plan));
|
|
60
|
-
await conn.query('COMMIT');
|
|
61
|
-
return out;
|
|
62
|
-
} catch (e) {
|
|
63
|
-
try {
|
|
64
|
-
await conn.query('ROLLBACK');
|
|
65
|
-
} catch (_) {
|
|
66
|
-
/* rollback 失败不掩盖原始错误 */
|
|
67
|
-
}
|
|
68
|
-
throw e;
|
|
69
|
-
} finally {
|
|
70
|
-
if (release) release();
|
|
71
|
-
}
|
|
72
|
-
},
|
|
93
|
+
withTransaction,
|
|
94
|
+
openTransaction,
|
|
73
95
|
};
|
|
74
96
|
}
|
|
75
97
|
|
package/src/executors/sqlite.js
CHANGED
|
@@ -49,26 +49,57 @@ function create(db, _options = {}) {
|
|
|
49
49
|
return { docs, rows, affectedRows };
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
exec
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
/** 显式事务句柄:BEGIN + 幂等 commit/rollback;release 为 no-op(单连接不归还) */
|
|
53
|
+
async function openTransaction() {
|
|
54
|
+
db.exec('BEGIN');
|
|
55
|
+
let closed = false;
|
|
56
|
+
return {
|
|
57
|
+
exec: runStmts,
|
|
58
|
+
async savepoint(name) {
|
|
59
|
+
/* 保存点(嵌套事务用);name 由 Host 生成(sp_<n>),非用户输入 */
|
|
60
|
+
db.exec(`SAVEPOINT ${name}`);
|
|
61
|
+
},
|
|
62
|
+
async releaseSavepoint(name) {
|
|
63
|
+
db.exec(`RELEASE SAVEPOINT ${name}`);
|
|
64
|
+
},
|
|
65
|
+
async rollbackToSavepoint(name) {
|
|
66
|
+
db.exec(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
67
|
+
},
|
|
68
|
+
async commit() {
|
|
69
|
+
if (closed) return;
|
|
70
|
+
closed = true;
|
|
60
71
|
db.exec('COMMIT');
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
},
|
|
73
|
+
async rollback() {
|
|
74
|
+
if (closed) return;
|
|
75
|
+
closed = true;
|
|
76
|
+
db.exec('ROLLBACK');
|
|
77
|
+
},
|
|
78
|
+
async release() {},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 事务执行:基于 openTransaction(无第二套事务路径),任一失败整体回滚;
|
|
83
|
+
* body(exec, tx) 第二参数为事务句柄(供上层读保存点原语),可选——旧单参写法继续可用 */
|
|
84
|
+
async function withTransaction(body) {
|
|
85
|
+
const tx = await openTransaction();
|
|
86
|
+
try {
|
|
87
|
+
const out = await body(tx.exec, tx);
|
|
88
|
+
await tx.commit();
|
|
89
|
+
return out;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
try {
|
|
92
|
+
await tx.rollback();
|
|
93
|
+
} catch (_) {
|
|
94
|
+
/* rollback 失败不掩盖原始错误 */
|
|
69
95
|
}
|
|
70
|
-
|
|
71
|
-
|
|
96
|
+
throw e;
|
|
97
|
+
} finally {
|
|
98
|
+
await tx.release();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { kind: 'sqlite', exec: runStmts, withTransaction, openTransaction };
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
module.exports = { create };
|
package/src/index.js
CHANGED
|
@@ -25,6 +25,7 @@ const { AsyncLocalStorage } = require('node:async_hooks');
|
|
|
25
25
|
|
|
26
26
|
const crud = require('./crud');
|
|
27
27
|
const datasource = require('./datasource');
|
|
28
|
+
const { Session, NonAtomicWriteError } = require('./datasource');
|
|
28
29
|
const ddl = require('./ddl');
|
|
29
30
|
const executors = require('./executors');
|
|
30
31
|
const feedback = require('./feedback');
|
|
@@ -146,21 +147,87 @@ class Store {
|
|
|
146
147
|
* fn 内 executeRaw / CRUD 均落到该源的事务连接(commit/rollback 一体);
|
|
147
148
|
* Mongo 源或执行器未实现 withTransaction 时按原样执行(跨源无法原子),
|
|
148
149
|
* 绝不静默假装已事务化。单源场景 source 传 'default'。
|
|
150
|
+
*
|
|
151
|
+
* 同源嵌套 transaction 会开保存点(内层失败只回滚本层);句柄无保存点原语时
|
|
152
|
+
* 降级并入外层并发 nested_savepoint_unsupported。
|
|
149
153
|
*/
|
|
150
154
|
async transaction(source, fn) {
|
|
151
155
|
return datasource.runInTransaction(source, fn);
|
|
152
156
|
}
|
|
153
157
|
|
|
154
158
|
/**
|
|
155
|
-
*
|
|
159
|
+
* 会话(工作单元):回调式,退出统一提交 / 异常统一回滚
|
|
160
|
+
*
|
|
161
|
+
* 用法:
|
|
162
|
+
* await store.session(async (s) => {
|
|
163
|
+
* await s.insert('Order', { ... });
|
|
164
|
+
* await s.update('Account', cond, { ... });
|
|
165
|
+
* });
|
|
166
|
+
*
|
|
167
|
+
* 约束:同一会话内写命令只允许落在**单一数据源**;跨源写退出时抛
|
|
168
|
+
* NonAtomicWriteError(先全部回滚,绝不提交半截)。
|
|
156
169
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
170
|
+
* 嵌套:内层会话作为嵌套作用域在已有事务上开保存点,内层失败只回滚本层
|
|
171
|
+
* (生命周期仍交外层)。
|
|
159
172
|
*/
|
|
160
|
-
async
|
|
173
|
+
async session(fn) {
|
|
174
|
+
if (typeof fn !== 'function') {
|
|
175
|
+
throw new TypeError('store.session(fn) 需要回调函数:await store.session(async (s) => { ... })');
|
|
176
|
+
}
|
|
177
|
+
const s = new Session();
|
|
178
|
+
const parent = datasource.currentSession();
|
|
179
|
+
if (parent !== null) {
|
|
180
|
+
// 嵌套:生命周期交外层;本层作为嵌套作用域(保存点隔离,失败只回滚本层)
|
|
181
|
+
s.bindOuter(parent);
|
|
182
|
+
const scope = parent.pushScope();
|
|
183
|
+
try {
|
|
184
|
+
const out = await fn(s);
|
|
185
|
+
await parent.popScope(scope, false);
|
|
186
|
+
return out;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
await parent.popScope(scope, true);
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return datasource.runWithSession(s, async () => {
|
|
193
|
+
try {
|
|
194
|
+
const out = await fn(s);
|
|
195
|
+
await s.exit(null);
|
|
196
|
+
return out;
|
|
197
|
+
} catch (err) {
|
|
198
|
+
await s.exit(err);
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 在指定 SQL 源执行原生 SQL(事务内可用;编译由 core rawStmtCompile 完成)
|
|
206
|
+
*
|
|
207
|
+
* 两档参数风格:位置档(params 为数组/null)→ SQL 原样透传,占位符为各后端
|
|
208
|
+
* 原生风格(mysql/sqlite 用 `?`,postgres 用 `$1..$n`);命名档(params 为对象)
|
|
209
|
+
* → SQL 文本中的 `:name` 编译为方言占位符(同名复用、跳过 `::` cast / 引号 /
|
|
210
|
+
* 注释边界;缺名 / 多余名显式报错)。isWrite 缺省时按 SQL 首词推断(读白名单外
|
|
211
|
+
* 一律按写——安全方向)。仅支持 SQL 源(Mongo 源抛 RawSqlError)。
|
|
212
|
+
* 对齐 py-store store.execute_raw。
|
|
213
|
+
*/
|
|
214
|
+
async executeRaw(source, sql, params = null, isWrite = null) {
|
|
161
215
|
return datasource.executeRaw(source, sql, params, isWrite);
|
|
162
216
|
}
|
|
163
217
|
|
|
218
|
+
/**
|
|
219
|
+
* 在指定 Mongo 源执行原生聚合管道(事务内可用;对标 SQL 侧 executeRaw)
|
|
220
|
+
*
|
|
221
|
+
* pipeline 为原生聚合管道(数组),options 为驱动原生透传项(allowDiskUse /
|
|
222
|
+
* batchSize / hint / maxTimeMS…,宿主不做白名单)。事务 / 会话作用域内自动透传
|
|
223
|
+
* session(由事务强制接管,options.session 不可覆盖);统一按读路径解析,
|
|
224
|
+
* $merge / $out 写管道请自行开事务。仅支持 Mongo 源(SQL 源抛 NativeCommandError
|
|
225
|
+
* 并指引 executeRaw)。返回 { rows }。对齐 py-store store.execute_native。
|
|
226
|
+
*/
|
|
227
|
+
async executeNative(source, collection, pipeline = [], options = null) {
|
|
228
|
+
return datasource.executeNative(source, collection, pipeline, options);
|
|
229
|
+
}
|
|
230
|
+
|
|
164
231
|
/** 从已注册 schema def 生成指定后端 DDL 文本(纯函数,不连库、不回写;铁律 6) */
|
|
165
232
|
generateDdl(backend, names) {
|
|
166
233
|
return ddl.generate(backend, names);
|
|
@@ -240,6 +307,8 @@ Store.prototype.PermissionError = permission.PermissionError;
|
|
|
240
307
|
Store.prototype.ProfileViolation = crud.ProfileViolation;
|
|
241
308
|
/** 原生 SQL 入口错误(实例可被 store.RawSqlError 捕获) */
|
|
242
309
|
Store.prototype.RawSqlError = datasource.RawSqlError;
|
|
310
|
+
/** 原生 Mongo 命令入口错误(实例可被 store.NativeCommandError 捕获) */
|
|
311
|
+
Store.prototype.NativeCommandError = datasource.NativeCommandError;
|
|
243
312
|
|
|
244
313
|
const store = new Store();
|
|
245
314
|
|
|
@@ -331,10 +400,13 @@ module.exports = {
|
|
|
331
400
|
store,
|
|
332
401
|
Store,
|
|
333
402
|
text2query,
|
|
403
|
+
Session,
|
|
404
|
+
NonAtomicWriteError,
|
|
334
405
|
PermissionError: permission.PermissionError,
|
|
335
406
|
ProfileViolation: crud.ProfileViolation,
|
|
336
407
|
PushdownUnsupportedError: datasource.PushdownUnsupportedError,
|
|
337
408
|
RawSqlError: datasource.RawSqlError,
|
|
409
|
+
NativeCommandError: datasource.NativeCommandError,
|
|
338
410
|
datasource,
|
|
339
411
|
ddl,
|
|
340
412
|
schema,
|