mm_api 1.1.4 → 1.1.7
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 +31 -1
- package/lib/api.js +252 -10
- package/package.json +4 -2
- package/tests/fixtures/dao_org.json +11 -0
- package/tests/fixtures/dao_org_plain.json +9 -0
- package/tests/org.test.js +121 -0
package/README.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
1
|
## API框架
|
|
2
2
|
用于实现web api和mvc模式路由请求
|
|
3
|
-
负责配置路由路径、访问权限、校验机制
|
|
3
|
+
负责配置路由路径、访问权限、校验机制
|
|
4
|
+
|
|
5
|
+
### 接口数据源(dao.json 的 db 字段)
|
|
6
|
+
|
|
7
|
+
接口用哪个库由接口自己声明,不依赖路由层注入。在 `dao.json` 里写 `db`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"name": "ai_agent",
|
|
12
|
+
"db": "org",
|
|
13
|
+
"schema": "ai.sql",
|
|
14
|
+
"table": "ai_agent",
|
|
15
|
+
...
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- `db` 为数据域名,执行时经 `daoAdmin(域)` 取数据源;省略时默认 `os`(主库)。
|
|
20
|
+
- `db: "org"` 表示按机构分库:机构ID取自 query 参数 `org_id`,取 `daoAdmin('org', { org_id })`,
|
|
21
|
+
即每个机构一个独立库(`org_{org_id}.db`)。
|
|
22
|
+
- 页面路由 `/ai/{org_id}/` 渲染时不带 query,自动回落到路径中的机构段;两者都没带时使用默认机构 `1`。
|
|
23
|
+
- `org_id` 是数据源标识而非表字段,需在 dao.json 的 `filter` 中声明:
|
|
24
|
+
`"filter": { "org_id": "" }`(值为空表示只抽取不赋值),DAO 生成 where 前会将其抽走(详见 mm_dao 文档)。
|
|
25
|
+
- `schema` 为表结构脚本:首次访问某个机构库时自动执行一次(幂等),之后跳过。
|
|
26
|
+
路径从 dao.json 所在目录向上逐级查找。
|
|
27
|
+
- 登录态、字典等系统表固定走主库,不受该字段影响。
|
|
28
|
+
|
|
29
|
+
调用示例:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
GET /api/ai/agent?org_id=2&method=get&size=10
|
|
33
|
+
```
|
package/lib/api.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require("path");
|
|
|
3
3
|
const Item = require('mm_machine').Mod;
|
|
4
4
|
const { string, date, object, file } = require("mm_helper");
|
|
5
5
|
const { getRet } = require('mm_ret');
|
|
6
|
+
const { sqlAdmin } = require('mm_sql');
|
|
6
7
|
const Oauth = require("./oauth.js"); // 是MM自带的身份验证机制,基于Oauth2.0,可以不使用
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -23,7 +24,12 @@ const Param = loadOptional("mm_param/lib/param.js"); // 是MM自带的参数机
|
|
|
23
24
|
// 回退到 mm_sql 的 Sql(仅支持 run(sql, param, timeout) 原始SQL执行)
|
|
24
25
|
const mm_dao = loadOptional("mm_dao");
|
|
25
26
|
const Dao = mm_dao ? mm_dao.Dao : null;
|
|
27
|
+
// 数据域入口:dao.json 声明 db 后,用对应域的数据源执行(未声明时仍为主库 os)
|
|
28
|
+
const daoAdmin = mm_dao ? mm_dao.daoAdmin : null;
|
|
26
29
|
const Sql = loadOptional("mm_sql/lib/sql.js"); // 是MM自带的sql机制,可以不使用
|
|
30
|
+
// 登录态存储:token 解析优先走 mm_state_store(用户状态)+ mm_dict(token→user_id 映射)
|
|
31
|
+
const mm_state_store = loadOptional("mm_state_store");
|
|
32
|
+
const mm_dict = loadOptional("mm_dict");
|
|
27
33
|
|
|
28
34
|
/** param 实例缓存(模块级,替代原全局 param 模块列表) */
|
|
29
35
|
const PARAM_CACHE = new Map();
|
|
@@ -189,6 +195,8 @@ Api.prototype._preset = function () {
|
|
|
189
195
|
this.param = null;
|
|
190
196
|
// sql模板配置 + 函数
|
|
191
197
|
this.sql = null;
|
|
198
|
+
// 数据域:接口默认使用主库(os),dao.json 的 db 字段可覆盖为其他域(如 org)
|
|
199
|
+
this.db_scope = this.config.db || "os";
|
|
192
200
|
|
|
193
201
|
var cg = this.config;
|
|
194
202
|
this._loadParam(cg.param_path);
|
|
@@ -328,6 +336,10 @@ Api.prototype._loadSql = function (file_path) {
|
|
|
328
336
|
cfg.name = this.config.name || cfg.name;
|
|
329
337
|
cfg.config_file = p;
|
|
330
338
|
cfg.dir = path.dirname(p);
|
|
339
|
+
// 数据域:dao.json 用 db 字段声明本接口的数据源(如 "org"),未声明时为主库 os
|
|
340
|
+
this.db_scope = cfg.db || this.db_scope || "os";
|
|
341
|
+
// 表结构脚本:org 域按机构分库时,首次访问该机构库自动建表
|
|
342
|
+
this.schema = this._resolveSchema(cfg.schema);
|
|
331
343
|
if (Dao) {
|
|
332
344
|
this.sql = new Dao(cfg, this);
|
|
333
345
|
} else {
|
|
@@ -498,6 +510,146 @@ Api.prototype._getState = async function (db, token) {
|
|
|
498
510
|
}
|
|
499
511
|
};
|
|
500
512
|
|
|
513
|
+
/**
|
|
514
|
+
* 解析本次请求的机构ID
|
|
515
|
+
* 机构是调用方指定的数据源标识,以 query 参数 org_id 为准;
|
|
516
|
+
* 页面路由 /ai/{org_id}/ 渲染时不带 query,回落到路径中的机构段;
|
|
517
|
+
* 两者都没有时使用默认机构 1。
|
|
518
|
+
* @param {object} ctx HTTP上下文
|
|
519
|
+
* @returns {number} 机构ID
|
|
520
|
+
* @private
|
|
521
|
+
*/
|
|
522
|
+
function _orgId(ctx) {
|
|
523
|
+
var q = (ctx && (ctx.query || (ctx.request && ctx.request.query))) || {};
|
|
524
|
+
var v = q.org_id;
|
|
525
|
+
if (v !== undefined && v !== null && v !== "") {
|
|
526
|
+
var n = parseInt(v, 10);
|
|
527
|
+
if (!isNaN(n) && n > 0) {
|
|
528
|
+
return n;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
var p = (ctx && (ctx.path || (ctx.request && ctx.request.path))) || "";
|
|
532
|
+
var m = /\/ai\/(\d+)(?=\/|$)/.exec(p);
|
|
533
|
+
return m ? parseInt(m[1], 10) : 1;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* 按数据域名取数据管理器
|
|
538
|
+
* 数据源由接口自己声明(dao.json 的 db 字段),不依赖调用方下传:
|
|
539
|
+
* os 为主库;org 域按机构分库(daoAdmin('org', { org_id }));
|
|
540
|
+
* 其余域名交由 mm_dao 的 daoAdmin 解析,域未配置时其内部回落主库。
|
|
541
|
+
* @param {string} scope 数据域名
|
|
542
|
+
* @param {number} [org_id] 机构ID(scope 为 org 时使用)
|
|
543
|
+
* @returns {object} 数据管理器
|
|
544
|
+
* @private
|
|
545
|
+
*/
|
|
546
|
+
function _scopedDb(scope, org_id) {
|
|
547
|
+
if (daoAdmin) {
|
|
548
|
+
try {
|
|
549
|
+
if (scope === "org") {
|
|
550
|
+
return daoAdmin("org", { org_id: org_id }).db();
|
|
551
|
+
}
|
|
552
|
+
if (scope && scope !== "os") {
|
|
553
|
+
return daoAdmin(scope).db();
|
|
554
|
+
}
|
|
555
|
+
} catch (err) {
|
|
556
|
+
// 域不可用时退回主库,避免因配置缺失导致接口整体不可用
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return sqlAdmin("os").db();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** 已建表的机构库标记,避免重复执行表结构脚本 */
|
|
563
|
+
const SCHEMA_READY = new Set();
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* 解析表结构脚本路径:从接口目录向上逐级查找(最多 8 级),命中即返回
|
|
567
|
+
* @param {string} name 脚本文件名
|
|
568
|
+
* @returns {string|null} 绝对路径,未找到返回 null
|
|
569
|
+
* @private
|
|
570
|
+
*/
|
|
571
|
+
Api.prototype._resolveSchema = function (name) {
|
|
572
|
+
if (!name) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
var dir = this.getDir();
|
|
576
|
+
for (var i = 0; i < 8 && dir; i++) {
|
|
577
|
+
var p = file.fullname(name, dir);
|
|
578
|
+
if (fs.existsSync(p)) {
|
|
579
|
+
return p;
|
|
580
|
+
}
|
|
581
|
+
var up = path.dirname(dir);
|
|
582
|
+
if (up === dir) {
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
dir = up;
|
|
586
|
+
}
|
|
587
|
+
return null;
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* 首次访问某个机构库时创建表结构(幂等:同一库只执行一次)
|
|
592
|
+
* @param {object} manager 该机构的数据域管理器(提供 load 执行表结构脚本)
|
|
593
|
+
* @param {object} db 数据管理器
|
|
594
|
+
* @returns {Promise<void>} 无返回值
|
|
595
|
+
* @private
|
|
596
|
+
*/
|
|
597
|
+
Api.prototype._ensureSchema = async function (manager, db) {
|
|
598
|
+
if (!this.schema || !manager) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
var key = (db.database ? db.database() : "") + "|" + this.schema;
|
|
602
|
+
if (SCHEMA_READY.has(key)) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
SCHEMA_READY.add(key);
|
|
606
|
+
try {
|
|
607
|
+
await manager.load(this.schema);
|
|
608
|
+
} catch (err) {
|
|
609
|
+
SCHEMA_READY.delete(key);
|
|
610
|
+
throw err;
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* 获取本接口的数据管理器(数据源由 dao.json 的 db 字段声明,默认 os)
|
|
616
|
+
* org 域按机构ID分库(org_id 取自 query,页面路径兜底),并在首次访问时建表
|
|
617
|
+
* @param {object} ctx HTTP上下文
|
|
618
|
+
* @returns {Promise<object>} 数据管理器
|
|
619
|
+
* @private
|
|
620
|
+
*/
|
|
621
|
+
Api.prototype._getDb = async function (ctx) {
|
|
622
|
+
var scope = this.db_scope || "os";
|
|
623
|
+
if (scope === "org") {
|
|
624
|
+
var org_id = _orgId(ctx);
|
|
625
|
+
var name = "org_" + org_id;
|
|
626
|
+
if (!this._db_manager || this._db_manager_scope !== name) {
|
|
627
|
+
this._org_manager = daoAdmin ? daoAdmin("org", { org_id: org_id }) : null;
|
|
628
|
+
this._db_manager = this._org_manager ? this._org_manager.db() : sqlAdmin("os").db();
|
|
629
|
+
this._db_manager_scope = name;
|
|
630
|
+
}
|
|
631
|
+
await this._ensureSchema(this._org_manager, this._db_manager);
|
|
632
|
+
return this._db_manager;
|
|
633
|
+
}
|
|
634
|
+
if (!this._db_manager || this._db_manager_scope !== scope) {
|
|
635
|
+
this._db_manager = _scopedDb(scope);
|
|
636
|
+
this._db_manager_scope = scope;
|
|
637
|
+
}
|
|
638
|
+
return this._db_manager;
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* 获取主库数据管理器(登录态、字典等系统表固定走主库)
|
|
643
|
+
* @returns {object} 数据管理器
|
|
644
|
+
* @private
|
|
645
|
+
*/
|
|
646
|
+
Api.prototype._getMainDb = function () {
|
|
647
|
+
if (!this._main_db_manager) {
|
|
648
|
+
this._main_db_manager = sqlAdmin("os").db();
|
|
649
|
+
}
|
|
650
|
+
return this._main_db_manager;
|
|
651
|
+
};
|
|
652
|
+
|
|
501
653
|
/**
|
|
502
654
|
* 获取登录态(原名 getState,因与 mm_corejs 生命周期 getState 冲突而更名)
|
|
503
655
|
* @param {object} ctx 请求上下文
|
|
@@ -528,15 +680,37 @@ Api.prototype._getUserFromSession = function (ctx) {
|
|
|
528
680
|
return null;
|
|
529
681
|
};
|
|
530
682
|
|
|
683
|
+
/**
|
|
684
|
+
* 从上下文中提取 auth token(协议头优先,Cookie 兜底)
|
|
685
|
+
* @param {object} ctx 请求上下文
|
|
686
|
+
* @returns {string} token,无则返回空串
|
|
687
|
+
* @private
|
|
688
|
+
*/
|
|
689
|
+
Api.prototype._getToken = function (ctx) {
|
|
690
|
+
var headers = (ctx && (ctx.headers || (ctx.request && ctx.request.headers))) || {};
|
|
691
|
+
var token = headers[this.config.token_key] || "";
|
|
692
|
+
if (!token && ctx) {
|
|
693
|
+
var cookies = ctx.cookies || (ctx.request && ctx.request.cookies);
|
|
694
|
+
if (cookies) {
|
|
695
|
+
token =
|
|
696
|
+
typeof cookies.get === "function"
|
|
697
|
+
? cookies.get(this.config.token_key) || ""
|
|
698
|
+
: cookies[this.config.token_key] || "";
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return String(token || "");
|
|
702
|
+
};
|
|
703
|
+
|
|
531
704
|
/**
|
|
532
705
|
* 从 token 中获取用户信息
|
|
706
|
+
* 解析顺序:缓存 → 用户状态存储(mm_dict 映射 / 解码 → mm_state_store)→ 数据库
|
|
533
707
|
* @param {object} ctx 请求上下文
|
|
534
708
|
* @param {object} db 数据管理器
|
|
535
709
|
* @returns {object|null} 用户信息
|
|
536
710
|
* @private
|
|
537
711
|
*/
|
|
538
712
|
Api.prototype._getUserFromToken = async function (params, ctx, options) {
|
|
539
|
-
var token =
|
|
713
|
+
var token = this._getToken(ctx);
|
|
540
714
|
if (!token) {
|
|
541
715
|
return null;
|
|
542
716
|
}
|
|
@@ -544,10 +718,76 @@ Api.prototype._getUserFromToken = async function (params, ctx, options) {
|
|
|
544
718
|
if (cache) {
|
|
545
719
|
var u = await cache.get(this.config.session_key + "_" + token);
|
|
546
720
|
if (u) {
|
|
547
|
-
return await this._processUserFromCache(u, ctx,
|
|
721
|
+
return await this._processUserFromCache(u, ctx, token);
|
|
548
722
|
}
|
|
549
723
|
}
|
|
550
|
-
|
|
724
|
+
var o = await this._getUserFromState(token, ctx);
|
|
725
|
+
if (o) {
|
|
726
|
+
return o;
|
|
727
|
+
}
|
|
728
|
+
// 数据库兜底:自行获取数据管理器查询
|
|
729
|
+
return await this._getUserFromDb(token, ctx);
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* 从用户状态存储获取用户信息
|
|
734
|
+
* token → user_id:mm_dict 映射,或 base64(JSON) 直接解码;
|
|
735
|
+
* user_id → 用户:mm_state_store 的 stateAdmin('user') 中 user_{id} 状态的 user 字段
|
|
736
|
+
* @param {string} token token
|
|
737
|
+
* @param {object} ctx 请求上下文
|
|
738
|
+
* @param {object} options 数据管理器
|
|
739
|
+
* @returns {object|null} 用户信息,未识别或状态中无用户资料返回 null
|
|
740
|
+
* @private
|
|
741
|
+
*/
|
|
742
|
+
Api.prototype._getUserFromState = async function (token, ctx, options) {
|
|
743
|
+
if (!mm_state_store || !mm_dict) {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
// 1. token → user_id:字典映射,或 base64(JSON) 解码
|
|
748
|
+
var user_id = null;
|
|
749
|
+
try {
|
|
750
|
+
user_id = mm_dict.dictAdmin("token").get(token);
|
|
751
|
+
} catch (err) {
|
|
752
|
+
user_id = null;
|
|
753
|
+
}
|
|
754
|
+
if (!user_id) {
|
|
755
|
+
try {
|
|
756
|
+
var json = Buffer.from(token, "base64").toString("utf8");
|
|
757
|
+
if (/^\s*\{[\s\S]*\}\s*$/.test(json)) {
|
|
758
|
+
var obj = JSON.parse(json);
|
|
759
|
+
if (obj && obj.user_id !== undefined && obj.user_id !== null && obj.user_id !== "") {
|
|
760
|
+
user_id = String(obj.user_id);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
} catch (err) {
|
|
764
|
+
user_id = null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (!user_id) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
// 2. user_id → 用户状态
|
|
771
|
+
var state = null;
|
|
772
|
+
try {
|
|
773
|
+
state = await mm_state_store.stateAdmin("user").get("user_" + user_id);
|
|
774
|
+
} catch (err) {
|
|
775
|
+
state = null;
|
|
776
|
+
}
|
|
777
|
+
if (!state || !state.user) {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
var o = { ...state.user };
|
|
781
|
+
o.token = token;
|
|
782
|
+
o.user_id = String(user_id);
|
|
783
|
+
if (ctx && ctx.session) {
|
|
784
|
+
ctx.session.uuid = ctx.session.uuid || token;
|
|
785
|
+
ctx.session.user = o;
|
|
786
|
+
}
|
|
787
|
+
return o;
|
|
788
|
+
} catch (err) {
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
551
791
|
};
|
|
552
792
|
|
|
553
793
|
/**
|
|
@@ -559,7 +799,7 @@ Api.prototype._getUserFromToken = async function (params, ctx, options) {
|
|
|
559
799
|
* @returns {object|null} 用户信息
|
|
560
800
|
* @private
|
|
561
801
|
*/
|
|
562
|
-
Api.prototype._processUserFromCache = async function (u, ctx,
|
|
802
|
+
Api.prototype._processUserFromCache = async function (u, ctx, token) {
|
|
563
803
|
var o;
|
|
564
804
|
if (typeof u === "string") {
|
|
565
805
|
o = { ...string.toJson(u) };
|
|
@@ -570,7 +810,7 @@ Api.prototype._processUserFromCache = async function (u, ctx, options, token) {
|
|
|
570
810
|
return null;
|
|
571
811
|
}
|
|
572
812
|
if (!o.user_id) {
|
|
573
|
-
return await this._getUserFromDb(token, ctx
|
|
813
|
+
return await this._getUserFromDb(token, ctx);
|
|
574
814
|
}
|
|
575
815
|
o.token = token;
|
|
576
816
|
return o;
|
|
@@ -584,8 +824,8 @@ Api.prototype._processUserFromCache = async function (u, ctx, options, token) {
|
|
|
584
824
|
* @returns {object|null} 用户信息
|
|
585
825
|
* @private
|
|
586
826
|
*/
|
|
587
|
-
Api.prototype._getUserFromDb = async function (token, ctx
|
|
588
|
-
var obj = await this._getState(
|
|
827
|
+
Api.prototype._getUserFromDb = async function (token, ctx) {
|
|
828
|
+
var obj = await this._getState(this._getMainDb(), token);
|
|
589
829
|
if (obj) {
|
|
590
830
|
var o = { ...obj.user };
|
|
591
831
|
ctx.session.user = obj.user;
|
|
@@ -633,13 +873,15 @@ Api.prototype.before = async function (params, ctx, options) {
|
|
|
633
873
|
Api.prototype.main = async function (params, ctx, options) {
|
|
634
874
|
if (this.sql) {
|
|
635
875
|
var req = ctx.request;
|
|
636
|
-
//
|
|
637
|
-
if (req.files) {
|
|
876
|
+
// 获取文件(仅在上传时处理,files 为空对象时不应往 body 写空字段)
|
|
877
|
+
if (req.files && req.files.file) {
|
|
638
878
|
var fobj = this.saveFile(req.files);
|
|
639
879
|
req.body.file = fobj.file;
|
|
640
880
|
req.body.url = fobj.url;
|
|
641
881
|
}
|
|
642
|
-
|
|
882
|
+
// 数据源由接口自行解析(dao.json 的 db 字段),
|
|
883
|
+
// 组织过滤由 DAO 按 dao.json 的 filter 声明统一抽取,此处不改动请求参数
|
|
884
|
+
var ret = await this.sql.run(req.query, req.body, await this._getDb(ctx));
|
|
643
885
|
return ret;
|
|
644
886
|
} else {
|
|
645
887
|
return getRet().error(10000, "接口函数未定义");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_api",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "API框架,用于实现web api和mvc模式路由请求,负责配置路由路径、访问权限、校验机制;基于 mm_corejs,内置管理器池,无全局/原型扩展依赖",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"mm_corejs": "^1.1.4",
|
|
26
26
|
"mm_dao": "^1.8.2",
|
|
27
|
+
"mm_dict": "^1.0.3",
|
|
27
28
|
"mm_helper": "^1.0.0",
|
|
28
29
|
"mm_machine": "^3.5.4",
|
|
29
30
|
"mm_param": "^1.1.0",
|
|
30
31
|
"mm_ret": "^1.4.8",
|
|
31
|
-
"mm_sql": "^1.8.0"
|
|
32
|
+
"mm_sql": "^1.8.0",
|
|
33
|
+
"mm_state_store": "^1.0.3"
|
|
32
34
|
},
|
|
33
35
|
"scripts": {
|
|
34
36
|
"test": "node test.js && node --test tests/*.test.js"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 机构数据源测试
|
|
5
|
+
* @description 覆盖 org 域的机构ID来源(query 优先、路径兜底),以及
|
|
6
|
+
* 控制参数在进入 where 之前被剔除(DAO 层统一处理)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const test = require('node:test');
|
|
10
|
+
const assert = require('node:assert/strict');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const Api = require('../lib/api.js');
|
|
14
|
+
const { daoAdmin } = require('mm_dao');
|
|
15
|
+
|
|
16
|
+
// 登记 org 域的分库模板(每个机构一个独立的 SQLite 库)
|
|
17
|
+
daoAdmin('org', {
|
|
18
|
+
sql: {
|
|
19
|
+
way: 'sqlite',
|
|
20
|
+
sqlite: { dir: path.join(os.tmpdir(), 'mm_api_org_test/'), database: 'org_{org_id}' }
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 构造接口实例
|
|
26
|
+
* @param {string} dao_file dao 配置文件名
|
|
27
|
+
* @returns {Api} 接口实例
|
|
28
|
+
*/
|
|
29
|
+
function makeApi(dao_file) {
|
|
30
|
+
const api = new Api({
|
|
31
|
+
name: 'org_probe_' + Math.random().toString(36).slice(2),
|
|
32
|
+
dir: __dirname,
|
|
33
|
+
db: 'org'
|
|
34
|
+
});
|
|
35
|
+
api._loadSql('fixtures/' + dao_file);
|
|
36
|
+
return api;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 拦截 DAO 执行,记录实际使用的库与查询参数
|
|
41
|
+
* @param {Api} api 接口实例
|
|
42
|
+
* @returns {object} 记录对象
|
|
43
|
+
*/
|
|
44
|
+
function spy(api) {
|
|
45
|
+
const seen = { db: '', query: null };
|
|
46
|
+
api.sql.run = async (query, body, db) => {
|
|
47
|
+
seen.db = String(db.database());
|
|
48
|
+
seen.query = { ...query };
|
|
49
|
+
return { error: 0 };
|
|
50
|
+
};
|
|
51
|
+
return seen;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test('org_id 取自 query 参数', async () => {
|
|
55
|
+
const api = makeApi('dao_org.json');
|
|
56
|
+
const seen = spy(api);
|
|
57
|
+
await api.main({}, { request: { query: { org_id: '2', method: 'get' }, body: {} }, path: '/api/ai/agent' }, {});
|
|
58
|
+
assert.equal(seen.db, 'org_2', '应落到机构 2 的库');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('页面路由不带 query 时回落路径中的机构段', async () => {
|
|
62
|
+
const api = makeApi('dao_org.json');
|
|
63
|
+
const seen = spy(api);
|
|
64
|
+
await api.main({}, { request: { query: { method: 'get' }, body: {} }, path: '/ai/3/' }, {});
|
|
65
|
+
assert.equal(seen.db, 'org_3', '应落到机构 3 的库');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('query 与路径同时存在时以 query 为准', async () => {
|
|
69
|
+
const api = makeApi('dao_org.json');
|
|
70
|
+
const seen = spy(api);
|
|
71
|
+
await api.main({}, { request: { query: { org_id: '5', method: 'get' }, body: {} }, path: '/ai/9/' }, {});
|
|
72
|
+
assert.equal(seen.db, 'org_5', 'query 优先于路径');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('都未指定时使用默认机构 1', async () => {
|
|
76
|
+
const api = makeApi('dao_org.json');
|
|
77
|
+
const seen = spy(api);
|
|
78
|
+
await api.main({}, { request: { query: { method: 'get' }, body: {} }, path: '/api/ai/agent' }, {});
|
|
79
|
+
assert.equal(seen.db, 'org_1', '默认落到机构 1');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('dao.json 声明 filter 后 org_id 不进入 where', () => {
|
|
83
|
+
const api = makeApi('dao_org.json');
|
|
84
|
+
const db = daoAdmin('org', { org_id: 2 }).db().new('ai_agent', 'agent_id');
|
|
85
|
+
const where = api.sql.toWhere(db, { method: 'get', org_id: '2' });
|
|
86
|
+
assert.equal(where, '', 'org_id 应被抽取,where 为空');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('未声明 filter 时 org_id 会被当成查询条件', () => {
|
|
90
|
+
const api = makeApi('dao_org_plain.json');
|
|
91
|
+
const db = daoAdmin('org', { org_id: 2 }).db().new('ai_agent', 'agent_id');
|
|
92
|
+
const where = api.sql.toWhere(db, { method: 'get', org_id: '2' });
|
|
93
|
+
assert.equal(where, "`org_id` = '2'", '未声明时按普通字段处理');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('控制参数不进入 where(含假值)', () => {
|
|
97
|
+
const api = makeApi('dao_org.json');
|
|
98
|
+
const db = daoAdmin('org', { org_id: 2 }).db().new('ai_agent', 'agent_id');
|
|
99
|
+
const where = api.sql.toWhere(db, {
|
|
100
|
+
method: 'get',
|
|
101
|
+
page: '0',
|
|
102
|
+
size: '',
|
|
103
|
+
orderby: 'agent_id desc',
|
|
104
|
+
db: 'org',
|
|
105
|
+
groupby: 'llm',
|
|
106
|
+
org_id: '2'
|
|
107
|
+
});
|
|
108
|
+
assert.equal(where, '', '分页、排序、数据域等控制参数都应被剔除');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('业务查询条件正常保留', () => {
|
|
112
|
+
const api = makeApi('dao_org.json');
|
|
113
|
+
const db = daoAdmin('org', { org_id: 2 }).db().new('ai_agent', 'agent_id');
|
|
114
|
+
const where = api.sql.toWhere(db, { method: 'get', org_id: '2', name: '张三' });
|
|
115
|
+
assert.equal(where, "`name` like '%张三%'", '业务字段应按 query 模板生成条件');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('非 org 域不受 org_id 影响', () => {
|
|
119
|
+
const api = new Api({ name: 'os_probe_' + Math.random().toString(36).slice(2), dir: __dirname });
|
|
120
|
+
assert.equal(api.db_scope, 'os', '未声明 db 时为主库');
|
|
121
|
+
});
|