@sfmc-bds/db-server 0.1.0 → 0.2.0-beta.1
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 +1 -1
- package/dist/db-tables.js +16 -17
- package/dist/db-tables.js.map +7 -1
- package/dist/domain/bridge.js +54 -77
- package/dist/domain/bridge.js.map +7 -1
- package/dist/domain/economy.d.ts +83 -2
- package/dist/domain/economy.d.ts.map +1 -1
- package/dist/domain/economy.js +292 -281
- package/dist/domain/economy.js.map +7 -1
- package/dist/domain/schema.js +480 -426
- package/dist/domain/schema.js.map +7 -1
- package/dist/domain/transaction.js +32 -80
- package/dist/domain/transaction.js.map +7 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +85 -84
- package/dist/env.js.map +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +218 -240
- package/dist/index.js.map +7 -1
- package/dist/lib/http.js +30 -39
- package/dist/lib/http.js.map +7 -1
- package/dist/lib/idempotency-store.js +34 -47
- package/dist/lib/idempotency-store.js.map +7 -1
- package/dist/lib/idempotency.js +8 -13
- package/dist/lib/idempotency.js.map +7 -1
- package/dist/lib/identifiers.js +18 -14
- package/dist/lib/identifiers.js.map +7 -1
- package/dist/lib/log.d.ts +3 -6
- package/dist/lib/log.d.ts.map +1 -1
- package/dist/lib/log.js +13 -14
- package/dist/lib/log.js.map +7 -1
- package/dist/lib/module-state.d.ts.map +1 -1
- package/dist/lib/module-state.js +32 -21
- package/dist/lib/module-state.js.map +7 -1
- package/dist/lib/order-by.d.ts +17 -0
- package/dist/lib/order-by.d.ts.map +1 -0
- package/dist/lib/order-by.js +24 -0
- package/dist/lib/order-by.js.map +7 -0
- package/dist/lib/runtime.js +16 -31
- package/dist/lib/runtime.js.map +7 -1
- package/dist/lib/sql-helpers.js +9 -27
- package/dist/lib/sql-helpers.js.map +7 -1
- package/dist/lib/sqlite.js +40 -52
- package/dist/lib/sqlite.js.map +7 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +154 -195
- package/dist/manifest-loader.js.map +7 -1
- package/dist/manifest.d.ts +3 -7
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +76 -115
- package/dist/manifest.js.map +7 -1
- package/dist/module-auth.d.ts +17 -6
- package/dist/module-auth.d.ts.map +1 -1
- package/dist/module-auth.js +93 -107
- package/dist/module-auth.js.map +7 -1
- package/dist/module-runtime-sync.d.ts +37 -0
- package/dist/module-runtime-sync.d.ts.map +1 -0
- package/dist/module-runtime-sync.js +54 -0
- package/dist/module-runtime-sync.js.map +7 -0
- package/dist/permission-gate.d.ts.map +1 -1
- package/dist/permission-gate.js +55 -86
- package/dist/permission-gate.js.map +7 -1
- package/dist/routes/_shared.d.ts +7 -2
- package/dist/routes/_shared.d.ts.map +1 -1
- package/dist/routes/_shared.js +16 -25
- package/dist/routes/_shared.js.map +7 -1
- package/dist/routes/config.d.ts +11 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +167 -213
- package/dist/routes/config.js.map +7 -1
- package/dist/routes/db-routes.d.ts +6 -10
- package/dist/routes/db-routes.d.ts.map +1 -1
- package/dist/routes/db-routes.js +198 -172
- package/dist/routes/db-routes.js.map +7 -1
- package/dist/routes/health.js +15 -20
- package/dist/routes/health.js.map +7 -1
- package/dist/routes/messages.d.ts.map +1 -1
- package/dist/routes/messages.js +56 -63
- package/dist/routes/messages.js.map +7 -1
- package/dist/routes/module-config-routes.d.ts.map +1 -1
- package/dist/routes/module-config-routes.js +92 -111
- package/dist/routes/module-config-routes.js.map +7 -1
- package/dist/routes/modules.d.ts.map +1 -1
- package/dist/routes/modules.js +82 -88
- package/dist/routes/modules.js.map +7 -1
- package/dist/routes/service-routes.d.ts.map +1 -1
- package/dist/routes/service-routes.js +54 -61
- package/dist/routes/service-routes.js.map +7 -1
- package/dist/runtime.test.js +248 -49
- package/dist/runtime.test.js.map +7 -1
- package/dist/schema-registry.d.ts.map +1 -1
- package/dist/schema-registry.js +119 -160
- package/dist/schema-registry.js.map +7 -1
- package/dist/server.js +69 -72
- package/dist/server.js.map +7 -1
- package/dist/service-registry.d.ts.map +1 -1
- package/dist/service-registry.js +60 -67
- package/dist/service-registry.js.map +7 -1
- package/dist/services/builtin-handlers.d.ts +4 -0
- package/dist/services/builtin-handlers.d.ts.map +1 -1
- package/dist/services/builtin-handlers.js +34 -20
- package/dist/services/builtin-handlers.js.map +7 -1
- package/dist/services/economy-handlers.js +143 -132
- package/dist/services/economy-handlers.js.map +7 -1
- package/dist/tx-runner.d.ts +35 -11
- package/dist/tx-runner.d.ts.map +1 -1
- package/dist/tx-runner.js +293 -250
- package/dist/tx-runner.js.map +7 -1
- package/dist/where.js +71 -94
- package/dist/where.js.map +7 -1
- package/package.json +4 -4
package/dist/tx-runner.js
CHANGED
|
@@ -1,273 +1,316 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tx-runner.ts — 事务 RPC 处理器
|
|
3
|
-
*
|
|
4
|
-
* 协议:
|
|
5
|
-
* POST /api/sfmc/db/tx
|
|
6
|
-
* body = { moduleId, steps: TxStep[] }
|
|
7
|
-
* reply: { ok: true } | { ok: false, step: <index>, error, code }
|
|
8
|
-
*
|
|
9
|
-
* 流程:
|
|
10
|
-
* 1. 校验 moduleId in enabled + permission "db:write:*"
|
|
11
|
-
* 2. BEGIN IMMEDIATE
|
|
12
|
-
* 3. 顺序跑 steps — 任一抛错 → ROLLBACK + reply { ok:false, step:<i>, ... }
|
|
13
|
-
* 4. 全过 → COMMIT + reply { ok:true }
|
|
14
|
-
*
|
|
15
|
-
* step.op:
|
|
16
|
-
* - query { table, opts? }
|
|
17
|
-
* - get { table, id }
|
|
18
|
-
* - insert { table, row }
|
|
19
|
-
* - update { table, id, patch }
|
|
20
|
-
* - delete { table, id, hard }
|
|
21
|
-
* - audit { table, rowId, action, data? } → 平台 sfmc__audit 表
|
|
22
|
-
* - service { name, input } → 派发;失败抛错回退整事务
|
|
23
|
-
*
|
|
24
|
-
* 事务内 service call:不持有 db handle 给 handler(避免并发写) —
|
|
25
|
-
* 实现:handler 是"同步纯函数",返回 result 写回 step 的输出。
|
|
26
|
-
* 若 handler 自己也想写 db,应通过其它接口在事务外做(避免嵌套)。
|
|
27
|
-
*/
|
|
28
1
|
import { randomUUID } from "node:crypto";
|
|
29
2
|
import { compile } from "./where.js";
|
|
30
3
|
import { log } from "./lib/log.js";
|
|
31
4
|
import { DispatchError } from "./service-registry.js";
|
|
32
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
PermissionDeniedError,
|
|
7
|
+
Perm,
|
|
8
|
+
assertModulePermission
|
|
9
|
+
} from "./permission-gate.js";
|
|
10
|
+
import { normalizeOrderBy } from "./lib/order-by.js";
|
|
33
11
|
const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
34
12
|
function identOk(t) {
|
|
35
|
-
|
|
36
|
-
throw new Error(`[tx] invalid table name "${t}"`);
|
|
13
|
+
if (!IDENT.test(t)) throw new Error(`[tx] invalid table name "${t}"`);
|
|
37
14
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
15
|
+
class TxRunner {
|
|
16
|
+
constructor(deps) {
|
|
17
|
+
this.deps = deps;
|
|
18
|
+
}
|
|
19
|
+
deps;
|
|
20
|
+
sessions = /* @__PURE__ */ new Map();
|
|
21
|
+
async run(req) {
|
|
22
|
+
const { moduleId, steps } = req;
|
|
23
|
+
const manifest = this.deps.enabled.get(moduleId);
|
|
24
|
+
if (!manifest) return { ok: false, step: -1, error: "\u6A21\u5757\u672A enabled", code: "forbidden" };
|
|
25
|
+
const traceId = randomUUID().slice(0, 8);
|
|
26
|
+
const results = [];
|
|
27
|
+
try {
|
|
28
|
+
this.deps.db.exec("BEGIN IMMEDIATE");
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return { ok: false, step: -1, error: `BEGIN \u5931\u8D25: ${e.message}`, code: "internal" };
|
|
42
31
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
this.deps.db.exec("BEGIN IMMEDIATE");
|
|
56
|
-
}
|
|
57
|
-
catch (e) {
|
|
58
|
-
return { ok: false, step: -1, error: `BEGIN 失败: ${e.message}`, code: "internal" };
|
|
59
|
-
}
|
|
60
|
-
for (let i = 0; i < steps.length; i++) {
|
|
61
|
-
const step = steps[i];
|
|
62
|
-
try {
|
|
63
|
-
const r = await this.runOne(moduleId, manifest, step);
|
|
64
|
-
results.push(r);
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
this.rollback();
|
|
68
|
-
const code = err instanceof PermissionDeniedError
|
|
69
|
-
? "permission_denied"
|
|
70
|
-
: err instanceof DispatchError &&
|
|
71
|
-
(err.code === "no_such_service" ||
|
|
72
|
-
err.code === "not_in_requires" ||
|
|
73
|
-
err.code === "forbidden" ||
|
|
74
|
-
err.code === "domain_error")
|
|
75
|
-
? err.code
|
|
76
|
-
: err.code === "no_such_service"
|
|
77
|
-
? "no_such_service"
|
|
78
|
-
: err.code === "not_in_requires"
|
|
79
|
-
? "not_in_requires"
|
|
80
|
-
: err.code === "no_such_table"
|
|
81
|
-
? "no_such_table"
|
|
82
|
-
: "internal";
|
|
83
|
-
log.warn(`[tx ${traceId}] step=${i} failed: ${err.message}`);
|
|
84
|
-
return { ok: false, step: i, error: err.message, code };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
try {
|
|
88
|
-
this.deps.db.exec("COMMIT");
|
|
89
|
-
}
|
|
90
|
-
catch (e) {
|
|
91
|
-
this.rollback();
|
|
92
|
-
return { ok: false, step: -1, error: `COMMIT 失败: ${e.message}`, code: "internal" };
|
|
93
|
-
}
|
|
94
|
-
log.info(`[tx ${traceId}] module=${moduleId} ${steps.length} steps OK`);
|
|
95
|
-
return { ok: true, results };
|
|
32
|
+
for (let i = 0; i < steps.length; i++) {
|
|
33
|
+
const step = steps[i];
|
|
34
|
+
try {
|
|
35
|
+
const r = await this.runOne(moduleId, manifest, step);
|
|
36
|
+
results.push(r);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
this.rollback();
|
|
39
|
+
const code = this.mapErrorCode(err);
|
|
40
|
+
log.warn(`[tx ${traceId}] step=${i} failed: ${err.message}`);
|
|
41
|
+
return { ok: false, step: i, error: err.message, code };
|
|
42
|
+
}
|
|
96
43
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// best-effort; fall through
|
|
103
|
-
}
|
|
44
|
+
try {
|
|
45
|
+
this.deps.db.exec("COMMIT");
|
|
46
|
+
} catch (e) {
|
|
47
|
+
this.rollback();
|
|
48
|
+
return { ok: false, step: -1, error: `COMMIT \u5931\u8D25: ${e.message}`, code: "internal" };
|
|
104
49
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
case "service":
|
|
121
|
-
return this.doService(moduleId, manifest, step);
|
|
122
|
-
}
|
|
50
|
+
log.info(`[tx ${traceId}] module=${moduleId} ${steps.length} steps OK`);
|
|
51
|
+
return { ok: true, results };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 交互式事务:begin → step* → commit|rollback。
|
|
55
|
+
* 供 SDK db.tx 在回调内 await query/get/call 真实结果(PR #31 未完成项)。
|
|
56
|
+
* 单连接模型:新会话会先清掉残留会话(避免 BEGIN 锁死)。
|
|
57
|
+
*/
|
|
58
|
+
beginSession(moduleId) {
|
|
59
|
+
const manifest = this.deps.enabled.get(moduleId);
|
|
60
|
+
if (!manifest) return { ok: false, step: -1, error: "\u6A21\u5757\u672A enabled", code: "forbidden" };
|
|
61
|
+
if (this.sessions.size > 0) {
|
|
62
|
+
for (const id of [...this.sessions.keys()]) {
|
|
63
|
+
this.abortSession(id);
|
|
64
|
+
}
|
|
123
65
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
66
|
+
try {
|
|
67
|
+
this.deps.db.exec("BEGIN IMMEDIATE");
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return { ok: false, step: -1, error: `BEGIN \u5931\u8D25: ${e.message}`, code: "internal" };
|
|
128
70
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
71
|
+
const txId = randomUUID();
|
|
72
|
+
this.sessions.set(txId, {
|
|
73
|
+
moduleId,
|
|
74
|
+
results: [],
|
|
75
|
+
openedAt: Date.now()
|
|
76
|
+
});
|
|
77
|
+
log.info(`[tx-session ${txId.slice(0, 8)}] begin module=${moduleId}`);
|
|
78
|
+
return { ok: true, txId };
|
|
79
|
+
}
|
|
80
|
+
async stepSession(txId, moduleId, step) {
|
|
81
|
+
const session = this.sessions.get(txId);
|
|
82
|
+
if (!session) {
|
|
83
|
+
return { ok: false, step: -1, error: "\u4E8B\u52A1\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u7ED3\u675F", code: "internal" };
|
|
133
84
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return; // 平台表 — 始终可读写(平台层)
|
|
137
|
-
if (!this.deps.schema.isRegistered(table)) {
|
|
138
|
-
const err = new Error(`表 "${table}" 未注册到 schema-registry`);
|
|
139
|
-
err.code = "no_such_table";
|
|
140
|
-
throw err;
|
|
141
|
-
}
|
|
85
|
+
if (session.moduleId !== moduleId) {
|
|
86
|
+
return { ok: false, step: session.results.length, error: "moduleId \u4E0E\u4F1A\u8BDD\u4E0D\u7B26", code: "forbidden" };
|
|
142
87
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const limit = typeof step.opts?.limit === "number" ? step.opts?.limit : undefined;
|
|
148
|
-
const offset = typeof step.opts?.offset === "number" ? step.opts?.offset : undefined;
|
|
149
|
-
const whereClause = where.sql;
|
|
150
|
-
const params = [...where.values];
|
|
151
|
-
let extra = "";
|
|
152
|
-
if (orderBy) {
|
|
153
|
-
if (!IDENT.test(orderBy.col))
|
|
154
|
-
throw new Error(`[tx] orderBy bad column "${orderBy.col}"`);
|
|
155
|
-
const dir = orderBy.dir === "desc" ? "DESC" : "ASC";
|
|
156
|
-
extra += ` ORDER BY "${orderBy.col}" ${dir}`;
|
|
157
|
-
}
|
|
158
|
-
if (typeof limit === "number") {
|
|
159
|
-
extra += " LIMIT " + Math.max(0, Math.floor(limit));
|
|
160
|
-
}
|
|
161
|
-
if (typeof offset === "number") {
|
|
162
|
-
extra += " OFFSET " + Math.max(0, Math.floor(offset));
|
|
163
|
-
}
|
|
164
|
-
const sql = `SELECT * FROM "${step.table}" WHERE ${whereClause}${extra}`;
|
|
165
|
-
const rows = this.deps.db.prepare(sql).all(...params);
|
|
166
|
-
return { op: "query", rows };
|
|
88
|
+
const manifest = this.deps.enabled.get(moduleId);
|
|
89
|
+
if (!manifest) {
|
|
90
|
+
this.abortSession(txId);
|
|
91
|
+
return { ok: false, step: session.results.length, error: "\u6A21\u5757\u672A enabled", code: "forbidden" };
|
|
167
92
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (pkCols.length === 1) {
|
|
179
|
-
return { clause: `"${pkCols[0]}" = ?`, values: [id] };
|
|
180
|
-
}
|
|
181
|
-
const parts = String(id).split("|");
|
|
182
|
-
if (parts.length === pkCols.length) {
|
|
183
|
-
return {
|
|
184
|
-
clause: pkCols.map((c) => `"${c}" = ?`).join(" AND "),
|
|
185
|
-
values: parts,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
// 联合主键但只传了首列(频道 id 等):按第一主键列匹配
|
|
189
|
-
return { clause: `"${pkCols[0]}" = ?`, values: [id] };
|
|
93
|
+
const stepIndex = session.results.length;
|
|
94
|
+
try {
|
|
95
|
+
const r = await this.runOne(moduleId, manifest, step);
|
|
96
|
+
session.results.push(r);
|
|
97
|
+
return { ok: true, result: r };
|
|
98
|
+
} catch (err) {
|
|
99
|
+
this.abortSession(txId);
|
|
100
|
+
const code = this.mapErrorCode(err);
|
|
101
|
+
log.warn(`[tx-session ${txId.slice(0, 8)}] step=${stepIndex} failed: ${err.message}`);
|
|
102
|
+
return { ok: false, step: stepIndex, error: err.message, code };
|
|
190
103
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
.get(...where.values);
|
|
197
|
-
return { op: "get", row: row ?? null };
|
|
104
|
+
}
|
|
105
|
+
commitSession(txId, moduleId) {
|
|
106
|
+
const session = this.sessions.get(txId);
|
|
107
|
+
if (!session) {
|
|
108
|
+
return { ok: false, step: -1, error: "\u4E8B\u52A1\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u7ED3\u675F", code: "internal" };
|
|
198
109
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const cols = Object.keys(step.row);
|
|
202
|
-
if (cols.length === 0)
|
|
203
|
-
throw new Error("[tx] insert row 必须至少一个字段");
|
|
204
|
-
for (const c of cols) {
|
|
205
|
-
if (!IDENT.test(c))
|
|
206
|
-
throw new Error(`[tx] invalid column name "${c}"`);
|
|
207
|
-
}
|
|
208
|
-
const placeholders = cols.map(() => "?").join(",");
|
|
209
|
-
const quoted = cols.map((c) => `"${c}"`).join(",");
|
|
210
|
-
const values = cols.map((c) => step.row[c]);
|
|
211
|
-
const sql = `INSERT INTO "${step.table}" (${quoted}) VALUES (${placeholders})`;
|
|
212
|
-
try {
|
|
213
|
-
this.deps.db.prepare(sql).run(...values);
|
|
214
|
-
}
|
|
215
|
-
catch (e) {
|
|
216
|
-
throw new Error(`INSERT ${step.table} 失败: ${e.message}`);
|
|
217
|
-
}
|
|
218
|
-
return { op: "insert", row: { ...step.row } };
|
|
110
|
+
if (session.moduleId !== moduleId) {
|
|
111
|
+
return { ok: false, step: -1, error: "moduleId \u4E0E\u4F1A\u8BDD\u4E0D\u7B26", code: "forbidden" };
|
|
219
112
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
for (const c of cols) {
|
|
226
|
-
if (!IDENT.test(c))
|
|
227
|
-
throw new Error(`[tx] invalid column name "${c}"`);
|
|
228
|
-
}
|
|
229
|
-
const where = this.pkWhere(step.table, step.id);
|
|
230
|
-
const sets = cols.map((c) => `"${c}" = ?`).join(",");
|
|
231
|
-
const params = [...cols.map((c) => step.patch[c]), ...where.values];
|
|
232
|
-
const sql = `UPDATE "${step.table}" SET ${sets} WHERE ${where.clause}`;
|
|
233
|
-
const result = this.deps.db.prepare(sql).run(...params);
|
|
234
|
-
if (result.changes === 0) {
|
|
235
|
-
throw new Error(`UPDATE ${step.table} id=${step.id} 影响 0 行(找不到 / 已删?)`);
|
|
236
|
-
}
|
|
237
|
-
return { op: "update", row: { ...step.patch, id: step.id } };
|
|
113
|
+
try {
|
|
114
|
+
this.deps.db.exec("COMMIT");
|
|
115
|
+
} catch (e) {
|
|
116
|
+
this.abortSession(txId);
|
|
117
|
+
return { ok: false, step: -1, error: `COMMIT \u5931\u8D25: ${e.message}`, code: "internal" };
|
|
238
118
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const hasDeletedAt = cols.find((c) => c.name === "_deleted_at");
|
|
249
|
-
if (!hasDeletedAt) {
|
|
250
|
-
throw new Error(`[tx] ${step.table} 没启用 softDelete,删不掉;请 hard=true 或 schema 设 softDelete=true`);
|
|
251
|
-
}
|
|
252
|
-
const sql = `UPDATE "${step.table}" SET "_deleted_at" = ?, "_version" = COALESCE("_version",0)+1 WHERE ${where.clause}`;
|
|
253
|
-
const r = this.deps.db.prepare(sql).run(Date.now(), ...where.values);
|
|
254
|
-
return { op: "delete", changes: Number(r.changes) };
|
|
119
|
+
const results = session.results;
|
|
120
|
+
this.sessions.delete(txId);
|
|
121
|
+
log.info(`[tx-session ${txId.slice(0, 8)}] commit ${results.length} steps OK`);
|
|
122
|
+
return { ok: true, results };
|
|
123
|
+
}
|
|
124
|
+
rollbackSession(txId, moduleId) {
|
|
125
|
+
const session = this.sessions.get(txId);
|
|
126
|
+
if (!session) {
|
|
127
|
+
return { ok: true };
|
|
255
128
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const dataJson = step.data ? JSON.stringify(step.data) : null;
|
|
259
|
-
const r = this.deps.db
|
|
260
|
-
.prepare(`INSERT INTO sfmc__audit (module_id, table_name, row_id, action, data, created_at)
|
|
261
|
-
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
262
|
-
.run(mod.id, step.table, step.rowId, step.action, dataJson, new Date().toISOString());
|
|
263
|
-
return { op: "audit", changes: Number(r.changes) };
|
|
129
|
+
if (session.moduleId !== moduleId) {
|
|
130
|
+
return { ok: false, step: -1, error: "moduleId \u4E0E\u4F1A\u8BDD\u4E0D\u7B26", code: "forbidden" };
|
|
264
131
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
132
|
+
this.abortSession(txId);
|
|
133
|
+
return { ok: true };
|
|
134
|
+
}
|
|
135
|
+
abortSession(txId) {
|
|
136
|
+
this.sessions.delete(txId);
|
|
137
|
+
this.rollback();
|
|
138
|
+
log.info(`[tx-session ${txId.slice(0, 8)}] aborted`);
|
|
139
|
+
}
|
|
140
|
+
mapErrorCode(err) {
|
|
141
|
+
return err instanceof PermissionDeniedError ? "permission_denied" : err instanceof DispatchError && (err.code === "no_such_service" || err.code === "not_in_requires" || err.code === "forbidden" || err.code === "domain_error") ? err.code : err.code === "no_such_service" ? "no_such_service" : err.code === "not_in_requires" ? "not_in_requires" : err.code === "no_such_table" ? "no_such_table" : "internal";
|
|
142
|
+
}
|
|
143
|
+
rollback() {
|
|
144
|
+
try {
|
|
145
|
+
this.deps.db.exec("ROLLBACK");
|
|
146
|
+
} catch {
|
|
271
147
|
}
|
|
148
|
+
}
|
|
149
|
+
/** 单 step 由对应权限检查 + SQL 路径执行 */
|
|
150
|
+
async runOne(moduleId, manifest, step) {
|
|
151
|
+
switch (step.op) {
|
|
152
|
+
case "query":
|
|
153
|
+
return this.doQuery(moduleId, manifest, step);
|
|
154
|
+
case "get":
|
|
155
|
+
return this.doGet(moduleId, manifest, step);
|
|
156
|
+
case "insert":
|
|
157
|
+
return this.doInsert(moduleId, manifest, step);
|
|
158
|
+
case "update":
|
|
159
|
+
return this.doUpdate(moduleId, manifest, step);
|
|
160
|
+
case "delete":
|
|
161
|
+
return this.doDelete(moduleId, manifest, step);
|
|
162
|
+
case "audit":
|
|
163
|
+
return this.doAudit(moduleId, manifest, step);
|
|
164
|
+
case "service":
|
|
165
|
+
return this.doService(moduleId, manifest, step);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
requireTableRead(mod, table) {
|
|
169
|
+
identOk(table);
|
|
170
|
+
this.assertTableRegistered(table);
|
|
171
|
+
assertModulePermission(mod.id, mod.permissions, Perm.dbRead(table));
|
|
172
|
+
}
|
|
173
|
+
requireTableWrite(mod, table) {
|
|
174
|
+
identOk(table);
|
|
175
|
+
this.assertTableRegistered(table);
|
|
176
|
+
assertModulePermission(mod.id, mod.permissions, Perm.dbWrite(table));
|
|
177
|
+
}
|
|
178
|
+
assertTableRegistered(table) {
|
|
179
|
+
if (table.startsWith("sfmc_")) return;
|
|
180
|
+
if (!this.deps.schema.isRegistered(table)) {
|
|
181
|
+
const err = new Error(`\u8868 "${table}" \u672A\u6CE8\u518C\u5230 schema-registry`);
|
|
182
|
+
err.code = "no_such_table";
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
doQuery(_mid, mod, step) {
|
|
187
|
+
this.requireTableRead(mod, step.table);
|
|
188
|
+
const where = compile(step.opts?.where);
|
|
189
|
+
const orders = normalizeOrderBy(step.opts?.orderBy);
|
|
190
|
+
const limit = typeof step.opts?.limit === "number" ? step.opts?.limit : void 0;
|
|
191
|
+
const offset = typeof step.opts?.offset === "number" ? step.opts?.offset : void 0;
|
|
192
|
+
const whereClause = where.sql;
|
|
193
|
+
const params = [...where.values];
|
|
194
|
+
let extra = "";
|
|
195
|
+
if (orders.length > 0) {
|
|
196
|
+
const parts = [];
|
|
197
|
+
for (const o of orders) {
|
|
198
|
+
if (!IDENT.test(o.col)) throw new Error(`[tx] orderBy bad column "${o.col}"`);
|
|
199
|
+
parts.push(`"${o.col}" ${o.dir === "desc" ? "DESC" : "ASC"}`);
|
|
200
|
+
}
|
|
201
|
+
extra += ` ORDER BY ${parts.join(", ")}`;
|
|
202
|
+
}
|
|
203
|
+
if (typeof limit === "number") {
|
|
204
|
+
extra += " LIMIT " + Math.max(0, Math.floor(limit));
|
|
205
|
+
}
|
|
206
|
+
if (typeof offset === "number") {
|
|
207
|
+
extra += " OFFSET " + Math.max(0, Math.floor(offset));
|
|
208
|
+
}
|
|
209
|
+
const sql = `SELECT * FROM "${step.table}" WHERE ${whereClause}${extra}`;
|
|
210
|
+
const rows = this.deps.db.prepare(sql).all(...params);
|
|
211
|
+
return { op: "query", rows };
|
|
212
|
+
}
|
|
213
|
+
/** 解析主键 WHERE。单列直接等值;联合主键支持 `a|b|c`(与 data-backup 约定一致)。 */
|
|
214
|
+
pkWhere(table, id) {
|
|
215
|
+
const cols = this.deps.db.prepare(`PRAGMA table_info("${table}")`).all();
|
|
216
|
+
const pkCols = cols.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
|
|
217
|
+
if (pkCols.length === 0) {
|
|
218
|
+
throw new Error(`[tx] ${table} \u6CA1\u6709 primary key`);
|
|
219
|
+
}
|
|
220
|
+
if (pkCols.length === 1) {
|
|
221
|
+
return { clause: `"${pkCols[0]}" = ?`, values: [id] };
|
|
222
|
+
}
|
|
223
|
+
const parts = String(id).split("|");
|
|
224
|
+
if (parts.length === pkCols.length) {
|
|
225
|
+
return {
|
|
226
|
+
clause: pkCols.map((c) => `"${c}" = ?`).join(" AND "),
|
|
227
|
+
values: parts
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return { clause: `"${pkCols[0]}" = ?`, values: [id] };
|
|
231
|
+
}
|
|
232
|
+
doGet(_mid, mod, step) {
|
|
233
|
+
this.requireTableRead(mod, step.table);
|
|
234
|
+
const where = this.pkWhere(step.table, step.id);
|
|
235
|
+
const row = this.deps.db.prepare(`SELECT * FROM "${step.table}" WHERE ${where.clause}`).get(...where.values);
|
|
236
|
+
return { op: "get", row: row ?? null };
|
|
237
|
+
}
|
|
238
|
+
doInsert(_mid, mod, step) {
|
|
239
|
+
this.requireTableWrite(mod, step.table);
|
|
240
|
+
const cols = Object.keys(step.row);
|
|
241
|
+
if (cols.length === 0) throw new Error("[tx] insert row \u5FC5\u987B\u81F3\u5C11\u4E00\u4E2A\u5B57\u6BB5");
|
|
242
|
+
for (const c of cols) {
|
|
243
|
+
if (!IDENT.test(c)) throw new Error(`[tx] invalid column name "${c}"`);
|
|
244
|
+
}
|
|
245
|
+
const placeholders = cols.map(() => "?").join(",");
|
|
246
|
+
const quoted = cols.map((c) => `"${c}"`).join(",");
|
|
247
|
+
const values = cols.map((c) => step.row[c]);
|
|
248
|
+
const sql = `INSERT INTO "${step.table}" (${quoted}) VALUES (${placeholders})`;
|
|
249
|
+
try {
|
|
250
|
+
this.deps.db.prepare(sql).run(...values);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
throw new Error(`INSERT ${step.table} \u5931\u8D25: ${e.message}`);
|
|
253
|
+
}
|
|
254
|
+
return { op: "insert", row: { ...step.row } };
|
|
255
|
+
}
|
|
256
|
+
doUpdate(_mid, mod, step) {
|
|
257
|
+
this.requireTableWrite(mod, step.table);
|
|
258
|
+
const cols = Object.keys(step.patch);
|
|
259
|
+
if (cols.length === 0) throw new Error("[tx] update patch \u4E0D\u80FD\u4E3A\u7A7A");
|
|
260
|
+
for (const c of cols) {
|
|
261
|
+
if (!IDENT.test(c)) throw new Error(`[tx] invalid column name "${c}"`);
|
|
262
|
+
}
|
|
263
|
+
const where = this.pkWhere(step.table, step.id);
|
|
264
|
+
const sets = cols.map((c) => `"${c}" = ?`).join(",");
|
|
265
|
+
const params = [...cols.map((c) => step.patch[c]), ...where.values];
|
|
266
|
+
const sql = `UPDATE "${step.table}" SET ${sets} WHERE ${where.clause}`;
|
|
267
|
+
const result = this.deps.db.prepare(sql).run(...params);
|
|
268
|
+
if (result.changes === 0) {
|
|
269
|
+
throw new Error(`UPDATE ${step.table} id=${step.id} \u5F71\u54CD 0 \u884C(\u627E\u4E0D\u5230 / \u5DF2\u5220?)`);
|
|
270
|
+
}
|
|
271
|
+
return { op: "update", row: { ...step.patch, id: step.id } };
|
|
272
|
+
}
|
|
273
|
+
doDelete(_mid, mod, step) {
|
|
274
|
+
this.requireTableWrite(mod, step.table);
|
|
275
|
+
const where = this.pkWhere(step.table, step.id);
|
|
276
|
+
if (step.hard) {
|
|
277
|
+
const sql2 = `DELETE FROM "${step.table}" WHERE ${where.clause}`;
|
|
278
|
+
const r2 = this.deps.db.prepare(sql2).run(...where.values);
|
|
279
|
+
return { op: "delete", changes: Number(r2.changes) };
|
|
280
|
+
}
|
|
281
|
+
const cols = this.deps.db.prepare(`PRAGMA table_info("${step.table}")`).all();
|
|
282
|
+
const hasDeletedAt = cols.find((c) => c.name === "_deleted_at");
|
|
283
|
+
if (!hasDeletedAt) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`[tx] ${step.table} \u6CA1\u542F\u7528 softDelete,\u5220\u4E0D\u6389;\u8BF7 hard=true \u6216 schema \u8BBE softDelete=true`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const sql = `UPDATE "${step.table}" SET "_deleted_at" = ?, "_version" = COALESCE("_version",0)+1 WHERE ${where.clause}`;
|
|
289
|
+
const r = this.deps.db.prepare(sql).run(Date.now(), ...where.values);
|
|
290
|
+
return { op: "delete", changes: Number(r.changes) };
|
|
291
|
+
}
|
|
292
|
+
doAudit(_mid, mod, step) {
|
|
293
|
+
this.requireTableWrite(mod, step.table);
|
|
294
|
+
const dataJson = step.data ? JSON.stringify(step.data) : null;
|
|
295
|
+
const r = this.deps.db.prepare(
|
|
296
|
+
`INSERT INTO sfmc__audit (module_id, table_name, row_id, action, data, created_at)
|
|
297
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
298
|
+
).run(mod.id, step.table, step.rowId, step.action, dataJson, (/* @__PURE__ */ new Date()).toISOString());
|
|
299
|
+
return { op: "audit", changes: Number(r.changes) };
|
|
300
|
+
}
|
|
301
|
+
async doService(_mid, mod, step) {
|
|
302
|
+
assertModulePermission(mod.id, mod.permissions, Perm.service(step.name));
|
|
303
|
+
const result = await this.deps.serviceRegistry.dispatch(
|
|
304
|
+
this.deps.enabled,
|
|
305
|
+
mod.id,
|
|
306
|
+
step.name,
|
|
307
|
+
step.input,
|
|
308
|
+
{ query: this.deps.query, db: this.deps.db }
|
|
309
|
+
);
|
|
310
|
+
return { op: "service", result: result.result };
|
|
311
|
+
}
|
|
272
312
|
}
|
|
273
|
-
|
|
313
|
+
export {
|
|
314
|
+
TxRunner
|
|
315
|
+
};
|
|
316
|
+
//# sourceMappingURL=tx-runner.js.map
|