nodejs-store 1.1.0 → 2.0.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 +7 -12
- package/package.json +2 -2
- package/src/crud/exec.js +8 -42
- package/src/crud/index.js +4 -4
- package/src/crud/mutation.js +14 -12
- package/src/crud/query.js +1 -2
- package/src/crud/write.js +1 -1
- package/src/datasource.js +7 -5
- package/src/executors/_values.js +55 -0
- package/src/executors/index.js +6 -1
- package/src/executors/mongo.js +102 -0
- package/src/executors/mysql.js +3 -2
- package/src/executors/postgres.js +2 -1
- package/src/executors/sqlite.js +6 -1
- package/src/index.js +21 -14
- package/src/introspect/_shared.js +28 -0
- package/src/introspect/mysql.js +5 -15
- package/src/introspect/postgres.js +5 -15
- package/src/permission.js +51 -0
- package/src/schema.js +8 -8
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ This is the Node.js port of [`py-store`](https://github.com/coenddt/py-store)
|
|
|
10
10
|
| --- | --- |
|
|
11
11
|
| MongoDB | native aggregation pipeline (`find`/`aggregate`/`$lookup`) |
|
|
12
12
|
| MySQL | parameterized SQL, `information_schema` introspection |
|
|
13
|
-
| SQLite | parameterized SQL, `sqlite_master` + `PRAGMA` introspection |
|
|
13
|
+
| SQLite | parameterized SQL, `sqlite_master` + `PRAGMA` introspection. **Sync driver** (`better-sqlite3`): calls block the event loop by design — for high-concurrency hot paths prefer MySQL/PostgreSQL/MongoDB, or isolate SQLite in a dedicated process |
|
|
14
14
|
| PostgreSQL | parameterized SQL (`$n`), `RETURNING` support |
|
|
15
15
|
|
|
16
16
|
GQL tree queries compile to a single native query per backend — never hand-write `$lookup` or raw SQL again.
|
|
@@ -123,7 +123,11 @@ Model($condition:@c0,$sort:@s1,$skip:@sk,$limit:@l1) {
|
|
|
123
123
|
|
|
124
124
|
- Values come from the params object: `{ c0: {...}, s1: {...} }`.
|
|
125
125
|
- Object sub-fields use dot notation; relations are declared in the schema (`type: 'many' | 'one'`) and resolved automatically — **do not hand-write `$lookup`**.
|
|
126
|
-
|
|
126
|
+
|
|
127
|
+
> **Breaking change**: user `$pipeline` passthrough and `store.aggregate()` were removed
|
|
128
|
+
> (raw aggregation escape hatch). A GQL containing `$pipeline` now fails explicitly instead
|
|
129
|
+
> of being silently ignored. Use `$condition`/`$sort`/`$skip`/`$limit` + relations; normalized
|
|
130
|
+
> aggregation (`$group`/`$sum`) is being redesigned and will return under a single GQL syntax.
|
|
127
131
|
|
|
128
132
|
## Query & write API
|
|
129
133
|
|
|
@@ -142,7 +146,6 @@ await store.updateMany('Post', { type: t }, { status: 'live' });
|
|
|
142
146
|
const r = await store.remove('Post', { _id: pid }); // archives to <collection>_deleted first
|
|
143
147
|
await store.mutation('Post', { ... }); // smart upsert + recursive relation children
|
|
144
148
|
await store.upsert('Post', { code: 'A1' }, { ... }); // explicit-condition upsert (no relation handling)
|
|
145
|
-
const rows = await store.aggregate('Post', pipeline); // native aggregation
|
|
146
149
|
```
|
|
147
150
|
|
|
148
151
|
Notes:
|
|
@@ -203,7 +206,7 @@ a missing context and always passes. `setRequireContext(false)` restores the def
|
|
|
203
206
|
},
|
|
204
207
|
computes: {
|
|
205
208
|
total: { type: 'float', depends: ['amount'], fn: (d) => d.amount * 1.1 },
|
|
206
|
-
itemCount: { type: 'int',
|
|
209
|
+
itemCount: { type: 'int', agg: { $count: 'items' } },
|
|
207
210
|
},
|
|
208
211
|
indexes: [
|
|
209
212
|
{ keys: { status: 1 } },
|
|
@@ -256,13 +259,6 @@ const defs = await store.syncSchema({
|
|
|
256
259
|
});
|
|
257
260
|
```
|
|
258
261
|
|
|
259
|
-
### `store.setAllowUserPipeline(allow = true)`
|
|
260
|
-
|
|
261
|
-
Registry-level guard for user-supplied `$pipeline` passthrough. Default is **allow**
|
|
262
|
-
(backward compatible); AI / 问数 hosts should call `store.setAllowUserPipeline(false)` as
|
|
263
|
-
defense in depth. `store.setRequireContext(...)` (fail-secure mode) is documented under
|
|
264
|
-
[Fail-secure mode](#fail-secure-mode-opt-in).
|
|
265
|
-
|
|
266
262
|
### `store.setFeedbackSink(fn)`
|
|
267
263
|
|
|
268
264
|
Take over the unified feedback channel used for fallback / degradation / interception
|
|
@@ -284,7 +280,6 @@ The package re-exports its building blocks for advanced hosts:
|
|
|
284
280
|
```js
|
|
285
281
|
const {
|
|
286
282
|
init, store, Store,
|
|
287
|
-
aggregate, // standalone aggregate(schemaName, pipeline, routeOverride)
|
|
288
283
|
PermissionError, // thrown on denied access (status = 403)
|
|
289
284
|
PushdownUnsupportedError, // thrown when a command cannot be safely pushed down
|
|
290
285
|
datasource, schema, permission, crud, executors, feedback, introspect,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodejs-store",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Lightweight multi-backend data layer (MongoDB / MySQL / SQLite / PostgreSQL): pure JSON schemas, GQL tree queries compiled to a single query, computed columns, soft-delete and role-based access control",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"mongodb": "^6.21.0",
|
|
44
44
|
"mysql2": "^3.24.4",
|
|
45
45
|
"pg": "^8.23.0",
|
|
46
|
-
"rust-store-node": "^
|
|
46
|
+
"rust-store-node": "^2.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@eslint/js": "^9.39.2",
|
package/src/crud/exec.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
const { PermissionError, getContext } = require('../permission');
|
|
16
16
|
const datasource = require('../datasource');
|
|
17
|
+
const { execMongo } = require('../executors/mongo');
|
|
17
18
|
const { get: _getSchema } = require('../schema');
|
|
18
19
|
|
|
19
20
|
const _PHASE1_IDS = /^\{\{phase1\.ids\}\}$/;
|
|
@@ -29,6 +30,11 @@ const _PERM_PREFIX = 'ERR_PERMISSION:';
|
|
|
29
30
|
/** 设置数据源连接映射(对 `../datasource` 的路由入口做包内透出) */
|
|
30
31
|
const setConnections = datasource.setConnections;
|
|
31
32
|
|
|
33
|
+
/** 单库简写:等价于 `setConnections({ default: db })`(对齐 py_store.crud.exec.set_db) */
|
|
34
|
+
function setDb(db) {
|
|
35
|
+
datasource.setConnections({ [datasource.DEFAULT_SOURCE]: db });
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
/** 按 schema 的 timestamps 单位产出当前时间戳('s' → 秒,其余/未启用 → 毫秒) */
|
|
33
39
|
function _nowFor(schemaName) {
|
|
34
40
|
const unit = _getSchema(schemaName).timestampUnit;
|
|
@@ -54,54 +60,13 @@ function _call(fn) {
|
|
|
54
60
|
|
|
55
61
|
// ─── 命令执行(唯一 IO 边界) ────────────────────────────────
|
|
56
62
|
|
|
57
|
-
/** Command JSON → MongoDB 原生驱动调用 */
|
|
58
|
-
async function _execMongo(db, cmd) {
|
|
59
|
-
const coll = db.collection(cmd.collection);
|
|
60
|
-
switch (cmd.kind) {
|
|
61
|
-
case 'find': {
|
|
62
|
-
const opts = cmd.projection ? { projection: cmd.projection } : undefined;
|
|
63
|
-
return coll.find(cmd.filter, opts).toArray();
|
|
64
|
-
}
|
|
65
|
-
case 'aggregate':
|
|
66
|
-
return coll.aggregate(cmd.pipeline).toArray();
|
|
67
|
-
case 'countDocuments':
|
|
68
|
-
return coll.countDocuments(cmd.filter);
|
|
69
|
-
case 'findOne': {
|
|
70
|
-
const opts = cmd.projection ? { projection: cmd.projection } : undefined;
|
|
71
|
-
return coll.findOne(cmd.filter, opts);
|
|
72
|
-
}
|
|
73
|
-
case 'insertOne':
|
|
74
|
-
await coll.insertOne(cmd.doc);
|
|
75
|
-
return cmd.doc;
|
|
76
|
-
case 'insertMany':
|
|
77
|
-
if (cmd.upsertById) {
|
|
78
|
-
// 归档幂等(core planArchiveDocs):按 _id 逐条覆盖 —— 「归档成功但删除失败」
|
|
79
|
-
// 的重试不再因 _id 冲突整批失败。SQL 侧由 dialect 的 ON CONFLICT/REPLACE 承接。
|
|
80
|
-
for (const doc of cmd.docs) {
|
|
81
|
-
await coll.replaceOne({ _id: doc._id }, doc, { upsert: true });
|
|
82
|
-
}
|
|
83
|
-
return { insertedCount: cmd.docs.length };
|
|
84
|
-
}
|
|
85
|
-
await coll.insertMany(cmd.docs);
|
|
86
|
-
return { insertedCount: cmd.docs.length };
|
|
87
|
-
case 'findOneAndUpdate':
|
|
88
|
-
return coll.findOneAndUpdate(cmd.filter, cmd.update, cmd.options);
|
|
89
|
-
case 'updateMany':
|
|
90
|
-
return coll.updateMany(cmd.filter, cmd.update);
|
|
91
|
-
case 'deleteMany':
|
|
92
|
-
return coll.deleteMany(cmd.filter);
|
|
93
|
-
default:
|
|
94
|
-
throw new Error(`未支持的命令: ${cmd.kind}`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
63
|
/** 在指定数据源上执行命令(Mongo 走原生驱动,SQL 走 translate → exec;
|
|
99
64
|
* 事务作用域内经 datasource.connectionFor 落到事务专用连接) */
|
|
100
65
|
async function _execOn(source, cmd) {
|
|
101
66
|
const connection = datasource.connectionFor(source);
|
|
102
67
|
const db = datasource.mongoDb(connection, source, cmd.namespace ?? null);
|
|
103
68
|
if (db) {
|
|
104
|
-
return
|
|
69
|
+
return execMongo(db, cmd);
|
|
105
70
|
}
|
|
106
71
|
return datasource.execSql(source, connection, cmd);
|
|
107
72
|
}
|
|
@@ -147,6 +112,7 @@ function resolvePlaceholders(command, { ids = null, steps = [] } = {}) {
|
|
|
147
112
|
|
|
148
113
|
module.exports = {
|
|
149
114
|
setConnections,
|
|
115
|
+
setDb,
|
|
150
116
|
_nowFor,
|
|
151
117
|
_ctx,
|
|
152
118
|
_call,
|
package/src/crud/index.js
CHANGED
|
@@ -16,17 +16,18 @@
|
|
|
16
16
|
* - [`id`]:ID 生成与 mutation ID 池遍历
|
|
17
17
|
* - [`query`]:读路径
|
|
18
18
|
* - [`write`]:写路径
|
|
19
|
-
* - [`mutation`]:mutation / upsert
|
|
19
|
+
* - [`mutation`]:mutation / upsert
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
const { setConnections, _nowFor, _ctx, _call, _exec, _substitute, resolvePlaceholders } = require('./exec');
|
|
22
|
+
const { setConnections, setDb, _nowFor, _ctx, _call, _exec, _substitute, resolvePlaceholders } = require('./exec');
|
|
23
23
|
const { _generateId, _truthy, _newIdPool } = require('./id');
|
|
24
24
|
const { query, queryOne, queryWithCount, queryFederated } = require('./query');
|
|
25
25
|
const { insert, insertMany, update, updateMany, remove, exists, count } = require('./write');
|
|
26
|
-
const { mutation, upsert
|
|
26
|
+
const { mutation, upsert } = require('./mutation');
|
|
27
27
|
|
|
28
28
|
module.exports = {
|
|
29
29
|
setConnections,
|
|
30
|
+
setDb,
|
|
30
31
|
query,
|
|
31
32
|
queryOne,
|
|
32
33
|
queryWithCount,
|
|
@@ -40,7 +41,6 @@ module.exports = {
|
|
|
40
41
|
count,
|
|
41
42
|
mutation,
|
|
42
43
|
upsert,
|
|
43
|
-
aggregate,
|
|
44
44
|
// ── Host 契约件(供跨语言同构契约测试与高级用法;下划线表示内部语义) ──
|
|
45
45
|
_substitute,
|
|
46
46
|
resolvePlaceholders,
|
package/src/crud/mutation.js
CHANGED
|
@@ -6,15 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
const { core: _core, get: _getSchema } = require('../schema');
|
|
8
8
|
const datasource = require('../datasource');
|
|
9
|
+
const { emit: _emitFeedback } = require('../feedback');
|
|
9
10
|
const { _call, _ctx, _exec, _nowFor, resolvePlaceholders } = require('./exec');
|
|
10
11
|
const { _generateId, _newIdPool } = require('./id');
|
|
11
12
|
|
|
12
13
|
/** mutation 单条:规划步骤序列 → 依序执行 + 父子 _id 占位符回填 */
|
|
13
|
-
async function _mutationOne(schemaName, data, routeOverride = null) {
|
|
14
|
+
async function _mutationOne(schemaName, data, now, routeOverride = null) {
|
|
14
15
|
const plan = _call(() =>
|
|
15
|
-
_core.planMutation(schemaName, data,
|
|
16
|
+
_core.planMutation(schemaName, data, now, _newIdPool(schemaName, data), _ctx(),
|
|
16
17
|
routeOverride));
|
|
17
18
|
|
|
19
|
+
// §11.4 静默点收口:规划期降级(如关系不可读被跳过)走统一反馈通道,禁止静默失守
|
|
20
|
+
for (const d of plan.degraded || []) {
|
|
21
|
+
_emitFeedback({ ...(d || {}), type: 'mutation_degraded' });
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
const runSteps = async () => {
|
|
19
25
|
const resolved = [];
|
|
20
26
|
let rootResult = null;
|
|
@@ -49,9 +55,13 @@ async function mutation(schemaName, data, routeOverride = null) {
|
|
|
49
55
|
|
|
50
56
|
if (!items.length) return isArray ? [] : null;
|
|
51
57
|
|
|
58
|
+
// §11.3 确定性输入:一次 mutation 调用共用一个 now
|
|
59
|
+
// (数组内多条 + 父子步骤 + 默认值 / 计算列全部同值)
|
|
60
|
+
const now = _nowFor(schemaName);
|
|
61
|
+
|
|
52
62
|
const results = [];
|
|
53
63
|
for (const item of items) {
|
|
54
|
-
results.push(await _mutationOne(schemaName, item, routeOverride));
|
|
64
|
+
results.push(await _mutationOne(schemaName, item, now, routeOverride));
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
return isArray ? results : results[0];
|
|
@@ -72,12 +82,4 @@ async function upsert(schemaName, condition, data, options = null, routeOverride
|
|
|
72
82
|
return result ? _call(() => _core.applyWriteDefaults(schemaName, result)) : null;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
/** 对指定 schema 执行 MongoDB 原生聚合查询 */
|
|
78
|
-
async function aggregate(schemaName, pipeline, routeOverride = null) {
|
|
79
|
-
const cmd = _call(() => _core.planAggregate(schemaName, pipeline ?? [], routeOverride));
|
|
80
|
-
return _exec(cmd);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
module.exports = { mutation, upsert, aggregate };
|
|
85
|
+
module.exports = { mutation, upsert };
|
package/src/crud/query.js
CHANGED
|
@@ -38,8 +38,7 @@ async function _finalize(plan, items) {
|
|
|
38
38
|
* GQL 查询(返回数组)
|
|
39
39
|
*
|
|
40
40
|
* 支持的 params 键(通过 GQL 的 @key 引用):
|
|
41
|
-
* $condition / $sort / $skip / $limit
|
|
42
|
-
* 使用 $pipeline 时,框架不追加 compute 层、不补默认值、不裁剪,完全由用户控制。
|
|
41
|
+
* $condition / $sort / $skip / $limit
|
|
43
42
|
*
|
|
44
43
|
* `routeOverride`(多租户路由,可选):`{ source?, namespace? }`,覆盖命令定位,
|
|
45
44
|
* 权限/计算列仍按结构 schema 判定(见 multi-datasource-routing-plan.md §6)。
|
package/src/crud/write.js
CHANGED
|
@@ -109,7 +109,7 @@ async function exists(schemaName, condition, routeOverride = null) {
|
|
|
109
109
|
|
|
110
110
|
/** 统计符合条件的文档数量 */
|
|
111
111
|
async function count(schemaName, filter = null, routeOverride = null) {
|
|
112
|
-
const cmd = _call(() => _core.planCount(schemaName, filter ?? null, routeOverride));
|
|
112
|
+
const cmd = _call(() => _core.planCount(schemaName, filter ?? null, _ctx(), routeOverride));
|
|
113
113
|
return _exec(cmd);
|
|
114
114
|
}
|
|
115
115
|
|
package/src/datasource.js
CHANGED
|
@@ -210,14 +210,11 @@ function route(cmd) {
|
|
|
210
210
|
* 使上层(crud/*)对 Mongo / SQL 两条路径无感。
|
|
211
211
|
*/
|
|
212
212
|
async function execSql(source, connection, cmd) {
|
|
213
|
-
if (typeof connection.exec !== 'function') {
|
|
214
|
-
throw new Error(
|
|
215
|
-
`SQL 数据源 ${source}(${connection.kind}) 的执行器未接入(见执行文档 Phase 4)`,
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
213
|
const plan = _core.dialectTranslate(connection.kind, cmd);
|
|
219
214
|
// Host 兜底:core 标记了无法安全下推的组合(如 $lookup 子 $limit 每父 top-N)时,
|
|
220
215
|
// 绝不执行「缺少该段」的 SQL(会静默返回错误结果),改为显式报错,由调用方降级重查。
|
|
216
|
+
// 先于执行器检查 —— 命令本身不可安全下推时,报下推不支持而非「执行器未接入」
|
|
217
|
+
// (对齐 py_store/datasource.py#exec_sql 的检查次序)。
|
|
221
218
|
if (Array.isArray(plan.unsupported) && plan.unsupported.length > 0) {
|
|
222
219
|
const err = new PushdownUnsupportedError(
|
|
223
220
|
source,
|
|
@@ -229,6 +226,11 @@ async function execSql(source, connection, cmd) {
|
|
|
229
226
|
_emitFeedback(err.feedback());
|
|
230
227
|
throw err;
|
|
231
228
|
}
|
|
229
|
+
if (typeof connection.exec !== 'function') {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`SQL 数据源 ${source}(${connection.kind}) 的执行器未接入(见执行文档 Phase 4)`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
232
234
|
const out = await connection.exec(plan);
|
|
233
235
|
return executors.shapeResult(cmd, out);
|
|
234
236
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 驱动原始值归一(SQL 执行器共用)
|
|
5
|
+
*
|
|
6
|
+
* `pg` / `mysql2` 对**精确数值列**默认返回**字符串**:
|
|
7
|
+
* - `pg`:`int8`(20) / `numeric`(1700) → string;
|
|
8
|
+
* - `mysql2`:`DECIMAL` / `NEWDECIMAL`(246) → string。
|
|
9
|
+
*
|
|
10
|
+
* 而 Mongo 驱动对这些值返回 `number`。未归一 → 同一查询在 SQL 后端返回 `"8"`、Mongo 返回
|
|
11
|
+
* `8`(**静默类型失真**,`JSON.stringify` 对拍与用户代码都能看到差异)。
|
|
12
|
+
*
|
|
13
|
+
* 归一**只依据驱动给出的列类型元数据**(`pg` 的 `res.fields[].dataTypeID`、`mysql2` 的
|
|
14
|
+
* `fields[].columnType`),因此**不会**误伤真正的字符串列(如 `_id` / `status`)。
|
|
15
|
+
*
|
|
16
|
+
* 与 py-store 的 `py_store/executors/_values.py`(`Decimal` → 数值)同源同责。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** pg 的数值类型 OID(int2/int4/int8/float4/float8/numeric) */
|
|
20
|
+
const PG_NUMERIC_OIDS = new Set([20, 21, 23, 700, 701, 1700]);
|
|
21
|
+
|
|
22
|
+
/** mysql2 的数值 columnType(DECIMAL/TINY/SHORT/LONG/FLOAT/DOUBLE/LONGLONG/INT24/NEWDECIMAL) */
|
|
23
|
+
const MYSQL_NUMERIC_TYPES = new Set([0, 1, 2, 3, 4, 5, 8, 9, 246]);
|
|
24
|
+
|
|
25
|
+
/** 字符串数值 → number;非有限数值(如超长整数溢出为 Infinity)保持原样,不静默改值 */
|
|
26
|
+
function toNumber(v) {
|
|
27
|
+
if (typeof v !== 'string' || v === '') return v;
|
|
28
|
+
const n = Number(v);
|
|
29
|
+
return Number.isFinite(n) ? n : v;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 行集归一:按列类型元数据把**数值列**的字符串值转 `number`。
|
|
34
|
+
*
|
|
35
|
+
* @param {object[]} rows 驱动返回的行
|
|
36
|
+
* @param {object[]|undefined} fields 列元数据(`dataTypeID` for pg / `columnType` for mysql2)
|
|
37
|
+
* @param {'postgres'|'mysql'} kind 驱动种类
|
|
38
|
+
*/
|
|
39
|
+
function normalizeRows(rows, fields, kind) {
|
|
40
|
+
if (!rows || !rows.length || !fields || !fields.length) return rows;
|
|
41
|
+
const numeric = [];
|
|
42
|
+
for (let i = 0; i < fields.length; i += 1) {
|
|
43
|
+
const t = kind === 'postgres' ? fields[i].dataTypeID : fields[i].columnType;
|
|
44
|
+
const hit = kind === 'postgres' ? PG_NUMERIC_OIDS.has(t) : MYSQL_NUMERIC_TYPES.has(t);
|
|
45
|
+
if (hit) numeric.push(fields[i].name);
|
|
46
|
+
}
|
|
47
|
+
if (!numeric.length) return rows;
|
|
48
|
+
return rows.map((row) => {
|
|
49
|
+
const out = { ...row };
|
|
50
|
+
for (const k of numeric) out[k] = toNumber(out[k]);
|
|
51
|
+
return out;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { MYSQL_NUMERIC_TYPES, PG_NUMERIC_OIDS, normalizeRows, toNumber };
|
package/src/executors/index.js
CHANGED
|
@@ -12,12 +12,17 @@
|
|
|
12
12
|
* (`crud/query|write|mutation`)透明:上层只看到同一套返回值语义。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
const mongo = require('./mongo');
|
|
15
16
|
const mysql = require('./mysql');
|
|
16
17
|
const postgres = require('./postgres');
|
|
17
18
|
const sqlite = require('./sqlite');
|
|
18
19
|
|
|
19
20
|
const _BACKENDS = { mysql, postgres, sqlite };
|
|
20
21
|
|
|
22
|
+
// 注意:sqlite 执行器基于 better-sqlite3 同步驱动,调用期间会阻塞事件循环
|
|
23
|
+
// (设计选择,非缺陷)——高并发主链路请用 mysql/postgres/mongo,或为 SQLite
|
|
24
|
+
// 单独起独立进程;详见 ./sqlite.js 模块头「同步阻塞说明」。
|
|
25
|
+
|
|
21
26
|
/** 创建 SQL 数据源连接描述符 `{ kind, exec }`(driver 为对应驱动实例/连接) */
|
|
22
27
|
function createConnection(kind, driver, options) {
|
|
23
28
|
const mod = _BACKENDS[kind];
|
|
@@ -57,4 +62,4 @@ function shapeResult(cmd, out) {
|
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
module.exports = { createConnection, shapeResult, mysql, postgres, sqlite };
|
|
65
|
+
module.exports = { createConnection, shapeResult, mongo, mysql, postgres, sqlite };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mongo 执行器(驱动:mongodb 原生)
|
|
5
|
+
*
|
|
6
|
+
* 只做「Command JSON → 原生驱动调用」(铁律 1/3):全部纯逻辑(GQL 解析、权限、
|
|
7
|
+
* 命令规划、结果后处理)都在 Rust core。对齐 ``py-store/src/py_store/executors/mongo.py``。
|
|
8
|
+
*
|
|
9
|
+
* 唯一原生边界改写:`_explicitNull` —— 把「字段 = null」的**等值条件**编译为
|
|
10
|
+
* 「字段存在且为 null」(`{$eq: null, $exists: true}`)。理由:Mongo 原生把 `{f: null}`
|
|
11
|
+
* 同时命中「值为 null」与「字段缺失」两类文档,而本 store 的三态契约(F-07/H-09)
|
|
12
|
+
* 要求 `null` 只命中显式 null、`$exists:false` 才命中缺失 —— 这与 SQL 侧
|
|
13
|
+
* `col IS NULL`(显式 null)语义对齐。运算对象(`$ne:null`/`$exists`/`$gt`…)不改写。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 递归改写 filter:`field: null`(标量等值)→ `field: {$eq: null, $exists: true}`。 */
|
|
17
|
+
function _explicitNull(v) {
|
|
18
|
+
if (Array.isArray(v)) return v.map(_explicitNull);
|
|
19
|
+
if (v && typeof v === 'object') {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const [k, val] of Object.entries(v)) {
|
|
22
|
+
if (k.startsWith('$') && (val !== null && typeof val === 'object')) {
|
|
23
|
+
out[k] = _explicitNull(val);
|
|
24
|
+
} else if (k.startsWith('$')) {
|
|
25
|
+
out[k] = val;
|
|
26
|
+
} else if (val !== null && typeof val === 'object') {
|
|
27
|
+
out[k] = _explicitNull(val);
|
|
28
|
+
} else if (val === null) {
|
|
29
|
+
out[k] = { $eq: null, $exists: true };
|
|
30
|
+
} else {
|
|
31
|
+
out[k] = val;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
return v;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _normFilter(cmd) {
|
|
40
|
+
if (cmd.filter && typeof cmd.filter === 'object' && !Array.isArray(cmd.filter)) {
|
|
41
|
+
cmd.filter = _explicitNull(cmd.filter);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _normPipeline(cmd) {
|
|
46
|
+
if (!Array.isArray(cmd.pipeline)) return;
|
|
47
|
+
for (const stage of cmd.pipeline) {
|
|
48
|
+
if (stage && typeof stage === 'object' && stage.$match && typeof stage.$match === 'object') {
|
|
49
|
+
stage.$match = _explicitNull(stage.$match);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Command JSON → MongoDB 原生驱动调用 */
|
|
55
|
+
async function execMongo(db, cmd) {
|
|
56
|
+
const coll = db.collection(cmd.collection);
|
|
57
|
+
switch (cmd.kind) {
|
|
58
|
+
case 'find': {
|
|
59
|
+
_normFilter(cmd);
|
|
60
|
+
const opts = cmd.projection ? { projection: cmd.projection } : undefined;
|
|
61
|
+
return coll.find(cmd.filter, opts).toArray();
|
|
62
|
+
}
|
|
63
|
+
case 'aggregate':
|
|
64
|
+
_normPipeline(cmd);
|
|
65
|
+
return coll.aggregate(cmd.pipeline).toArray();
|
|
66
|
+
case 'countDocuments':
|
|
67
|
+
_normFilter(cmd);
|
|
68
|
+
return coll.countDocuments(cmd.filter);
|
|
69
|
+
case 'findOne': {
|
|
70
|
+
_normFilter(cmd);
|
|
71
|
+
const opts = cmd.projection ? { projection: cmd.projection } : undefined;
|
|
72
|
+
return coll.findOne(cmd.filter, opts);
|
|
73
|
+
}
|
|
74
|
+
case 'insertOne':
|
|
75
|
+
await coll.insertOne(cmd.doc);
|
|
76
|
+
return cmd.doc;
|
|
77
|
+
case 'insertMany':
|
|
78
|
+
if (cmd.upsertById) {
|
|
79
|
+
// 归档幂等(core planArchiveDocs):按 _id 逐条覆盖 —— 「归档成功但删除失败」
|
|
80
|
+
// 的重试不再因 _id 冲突整批失败。SQL 侧由 dialect 的 ON CONFLICT/REPLACE 承接。
|
|
81
|
+
for (const doc of cmd.docs) {
|
|
82
|
+
await coll.replaceOne({ _id: doc._id }, doc, { upsert: true });
|
|
83
|
+
}
|
|
84
|
+
return { insertedCount: cmd.docs.length };
|
|
85
|
+
}
|
|
86
|
+
await coll.insertMany(cmd.docs);
|
|
87
|
+
return { insertedCount: cmd.docs.length };
|
|
88
|
+
case 'findOneAndUpdate':
|
|
89
|
+
_normFilter(cmd);
|
|
90
|
+
return coll.findOneAndUpdate(cmd.filter, cmd.update, cmd.options);
|
|
91
|
+
case 'updateMany':
|
|
92
|
+
_normFilter(cmd);
|
|
93
|
+
return coll.updateMany(cmd.filter, cmd.update);
|
|
94
|
+
case 'deleteMany':
|
|
95
|
+
_normFilter(cmd);
|
|
96
|
+
return coll.deleteMany(cmd.filter);
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`未支持的命令: ${cmd.kind}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { execMongo };
|
package/src/executors/mysql.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { core: _core } = require('../schema');
|
|
13
|
+
const { normalizeRows } = require('./_values');
|
|
13
14
|
|
|
14
15
|
/** mysql2 返回 RowDataPacket 实例,转普通对象后再交 core(绑定层只认纯 JSON) */
|
|
15
16
|
function _plain(row) {
|
|
@@ -30,9 +31,9 @@ function create(driver, _options = {}) {
|
|
|
30
31
|
let rows = null;
|
|
31
32
|
let affectedRows = 0;
|
|
32
33
|
for (const stmt of plan.stmts) {
|
|
33
|
-
const [raw] = await conn.execute(stmt.text, stmt.params || []);
|
|
34
|
+
const [raw, fields] = await conn.execute(stmt.text, stmt.params || []);
|
|
34
35
|
if (Array.isArray(raw)) {
|
|
35
|
-
rows = raw.map(_plain);
|
|
36
|
+
rows = normalizeRows(raw.map(_plain), fields, 'mysql');
|
|
36
37
|
if (stmt.rowShape) docs = _core.restoreRows(stmt.rowShape, rows);
|
|
37
38
|
} else {
|
|
38
39
|
affectedRows = Number(raw.affectedRows || 0);
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { core: _core } = require('../schema');
|
|
13
|
+
const { normalizeRows } = require('./_values');
|
|
13
14
|
|
|
14
15
|
/** 创建执行器描述符(可直接作为 `init(connections)` 的一个 SQL 数据源连接) */
|
|
15
16
|
function create(driver, _options = {}) {
|
|
@@ -24,7 +25,7 @@ function create(driver, _options = {}) {
|
|
|
24
25
|
let affectedRows = 0;
|
|
25
26
|
for (const stmt of plan.stmts) {
|
|
26
27
|
const res = await conn.query(stmt.text, stmt.params || []);
|
|
27
|
-
rows = res.rows || [];
|
|
28
|
+
rows = normalizeRows(res.rows || [], res.fields, 'postgres');
|
|
28
29
|
affectedRows = Number(res.rowCount || 0);
|
|
29
30
|
if (stmt.rowShape) docs = _core.restoreRows(stmt.rowShape, rows);
|
|
30
31
|
}
|
package/src/executors/sqlite.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* 产出,本模块不拼任何 SQL;带 `rowShape` 的语句结果交 core `restoreRows` 还原为
|
|
8
8
|
* 嵌套文档。返回值是中立包络 `{ docs, rows, affectedRows }`,由 `./index.js`
|
|
9
9
|
* 依 command.kind 塑形为 Mongo 驱动等价返回值。
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ 同步阻塞说明:better-sqlite3 是**同步驱动**,`prepare/all/run` 在 async 契约内
|
|
12
|
+
* 仍会**阻塞事件循环**(本模块保持同步调用是有意为之的设计选择:单连接语义最简、
|
|
13
|
+
* 无跨线程开销)。高并发主链路请改用 MySQL / PostgreSQL / MongoDB 数据源,
|
|
14
|
+
* 或为 SQLite 单独起独立进程隔离阻塞面——本执行器不适合多请求共享的事件循环热路径。
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
const { core: _core } = require('../schema');
|
|
@@ -26,7 +31,7 @@ function create(db, _options = {}) {
|
|
|
26
31
|
throw new TypeError('sqlite 执行器需要 better-sqlite3 Database 实例');
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
/** 依序执行 plan.stmts(better-sqlite3
|
|
34
|
+
/** 依序执行 plan.stmts(better-sqlite3 同步单连接;同步调用会阻塞事件循环,见模块头说明) */
|
|
30
35
|
function runStmts(plan) {
|
|
31
36
|
let docs = null;
|
|
32
37
|
let rows = null;
|
package/src/index.js
CHANGED
|
@@ -108,11 +108,6 @@ class Store {
|
|
|
108
108
|
return crud.upsert(schemaName, condition, data, options, routeOverride);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
// ── 原生聚合 ──
|
|
112
|
-
async aggregate(schemaName, pl, routeOverride) {
|
|
113
|
-
return crud.aggregate(schemaName, pl, routeOverride);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
111
|
// ── 结构同步(SQL 数据源:introspect → schemaFromRows → mergeSchema → register) ──
|
|
117
112
|
async syncSchema(opts) {
|
|
118
113
|
return syncSchema(opts);
|
|
@@ -125,17 +120,22 @@ class Store {
|
|
|
125
120
|
}
|
|
126
121
|
|
|
127
122
|
// ── 宿主接入守卫(Registry 级,对齐 py-store c44001e) ──
|
|
128
|
-
/** 开关用户 $pipeline 直通(默认允许;AI 问数宿主建议关闭作纵深防御) */
|
|
129
|
-
setAllowUserPipeline(allow = true) {
|
|
130
|
-
return schema.setAllowUserPipeline(allow);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
123
|
/**
|
|
134
124
|
* 开关「上下文强制」(默认关闭 = fail-open)。开启后:所有查询/写入在 ctx 缺失时
|
|
135
125
|
* 抛 `ERR_NO_CONTEXT`(fail-secure);内部调用须显式传 `{ internal: true }` 上下文。
|
|
136
126
|
*/
|
|
137
|
-
setRequireContext(
|
|
138
|
-
return schema.setRequireContext(
|
|
127
|
+
setRequireContext(needCtx = true) {
|
|
128
|
+
return schema.setRequireContext(needCtx);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 「上下文强制」开关当前值(对齐 py-store store.require_context) */
|
|
132
|
+
requireContext() {
|
|
133
|
+
return schema.requireContext();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 设置数据源连接映射(多后端路由;对齐 py-store store.set_connections) */
|
|
137
|
+
setConnections(connections) {
|
|
138
|
+
return datasource.setConnections(connections);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/** 注册反馈事件回调(兜底/降级/拦截的统一出口);传 null 恢复默认 stderr */
|
|
@@ -214,7 +214,15 @@ async function _createIndexesIfNeeded() {
|
|
|
214
214
|
|
|
215
215
|
await coll.createIndex(Object.entries(keys), finalOptions);
|
|
216
216
|
} catch (e) {
|
|
217
|
-
|
|
217
|
+
// 索引创建失败不阻塞 init(辅助动作),但必须走统一反馈通道:
|
|
218
|
+
// 无 sink 时由 feedback 默认落 stderr(不双份打印),宿主可 setFeedbackSink 接管
|
|
219
|
+
feedback.emit({
|
|
220
|
+
type: 'index_create_failed',
|
|
221
|
+
code: 'indexCreateFailed',
|
|
222
|
+
layer: 'host',
|
|
223
|
+
message: `创建索引失败 ${s.collection}: ${e && e.message ? e.message : e}`,
|
|
224
|
+
hint: '检查该集合的索引定义与连接权限;索引缺失不影响读写,相关查询将退化为全表扫描',
|
|
225
|
+
});
|
|
218
226
|
}
|
|
219
227
|
}
|
|
220
228
|
}
|
|
@@ -245,7 +253,6 @@ module.exports = {
|
|
|
245
253
|
init,
|
|
246
254
|
store,
|
|
247
255
|
Store,
|
|
248
|
-
aggregate: crud.aggregate,
|
|
249
256
|
PermissionError: permission.PermissionError,
|
|
250
257
|
PushdownUnsupportedError: datasource.PushdownUnsupportedError,
|
|
251
258
|
datasource,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* introspect 共享工具(mysql / postgres 复用,避免复制粘贴式重复)
|
|
5
|
+
*
|
|
6
|
+
* 两后端 `_INDEXES` 查询产出的行形态不同(MySQL `nonUnique` 取反、PG `unique` 直取),
|
|
7
|
+
* 由调用方以 `uniqueOf(row)` 参数化该差异;归并逻辑本身逐行一致,抽为单份实现。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 把 `{table,name,column,...}` 索引行按 `(table, name)` 归并出
|
|
12
|
+
* `{table, name, columns, unique}` 数组(`unique` 取 `uniqueOf(row)` 的 0/1)
|
|
13
|
+
*/
|
|
14
|
+
function groupIndexes(rows, uniqueOf) {
|
|
15
|
+
const byKey = new Map();
|
|
16
|
+
for (const r of rows) {
|
|
17
|
+
const key = `${r.table}::${r.name}`;
|
|
18
|
+
let entry = byKey.get(key);
|
|
19
|
+
if (!entry) {
|
|
20
|
+
entry = { table: r.table, name: r.name, columns: [], unique: uniqueOf(r) };
|
|
21
|
+
byKey.set(key, entry);
|
|
22
|
+
}
|
|
23
|
+
entry.columns.push(r.column);
|
|
24
|
+
}
|
|
25
|
+
return [...byKey.values()];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { groupIndexes };
|
package/src/introspect/mysql.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* 交 core `schemaFromRows` 做纯映射。建议使用只读账号;库名取当前连接的 `DATABASE()`。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
const { groupIndexes } = require('./_shared');
|
|
11
|
+
|
|
10
12
|
const _TABLES = `
|
|
11
13
|
SELECT table_name AS name
|
|
12
14
|
FROM information_schema.tables
|
|
@@ -42,20 +44,8 @@ const _INDEXES = `
|
|
|
42
44
|
WHERE table_schema = DATABASE()
|
|
43
45
|
ORDER BY table_name, index_name, seq_in_index`;
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const byKey = new Map();
|
|
48
|
-
for (const r of rows) {
|
|
49
|
-
const key = `${r.table}::${r.name}`;
|
|
50
|
-
let entry = byKey.get(key);
|
|
51
|
-
if (!entry) {
|
|
52
|
-
entry = { table: r.table, name: r.name, columns: [], unique: Number(r.nonUnique) ? 0 : 1 };
|
|
53
|
-
byKey.set(key, entry);
|
|
54
|
-
}
|
|
55
|
-
entry.columns.push(r.column);
|
|
56
|
-
}
|
|
57
|
-
return [...byKey.values()];
|
|
58
|
-
}
|
|
47
|
+
// MySQL 行携带 nonUnique(1=非唯一)→ 取反为 unique 标志;归并逻辑共享见 _shared.js
|
|
48
|
+
const _uniqueOf = (r) => (Number(r.nonUnique) ? 0 : 1);
|
|
59
49
|
|
|
60
50
|
async function introspect(driver, { database = null } = {}) {
|
|
61
51
|
if (!driver || typeof driver.execute !== 'function') {
|
|
@@ -91,7 +81,7 @@ async function introspect(driver, { database = null } = {}) {
|
|
|
91
81
|
pk: c.columnKey === 'PRI' ? 1 : 0,
|
|
92
82
|
})),
|
|
93
83
|
fks,
|
|
94
|
-
indexes:
|
|
84
|
+
indexes: groupIndexes(indexRows, _uniqueOf),
|
|
95
85
|
};
|
|
96
86
|
}
|
|
97
87
|
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* 产出规范化行 JSON,交 core `schemaFromRows` 做纯映射。建议使用只读账号。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
const { groupIndexes } = require('./_shared');
|
|
11
|
+
|
|
10
12
|
const _TABLES = `
|
|
11
13
|
SELECT table_name AS name
|
|
12
14
|
FROM information_schema.tables
|
|
@@ -63,20 +65,8 @@ const _INDEXES = `
|
|
|
63
65
|
WHERE ns.nspname = $1 AND NOT ix.indisprimary
|
|
64
66
|
ORDER BY t.relname, i.relname`;
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const byKey = new Map();
|
|
69
|
-
for (const r of rows) {
|
|
70
|
-
const key = `${r.table}::${r.name}`;
|
|
71
|
-
let entry = byKey.get(key);
|
|
72
|
-
if (!entry) {
|
|
73
|
-
entry = { table: r.table, name: r.name, columns: [], unique: Number(r.unique) };
|
|
74
|
-
byKey.set(key, entry);
|
|
75
|
-
}
|
|
76
|
-
entry.columns.push(r.column);
|
|
77
|
-
}
|
|
78
|
-
return [...byKey.values()];
|
|
79
|
-
}
|
|
68
|
+
// PG 行已带 unique(0/1)→ 直取;归并逻辑共享见 _shared.js
|
|
69
|
+
const _uniqueOf = (r) => Number(r.unique);
|
|
80
70
|
|
|
81
71
|
async function introspect(driver, opts = {}) {
|
|
82
72
|
if (!driver || typeof driver.query !== 'function') {
|
|
@@ -106,7 +96,7 @@ async function introspect(driver, opts = {}) {
|
|
|
106
96
|
pk: Number(c.pk) || 0,
|
|
107
97
|
})),
|
|
108
98
|
fks: fks.rows,
|
|
109
|
-
indexes:
|
|
99
|
+
indexes: groupIndexes(indexRows.rows, _uniqueOf),
|
|
110
100
|
};
|
|
111
101
|
}
|
|
112
102
|
|
package/src/permission.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
13
13
|
|
|
14
|
+
const { core } = require('./schema');
|
|
15
|
+
|
|
14
16
|
const _als = new AsyncLocalStorage();
|
|
15
17
|
|
|
16
18
|
/** 设置当前请求的上下文,每次请求开始时调用一次 */
|
|
@@ -41,6 +43,47 @@ async function runAsInternal(fn) {
|
|
|
41
43
|
return _als.run(next, () => fn());
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
// ─── core 权限方法包装(对齐 py_store.permission 的同名薄包装) ──────
|
|
47
|
+
|
|
48
|
+
/** 接受 schema 对象或 schema 名称 */
|
|
49
|
+
function _model(schema) {
|
|
50
|
+
return typeof schema === 'string' ? schema : schema.name;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function canReadSchema(schema, ctx) {
|
|
54
|
+
return core.canRead(_model(schema), ctx ?? null);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function canWriteSchema(schema, ctx) {
|
|
58
|
+
return core.canWrite(_model(schema), ctx ?? null);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function shouldInjectOwnerCondition(schema, ctx) {
|
|
62
|
+
return core.shouldInjectOwner(_model(schema), ctx ?? null);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mergeOwnerCondition(schema, ctx, condition) {
|
|
66
|
+
const out = core.mergeOwnerCondition(_model(schema), ctx ?? null, condition ?? null);
|
|
67
|
+
// core 在「不注入」时返回 null(无法区分原条件为 null)→ 原样返回入参条件
|
|
68
|
+
return out === null || out === undefined ? condition : out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getReadableFields(schema, ctx) {
|
|
72
|
+
return core.readableFields(_model(schema), ctx ?? null);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getReadableRelations(schema, ctx) {
|
|
76
|
+
return core.readableRelations(_model(schema), ctx ?? null);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getWritableFields(schema, ctx) {
|
|
80
|
+
return core.writableFields(_model(schema), ctx ?? null);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function filterWritableData(schema, ctx, data) {
|
|
84
|
+
return core.filterWritableData(_model(schema), ctx ?? null, data);
|
|
85
|
+
}
|
|
86
|
+
|
|
44
87
|
// ─── 自定义错误 ──────────────────────────────────────────────
|
|
45
88
|
|
|
46
89
|
class PermissionError extends Error {
|
|
@@ -56,5 +99,13 @@ module.exports = {
|
|
|
56
99
|
getContext,
|
|
57
100
|
scopedRoles,
|
|
58
101
|
runAsInternal,
|
|
102
|
+
canReadSchema,
|
|
103
|
+
canWriteSchema,
|
|
104
|
+
shouldInjectOwnerCondition,
|
|
105
|
+
mergeOwnerCondition,
|
|
106
|
+
getReadableFields,
|
|
107
|
+
getReadableRelations,
|
|
108
|
+
getWritableFields,
|
|
109
|
+
filterWritableData,
|
|
59
110
|
PermissionError,
|
|
60
111
|
};
|
package/src/schema.js
CHANGED
|
@@ -100,19 +100,19 @@ function list() {
|
|
|
100
100
|
return core.list();
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/** 开关用户 $pipeline 直通(默认允许;AI 问数宿主建议关闭作纵深防御) */
|
|
104
|
-
function setAllowUserPipeline(allow = true) {
|
|
105
|
-
core.setAllowUserPipeline(Boolean(allow));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
103
|
/**
|
|
109
104
|
* 开关「上下文强制」(默认关闭 = fail-open,与 JS 原版语义一致)。
|
|
110
105
|
*
|
|
111
106
|
* 开启后:所有 plan 入口遇 ctx 缺失抛 `ERR_NO_CONTEXT` 错误(fail-secure);
|
|
112
107
|
* 内部调用(索引创建、归档回填、后台任务等)须显式传 `{ internal: true }` 上下文。
|
|
113
108
|
*/
|
|
114
|
-
function setRequireContext(
|
|
115
|
-
core.setRequireContext(Boolean(
|
|
109
|
+
function setRequireContext(needCtx = true) {
|
|
110
|
+
core.setRequireContext(Boolean(needCtx));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 「上下文强制」开关当前值(对齐 py_store.schema.require_context) */
|
|
114
|
+
function requireContext() {
|
|
115
|
+
return core.requireContext();
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/** 取 asyncFn 计算列实现(fnRef 缺省 = 计算列 key 名) */
|
|
@@ -120,4 +120,4 @@ function getAsyncFn(fnRef) {
|
|
|
120
120
|
return _asyncFns[fnRef];
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
module.exports = { core, register, get, has, list,
|
|
123
|
+
module.exports = { core, register, get, has, list, setRequireContext, requireContext, getAsyncFn };
|