nodejs-store 1.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/LICENSE +21 -0
- package/README.md +201 -0
- package/package.json +46 -0
- package/src/core.js +55 -0
- package/src/crud/exec.js +139 -0
- package/src/crud/id.js +59 -0
- package/src/crud/index.js +55 -0
- package/src/crud/mutation.js +72 -0
- package/src/crud/query.js +123 -0
- package/src/crud/write.js +103 -0
- package/src/datasource.js +148 -0
- package/src/executors/index.js +60 -0
- package/src/executors/mysql.js +46 -0
- package/src/executors/postgres.js +36 -0
- package/src/executors/sqlite.js +49 -0
- package/src/index.js +236 -0
- package/src/introspect/index.js +24 -0
- package/src/introspect/mysql.js +98 -0
- package/src/introspect/postgres.js +113 -0
- package/src/introspect/sqlite.js +79 -0
- package/src/permission.js +60 -0
- package/src/schema.js +106 -0
- package/src/sync.js +44 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* nodejs-store — 轻量多后端数据层(Node.js 版,Rust 单核心架构;支持 MongoDB / MySQL / SQLite / PostgreSQL)
|
|
5
|
+
*
|
|
6
|
+
* 核心理念:
|
|
7
|
+
* 1. 纯 JSON schema 定义,零代码
|
|
8
|
+
* 2. Rust core 统一实现 GQL 解析 / 权限 / 计算列 / 命令规划(core-node 绑定)
|
|
9
|
+
* 3. src/*.js 为薄 Host 适配层:驱动 IO + 回调 + 占位符替换
|
|
10
|
+
* 4. Python 侧(core-py)复用同一 Rust core,双端语义天然一致
|
|
11
|
+
*
|
|
12
|
+
* Rust core 与 Node/Python 绑定位于独立仓库 rust-store,本仓库通过其绑定产物引用。
|
|
13
|
+
*
|
|
14
|
+
* 用法:
|
|
15
|
+
* const { MongoClient } = require('mongodb');
|
|
16
|
+
* const { init, store } = require('nodejs-store');
|
|
17
|
+
*
|
|
18
|
+
* const client = new MongoClient(uri);
|
|
19
|
+
* await client.connect();
|
|
20
|
+
* await init(client.db('mydb'));
|
|
21
|
+
* const items = await store.query('Model($condition:@c0) { field1, field2 }', { c0: {} });
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const crud = require('./crud');
|
|
25
|
+
const datasource = require('./datasource');
|
|
26
|
+
const executors = require('./executors');
|
|
27
|
+
const introspect = require('./introspect');
|
|
28
|
+
const permission = require('./permission');
|
|
29
|
+
const schema = require('./schema');
|
|
30
|
+
const { syncSchema } = require('./sync');
|
|
31
|
+
|
|
32
|
+
class Store {
|
|
33
|
+
// ── Schema 管理 ──
|
|
34
|
+
register(defn) {
|
|
35
|
+
return schema.register(defn);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get(name) {
|
|
39
|
+
return schema.get(name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
has(name) {
|
|
43
|
+
return schema.has(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
list() {
|
|
47
|
+
return schema.list();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── CRUD ──
|
|
51
|
+
/**
|
|
52
|
+
* GQL 查询。`routeOverride`(可选):`{ source?, namespace? }` 多租户路由,
|
|
53
|
+
* 覆盖命令定位(权限/计算列仍按结构 schema 判定)。下同。
|
|
54
|
+
*/
|
|
55
|
+
async query(gql, params, routeOverride) {
|
|
56
|
+
return crud.query(gql, params, routeOverride);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async queryOne(gql, params, routeOverride) {
|
|
60
|
+
return crud.queryOne(gql, params, routeOverride);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async queryWithCount(gql, params, routeOverride) {
|
|
64
|
+
return crud.queryWithCount(gql, params, routeOverride);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 跨库联邦查询(一条 GQL 跨多数据源:各源取数 → 内存 join → 统一后处理) */
|
|
68
|
+
async queryFederated(gql, params) {
|
|
69
|
+
return crud.queryFederated(gql, params);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async insert(schemaName, data, routeOverride) {
|
|
73
|
+
return crud.insert(schemaName, data, routeOverride);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async insertMany(schemaName, docs, routeOverride) {
|
|
77
|
+
return crud.insertMany(schemaName, docs, routeOverride);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async update(schemaName, condition, data, options, routeOverride) {
|
|
81
|
+
return crud.update(schemaName, condition, data, options, routeOverride);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async updateMany(schemaName, condition, data, routeOverride) {
|
|
85
|
+
return crud.updateMany(schemaName, condition, data, routeOverride);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async remove(schemaName, condition, routeOverride) {
|
|
89
|
+
return crud.remove(schemaName, condition, routeOverride);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async exists(schemaName, condition, routeOverride) {
|
|
93
|
+
return crud.exists(schemaName, condition, routeOverride);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async count(schemaName, filter, routeOverride) {
|
|
97
|
+
return crud.count(schemaName, filter, routeOverride);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Mutation / Upsert ──
|
|
101
|
+
async mutation(schemaName, data, routeOverride) {
|
|
102
|
+
return crud.mutation(schemaName, data, routeOverride);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async upsert(schemaName, condition, data, options, routeOverride) {
|
|
106
|
+
return crud.upsert(schemaName, condition, data, options, routeOverride);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 原生聚合 ──
|
|
110
|
+
async aggregate(schemaName, pl, routeOverride) {
|
|
111
|
+
return crud.aggregate(schemaName, pl, routeOverride);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── 结构同步(SQL 数据源:introspect → schemaFromRows → mergeSchema → register) ──
|
|
115
|
+
async syncSchema(opts) {
|
|
116
|
+
return syncSchema(opts);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── 底层工具(调试/高级用法) ──
|
|
120
|
+
/** 解析 GQL 并构建 pipeline,返回 `{tokens, ast, pipeline, projection}` */
|
|
121
|
+
buildPipeline(gql, params) {
|
|
122
|
+
return schema.core.buildPipeline(gql, params ?? {}, permission.getContext() ?? null);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── 权限控制(AsyncLocalStorage 上下文) ──
|
|
126
|
+
setContext(ctx) {
|
|
127
|
+
return permission.setContext(ctx);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getContext() {
|
|
131
|
+
return permission.getContext();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
scopedRoles(roles, fn) {
|
|
135
|
+
return permission.scopedRoles(roles, fn);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async runAsInternal(fn) {
|
|
139
|
+
return permission.runAsInternal(fn);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 自定义权限错误(实例可被 store.PermissionError 捕获) */
|
|
144
|
+
Store.prototype.PermissionError = permission.PermissionError;
|
|
145
|
+
|
|
146
|
+
const store = new Store();
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 索引名对齐 MongoDB 自动命名(k1_v1_k2_v2),用于幂等创建。
|
|
150
|
+
*
|
|
151
|
+
* 按 source 分派:Mongo 源执行 `_createIndexesIfNeeded`;SQL 后端**不建索引**
|
|
152
|
+
* (`schema.indexes` 仅作元数据,见执行文档 Phase 3 动作 5)。
|
|
153
|
+
*/
|
|
154
|
+
async function _createIndexesIfNeeded() {
|
|
155
|
+
const names = schema.list();
|
|
156
|
+
for (const name of names) {
|
|
157
|
+
const s = schema.get(name);
|
|
158
|
+
// 索引创建是初始化的辅助动作(非命令路由):schema 绑定的 source 暂未在
|
|
159
|
+
// 当前连接映射中时跳过,不阻塞 init(命令路由的 fail fast 不在此处)
|
|
160
|
+
let db;
|
|
161
|
+
try {
|
|
162
|
+
db = datasource.dbOfSchema(name); // Mongo 按 (datasource, namespace) 解析;SQL 源返回 null
|
|
163
|
+
} catch (e) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!db) continue; // SQL 后端不建索引
|
|
167
|
+
|
|
168
|
+
const coll = db.collection(s.collection);
|
|
169
|
+
|
|
170
|
+
let existingIndexes;
|
|
171
|
+
try {
|
|
172
|
+
existingIndexes = await coll.listIndexes().toArray();
|
|
173
|
+
} catch (e) {
|
|
174
|
+
existingIndexes = [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const idx of s.indexes || []) {
|
|
178
|
+
try {
|
|
179
|
+
const keys = idx.keys;
|
|
180
|
+
if (!keys) continue;
|
|
181
|
+
|
|
182
|
+
// 合并 inline 选项(unique/sparse/expireAfterSeconds 等)与显式 options
|
|
183
|
+
const explicitOptions = idx.options || {};
|
|
184
|
+
const finalOptions = {};
|
|
185
|
+
for (const [k, v] of Object.entries(idx)) {
|
|
186
|
+
if (k !== 'keys' && k !== 'options') finalOptions[k] = v;
|
|
187
|
+
}
|
|
188
|
+
Object.assign(finalOptions, explicitOptions);
|
|
189
|
+
|
|
190
|
+
// 检查是否已有同 key 模式的索引(忽略选项差异)
|
|
191
|
+
const nameFromKeys = Object.entries(keys).map(([k, v]) => `${k}_${v}`).join('_');
|
|
192
|
+
if (existingIndexes.some((ei) => ei.name === nameFromKeys)) continue;
|
|
193
|
+
|
|
194
|
+
await coll.createIndex(Object.entries(keys), finalOptions);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.error(`[MongoStore] 创建索引失败 ${s.collection}: ${e && e.message ? e.message : e}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 初始化 store — 传入数据源连接映射
|
|
204
|
+
*
|
|
205
|
+
* - 多源:`init({ default: db, mongo_b: client, pg_a: { kind: 'postgres', exec }, ... })`
|
|
206
|
+
* - 单源简写:`init(db)` / `init(client)`(Mongo db 实例或 MongoClient,自动归一为
|
|
207
|
+
* `{ default: 连接 }`)
|
|
208
|
+
*
|
|
209
|
+
* 连接按命令的 `source` 路由、`namespace` 定位库(schema 声明);缺省绑定回落 `default`。
|
|
210
|
+
*/
|
|
211
|
+
async function init(connections) {
|
|
212
|
+
if (!connections || typeof connections !== 'object') {
|
|
213
|
+
throw new TypeError('init(connections) 需要数据源连接映射(或单个 MongoDB db 实例)');
|
|
214
|
+
}
|
|
215
|
+
datasource.setConnections(connections);
|
|
216
|
+
|
|
217
|
+
// 自动创建索引(仅 Mongo 源)— 幂等安全
|
|
218
|
+
await _createIndexesIfNeeded();
|
|
219
|
+
|
|
220
|
+
return store;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
init,
|
|
225
|
+
store,
|
|
226
|
+
Store,
|
|
227
|
+
aggregate: crud.aggregate,
|
|
228
|
+
PermissionError: permission.PermissionError,
|
|
229
|
+
datasource,
|
|
230
|
+
schema,
|
|
231
|
+
permission,
|
|
232
|
+
crud,
|
|
233
|
+
executors,
|
|
234
|
+
introspect,
|
|
235
|
+
syncSchema,
|
|
236
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* introspection 分派:按后端名把驱动交对应模块,产出统一的规范化行 JSON。
|
|
5
|
+
*
|
|
6
|
+
* 统一输出(交 core `schemaFromRows`):
|
|
7
|
+
* `{ tables:[{name}], columns:[{table,name,type,notnull,pk}],
|
|
8
|
+
* fks:[{table,column,refTable,refColumn}], indexes:[{table,name,columns,unique}] }`
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const mysql = require('./mysql');
|
|
12
|
+
const postgres = require('./postgres');
|
|
13
|
+
const sqlite = require('./sqlite');
|
|
14
|
+
|
|
15
|
+
const _BACKENDS = { mysql, postgres, sqlite };
|
|
16
|
+
|
|
17
|
+
/** 按后端名执行 introspection(sqlite 同步、mysql/postgres 异步,统一 await 返回) */
|
|
18
|
+
async function run(backend, driver, options) {
|
|
19
|
+
const mod = _BACKENDS[backend];
|
|
20
|
+
if (!mod) throw new Error(`未知 introspection 后端: ${backend}(支持 mysql/postgres/sqlite)`);
|
|
21
|
+
return mod.introspect(driver, options);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { run, mysql, postgres, sqlite };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MySQL introspection(驱动:mysql2/promise)
|
|
5
|
+
*
|
|
6
|
+
* 只发 `information_schema` 只读查询(铁律 6:绝不写 DDL 回库),产出规范化行 JSON,
|
|
7
|
+
* 交 core `schemaFromRows` 做纯映射。建议使用只读账号;库名取当前连接的 `DATABASE()`。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const _TABLES = `
|
|
11
|
+
SELECT table_name AS name
|
|
12
|
+
FROM information_schema.tables
|
|
13
|
+
WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'
|
|
14
|
+
ORDER BY table_name`;
|
|
15
|
+
|
|
16
|
+
const _COLUMNS = `
|
|
17
|
+
SELECT table_name AS "table",
|
|
18
|
+
column_name AS name,
|
|
19
|
+
column_type AS type,
|
|
20
|
+
is_nullable AS nullable,
|
|
21
|
+
column_key AS columnKey
|
|
22
|
+
FROM information_schema.columns
|
|
23
|
+
WHERE table_schema = DATABASE()
|
|
24
|
+
ORDER BY table_name, ordinal_position`;
|
|
25
|
+
|
|
26
|
+
const _FKS = `
|
|
27
|
+
SELECT table_name AS "table",
|
|
28
|
+
column_name AS \`column\`,
|
|
29
|
+
referenced_table_name AS "refTable",
|
|
30
|
+
referenced_column_name AS "refColumn"
|
|
31
|
+
FROM information_schema.key_column_usage
|
|
32
|
+
WHERE table_schema = DATABASE() AND referenced_table_name IS NOT NULL
|
|
33
|
+
ORDER BY table_name, ordinal_position`;
|
|
34
|
+
|
|
35
|
+
const _INDEXES = `
|
|
36
|
+
SELECT table_name AS "table",
|
|
37
|
+
index_name AS name,
|
|
38
|
+
non_unique AS nonUnique,
|
|
39
|
+
seq_in_index AS seq,
|
|
40
|
+
column_name AS \`column\`
|
|
41
|
+
FROM information_schema.statistics
|
|
42
|
+
WHERE table_schema = DATABASE()
|
|
43
|
+
ORDER BY table_name, index_name, seq_in_index`;
|
|
44
|
+
|
|
45
|
+
/** 把 `{table,name,nonUnique,column}` 行按索引名归并出 columns 数组 */
|
|
46
|
+
function _groupIndexes(rows) {
|
|
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
|
+
}
|
|
59
|
+
|
|
60
|
+
async function introspect(driver, { database = null } = {}) {
|
|
61
|
+
if (!driver || typeof driver.execute !== 'function') {
|
|
62
|
+
throw new TypeError('mysql introspection 需要 mysql2/promise 的连接或连接池');
|
|
63
|
+
}
|
|
64
|
+
// 显式传 database(连接串不带库或跨库同步)→ 参数化 table_schema;
|
|
65
|
+
// 缺省用当前连接的 DATABASE()。显式库名会作为 namespace 透出到 def。
|
|
66
|
+
const schemaFilter = database != null ? 'table_schema = ?' : 'table_schema = DATABASE()';
|
|
67
|
+
const params = database != null ? [database] : [];
|
|
68
|
+
const tablesSql = (base) => base.replace('table_schema = DATABASE()', schemaFilter);
|
|
69
|
+
|
|
70
|
+
const run = async (sql, args = []) => {
|
|
71
|
+
const [rows] = await driver.execute(sql, args);
|
|
72
|
+
return rows;
|
|
73
|
+
};
|
|
74
|
+
const [tables, columns, fks, indexRows] = await Promise.all([
|
|
75
|
+
run(tablesSql(_TABLES), params),
|
|
76
|
+
run(tablesSql(_COLUMNS), params),
|
|
77
|
+
run(tablesSql(_FKS), params),
|
|
78
|
+
run(tablesSql(_INDEXES), params),
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
// 显式库名 → 行携带 namespace(core schemaFromRows 会写进 def)
|
|
83
|
+
tables: database != null
|
|
84
|
+
? tables.map((t) => ({ ...t, namespace: database }))
|
|
85
|
+
: tables,
|
|
86
|
+
columns: columns.map((c) => ({
|
|
87
|
+
table: c.table,
|
|
88
|
+
name: c.name,
|
|
89
|
+
type: c.type || '',
|
|
90
|
+
notnull: c.nullable === 'NO' ? 1 : 0,
|
|
91
|
+
pk: c.columnKey === 'PRI' ? 1 : 0,
|
|
92
|
+
})),
|
|
93
|
+
fks,
|
|
94
|
+
indexes: _groupIndexes(indexRows),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { introspect };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PostgreSQL introspection(驱动:pg)
|
|
5
|
+
*
|
|
6
|
+
* 只发 `information_schema` / `pg_catalog` 只读查询(铁律 6:绝不写 DDL 回库),
|
|
7
|
+
* 产出规范化行 JSON,交 core `schemaFromRows` 做纯映射。建议使用只读账号。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const _TABLES = `
|
|
11
|
+
SELECT table_name AS name
|
|
12
|
+
FROM information_schema.tables
|
|
13
|
+
WHERE table_schema = $1 AND table_type = 'BASE TABLE'
|
|
14
|
+
ORDER BY table_name`;
|
|
15
|
+
|
|
16
|
+
const _COLUMNS = `
|
|
17
|
+
SELECT c.table_name AS "table",
|
|
18
|
+
c.column_name AS name,
|
|
19
|
+
c.data_type AS type,
|
|
20
|
+
c.is_nullable AS nullable,
|
|
21
|
+
CASE WHEN pk.column_name IS NULL THEN 0 ELSE 1 END AS pk
|
|
22
|
+
FROM information_schema.columns c
|
|
23
|
+
LEFT JOIN (
|
|
24
|
+
SELECT kcu.table_schema, kcu.table_name, kcu.column_name
|
|
25
|
+
FROM information_schema.table_constraints tc
|
|
26
|
+
JOIN information_schema.key_column_usage kcu
|
|
27
|
+
ON kcu.constraint_name = tc.constraint_name
|
|
28
|
+
AND kcu.table_schema = tc.table_schema
|
|
29
|
+
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1
|
|
30
|
+
) pk
|
|
31
|
+
ON pk.table_schema = c.table_schema
|
|
32
|
+
AND pk.table_name = c.table_name
|
|
33
|
+
AND pk.column_name = c.column_name
|
|
34
|
+
WHERE c.table_schema = $1
|
|
35
|
+
ORDER BY c.table_name, c.ordinal_position`;
|
|
36
|
+
|
|
37
|
+
const _FKS = `
|
|
38
|
+
SELECT src.relname AS "table",
|
|
39
|
+
sa.attname AS column,
|
|
40
|
+
ref.relname AS "refTable",
|
|
41
|
+
ra.attname AS "refColumn"
|
|
42
|
+
FROM pg_constraint con
|
|
43
|
+
JOIN pg_class src ON src.oid = con.conrelid
|
|
44
|
+
JOIN pg_class ref ON ref.oid = con.confrelid
|
|
45
|
+
JOIN pg_namespace ns ON ns.oid = src.relnamespace
|
|
46
|
+
JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord) ON true
|
|
47
|
+
JOIN LATERAL unnest(con.confkey) WITH ORDINALITY AS f(attnum, ord) ON f.ord = k.ord
|
|
48
|
+
JOIN pg_attribute sa ON sa.attrelid = con.conrelid AND sa.attnum = k.attnum
|
|
49
|
+
JOIN pg_attribute ra ON ra.attrelid = con.confrelid AND ra.attnum = f.attnum
|
|
50
|
+
WHERE con.contype = 'f' AND ns.nspname = $1
|
|
51
|
+
ORDER BY src.relname, k.ord`;
|
|
52
|
+
|
|
53
|
+
const _INDEXES = `
|
|
54
|
+
SELECT t.relname AS "table",
|
|
55
|
+
i.relname AS name,
|
|
56
|
+
CASE WHEN ix.indisunique THEN 1 ELSE 0 END AS unique,
|
|
57
|
+
a.attname AS column
|
|
58
|
+
FROM pg_index ix
|
|
59
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
60
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
61
|
+
JOIN pg_namespace ns ON ns.oid = t.relnamespace
|
|
62
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
63
|
+
WHERE ns.nspname = $1 AND NOT ix.indisprimary
|
|
64
|
+
ORDER BY t.relname, i.relname`;
|
|
65
|
+
|
|
66
|
+
/** 把 `{table,name,unique,column}` 行按索引名归并出 columns 数组 */
|
|
67
|
+
function _groupIndexes(rows) {
|
|
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
|
+
}
|
|
80
|
+
|
|
81
|
+
async function introspect(driver, opts = {}) {
|
|
82
|
+
if (!driver || typeof driver.query !== 'function') {
|
|
83
|
+
throw new TypeError('postgres introspection 需要 pg 的 Pool/Client 实例');
|
|
84
|
+
}
|
|
85
|
+
// 查询按 schema 过滤(缺省 public);显式传入 schema/namespace 时作为 namespace
|
|
86
|
+
// 透出到 def(缺省不透出 = 连接默认 search_path,保持既有行为零变更)。
|
|
87
|
+
const explicit = opts.schema !== undefined || opts.namespace !== undefined;
|
|
88
|
+
const schema = opts.schema ?? opts.namespace ?? 'public';
|
|
89
|
+
const namespace = explicit ? schema : null;
|
|
90
|
+
const [tables, columns, fks, indexRows] = await Promise.all([
|
|
91
|
+
driver.query(_TABLES, [schema]),
|
|
92
|
+
driver.query(_COLUMNS, [schema]),
|
|
93
|
+
driver.query(_FKS, [schema]),
|
|
94
|
+
driver.query(_INDEXES, [schema]),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
tables: namespace
|
|
99
|
+
? tables.rows.map((t) => ({ ...t, namespace }))
|
|
100
|
+
: tables.rows,
|
|
101
|
+
columns: columns.rows.map((c) => ({
|
|
102
|
+
table: c.table,
|
|
103
|
+
name: c.name,
|
|
104
|
+
type: c.type || '',
|
|
105
|
+
notnull: c.nullable === 'NO' ? 1 : 0,
|
|
106
|
+
pk: Number(c.pk) || 0,
|
|
107
|
+
})),
|
|
108
|
+
fks: fks.rows,
|
|
109
|
+
indexes: _groupIndexes(indexRows.rows),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { introspect };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SQLite introspection(驱动:better-sqlite3)
|
|
5
|
+
*
|
|
6
|
+
* 只发 `sqlite_master` / `PRAGMA` 只读查询(铁律 6:绝不写 DDL 回库),产出规范化
|
|
7
|
+
* 行 JSON,交 core `schemaFromRows` 做纯映射(本模块不做任何 schema 推断)。
|
|
8
|
+
*
|
|
9
|
+
* 返回:`{ tables, columns, fks, indexes }`(见 `dialect/introspect.rs` 的输入约定)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** PRAGMA 不支持参数化,需内联表名;标识符来自 sqlite_master(非用户输入),并做引号转义 */
|
|
13
|
+
function _quote(name) {
|
|
14
|
+
return `"${String(name).replace(/"/g, '""')}"`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function introspect(db, { database = null } = {}) {
|
|
18
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
19
|
+
throw new TypeError('sqlite introspection 需要 better-sqlite3 Database 实例');
|
|
20
|
+
}
|
|
21
|
+
const tables = [];
|
|
22
|
+
const columns = [];
|
|
23
|
+
const fks = [];
|
|
24
|
+
const indexes = [];
|
|
25
|
+
|
|
26
|
+
// attached db 过滤:PRAGMA database_list 校验库名存在(main/temp/ATTACH 的库名),
|
|
27
|
+
// 表清单改从 `<db>.sqlite_master` 读取;显式库名作为 namespace 透出到 def。
|
|
28
|
+
let masterFrom = 'sqlite_master';
|
|
29
|
+
if (database != null) {
|
|
30
|
+
const known = db.prepare('PRAGMA database_list').all().some((r) => r.name === database);
|
|
31
|
+
if (!known) {
|
|
32
|
+
const names = db.prepare('PRAGMA database_list').all().map((r) => r.name).join(', ');
|
|
33
|
+
throw new Error(`SQLite attached db 不存在: ${database}(当前 attached: ${names})`);
|
|
34
|
+
}
|
|
35
|
+
masterFrom = `${_quote(database)}.sqlite_master`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const tableRows = db
|
|
39
|
+
.prepare(`SELECT name FROM ${masterFrom} WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`)
|
|
40
|
+
.all();
|
|
41
|
+
|
|
42
|
+
for (const { name } of tableRows) {
|
|
43
|
+
tables.push(database != null ? { name, namespace: database } : { name });
|
|
44
|
+
|
|
45
|
+
for (const c of db.prepare(`PRAGMA table_info(${_quote(name)})`).all()) {
|
|
46
|
+
columns.push({
|
|
47
|
+
table: name,
|
|
48
|
+
name: c.name,
|
|
49
|
+
type: c.type || '',
|
|
50
|
+
notnull: c.notnull ? 1 : 0,
|
|
51
|
+
pk: c.pk ? 1 : 0,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const f of db.prepare(`PRAGMA foreign_key_list(${_quote(name)})`).all()) {
|
|
56
|
+
fks.push({
|
|
57
|
+
table: name,
|
|
58
|
+
column: f.from,
|
|
59
|
+
refTable: f.table,
|
|
60
|
+
refColumn: f.to || '_id',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const idx of db.prepare(`PRAGMA index_list(${_quote(name)})`).all()) {
|
|
65
|
+
if (idx.origin === 'pk') continue; // 主键索引不重复登记
|
|
66
|
+
const info = db.prepare(`PRAGMA index_info(${_quote(idx.name)})`).all();
|
|
67
|
+
indexes.push({
|
|
68
|
+
table: name,
|
|
69
|
+
name: idx.name,
|
|
70
|
+
columns: info.map((i) => i.name),
|
|
71
|
+
unique: idx.unique ? 1 : 0,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { tables, columns, fks, indexes };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { introspect };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 权限上下文 — AsyncLocalStorage 请求上下文 + 自定义错误
|
|
5
|
+
*
|
|
6
|
+
* 角色评估、字段过滤、所有者条件注入等权限逻辑已全部下沉 Rust core;
|
|
7
|
+
* 本模块只保留 Host 侧职责:
|
|
8
|
+
* - 请求上下文的隐式传递(core 的 ctx 一律显式入参,由本模块取出后传入)
|
|
9
|
+
* - PermissionError(core 返回的权限类错误消息由 crud.js 映射为本错误类型)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
13
|
+
|
|
14
|
+
const _als = new AsyncLocalStorage();
|
|
15
|
+
|
|
16
|
+
/** 设置当前请求的上下文,每次请求开始时调用一次 */
|
|
17
|
+
function setContext(ctx) {
|
|
18
|
+
_als.enterWith(ctx);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 获取当前请求的上下文,无则 undefined */
|
|
22
|
+
function getContext() {
|
|
23
|
+
return _als.getStore();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 以指定角色进入临时权限上下文(嵌套安全),执行 fn 后自动恢复原上下文。
|
|
28
|
+
*
|
|
29
|
+
* 供 AI 查询执行等显式角色注入场景使用,取代 setContext + finally setContext(null)
|
|
30
|
+
* 的清空式写法(后者嵌套时会误清外层上下文,且「无上下文 = 权限全放行」,清空即静默失守方向)。
|
|
31
|
+
*/
|
|
32
|
+
function scopedRoles(roles, fn) {
|
|
33
|
+
const next = { ...(getContext() || {}), roles };
|
|
34
|
+
return _als.run(next, () => fn());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 在内部上下文中执行操作(绕过权限检查),结束后自动恢复上下文 */
|
|
38
|
+
async function runAsInternal(fn) {
|
|
39
|
+
const prev = getContext();
|
|
40
|
+
const next = { ...(prev || {}), internal: true };
|
|
41
|
+
return _als.run(next, () => fn());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── 自定义错误 ──────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
class PermissionError extends Error {
|
|
47
|
+
constructor(message, status = 403) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = 'PermissionError';
|
|
50
|
+
this.status = status;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
setContext,
|
|
56
|
+
getContext,
|
|
57
|
+
scopedRoles,
|
|
58
|
+
runAsInternal,
|
|
59
|
+
PermissionError,
|
|
60
|
+
};
|