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.
@@ -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 };
@@ -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
- /** Command JSON → MongoDB 原生驱动调用 */
55
- async function execMongo(db, cmd) {
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 };
@@ -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
- * 事务执行:body(executeOnTx) 的所有 plan 落在同一连接同一事务内,
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
- * 事务执行:显式 BEGIN/COMMIT/ROLLBACK 包住 body 的全部 plan。
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
 
@@ -49,26 +49,57 @@ function create(db, _options = {}) {
49
49
  return { docs, rows, affectedRows };
50
50
  }
51
51
 
52
- return {
53
- kind: 'sqlite',
54
- exec: runStmts,
55
- /** 事务执行:显式 BEGIN/COMMIT/ROLLBACK(better-sqlite3 默认 autocommit,显式开事务安全) */
56
- async withTransaction(body) {
57
- db.exec('BEGIN');
58
- try {
59
- const out = await body(runStmts);
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
- return out;
62
- } catch (e) {
63
- try {
64
- db.exec('ROLLBACK');
65
- } catch (_) {
66
- /* rollback 失败不掩盖原始错误 */
67
- }
68
- throw e;
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
- * 在指定 SQL 源执行原生 SQL(事务内可用;占位符按各后端原生风格)
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
- * mysql/sqlite 用 `?`,postgres 用 `$1..$n`;仅支持 SQL 源(Mongo 源抛 RawSqlError)。
158
- * isWrite=false 取行(rows),true 取影响行数(affectedRows)。对齐 py-store store.execute_raw。
170
+ * 嵌套:内层会话作为嵌套作用域在已有事务上开保存点,内层失败只回滚本层
171
+ * (生命周期仍交外层)。
159
172
  */
160
- async executeRaw(source, sql, params, isWrite) {
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,