mm_api 1.1.0 → 1.1.2

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/lib/manager.js CHANGED
@@ -1,12 +1,12 @@
1
- const Manager = require("mm_corejs").Manager;
2
- const Drive = require("./drive");
1
+ const Manager = require('mm_machine').Manager;
2
+ const Api = require("./api");
3
3
 
4
4
  /**
5
- * Api接口类(Web API 网关管理器,基于 mm_corejs,无全局/原型扩展依赖)
5
+ * ApiManager接口类(Web API 网关管理器,基于 mm_corejs,无全局/原型扩展依赖)
6
6
  * @class
7
7
  * @augments {Manager}
8
8
  */
9
- class Api extends Manager {
9
+ class ApiManager extends Manager {
10
10
  /**
11
11
  * 配置参数
12
12
  * @type {object}
@@ -55,25 +55,50 @@ class Api extends Manager {
55
55
  * @param {object} parent 父级模块(仅作兼容传入,不使用)
56
56
  */
57
57
  constructor(config, parent) {
58
- super({ ...Api.config, ...config }, parent);
59
- // 插件注册表(方案B:手动注册,供驱动 plugin() 查找)
60
- this.plugins = {};
58
+ super({ ...ApiManager.config, ...config }, parent);
61
59
  }
62
60
  }
63
61
 
64
62
  /**
65
- * Api驱动类
63
+ * ApiManager驱动类
66
64
  * @type {Drive}
67
65
  */
68
- Api.prototype.Drive = Drive;
66
+ ApiManager.prototype.Drive = Api;
67
+
68
+ /**
69
+ * 初始化桥接(生命周期汇聚点:init() 与 do('init') 均调用,触发 onInit)
70
+ * @returns {Promise<void>} 初始化完成
71
+ * @private
72
+ */
73
+ ApiManager.prototype._init = async function () {
74
+ await this.onInit();
75
+ };
76
+
77
+ /**
78
+ * 启动桥接(生命周期汇聚点,触发 onStart)
79
+ * @returns {Promise<void>} 启动完成
80
+ * @private
81
+ */
82
+ ApiManager.prototype._start = async function () {
83
+ await this.onStart();
84
+ };
85
+
86
+ /**
87
+ * 停止桥接(生命周期汇聚点,触发 onStop)
88
+ * @returns {Promise<void>} 停止完成
89
+ * @private
90
+ */
91
+ ApiManager.prototype._stop = async function () {
92
+ await this.onStop();
93
+ };
69
94
 
70
95
  /**
71
96
  * 注册一个 api 驱动(方案B:手动注册,不扫描目录)
72
97
  * @param {object} config 驱动配置(必须包含 name)
73
98
  * @returns {object} 驱动实例
74
99
  */
75
- Api.prototype.add = function (config) {
76
- var drive = new Drive({ ...config }, this);
100
+ ApiManager.prototype.add = function (config) {
101
+ var drive = new this.Drive(config, this);
77
102
  this.set(drive.config.name, drive);
78
103
  return drive;
79
104
  };
@@ -83,7 +108,7 @@ Api.prototype.add = function (config) {
83
108
  * @param {string} name 驱动名称
84
109
  * @returns {Promise<boolean>} 是否删除成功
85
110
  */
86
- Api.prototype.del = async function (name) {
111
+ ApiManager.prototype.del = async function (name) {
87
112
  var drive = this.get(name);
88
113
  if (drive) {
89
114
  await drive.stop();
@@ -97,7 +122,7 @@ Api.prototype.del = async function (name) {
97
122
  * @param {string} name 驱动名称
98
123
  * @returns {Promise<boolean>} 是否执行成功
99
124
  */
100
- Api.prototype.reload = async function (name) {
125
+ ApiManager.prototype.reload = async function (name) {
101
126
  var drive = this.get(name);
102
127
  if (!drive) {
103
128
  this.log("warn", "驱动不存在,无法重启", name);
@@ -115,26 +140,13 @@ Api.prototype.reload = async function (name) {
115
140
  };
116
141
 
117
142
  /**
118
- * 注册插件集合(供驱动 plugin() 查找,替代原全局插件池)
119
- * @param {string} name 插件作用域名称(app 名)
120
- * @param {object} obj 插件集合对象(需含 get 方法)
121
- * @returns {object} 当前管理器实例
122
- */
123
- Api.prototype.usePlugin = function (name, obj) {
124
- this.plugins[name] = obj;
125
- return this;
126
- };
127
-
128
- /**
129
- * 初始化驱动实例(仅未初始化时执行生命周期 init,重复调用安全)
143
+ * 初始化驱动实例(执行生命周期 init)
130
144
  * @param {object} drive 驱动实例
131
145
  * @returns {Promise<void>} 初始化完成
132
146
  * @private
133
147
  */
134
- Api.prototype._initDrive = async function (drive) {
135
- if (drive.isState("created")) {
136
- await drive.init();
137
- }
148
+ ApiManager.prototype._initDrive = async function (drive) {
149
+ await drive.init();
138
150
  };
139
151
 
140
152
  /**
@@ -142,7 +154,7 @@ Api.prototype._initDrive = async function (drive) {
142
154
  * @returns {Array} 驱动配置数组
143
155
  * @private
144
156
  */
145
- Api.prototype._getInfos = function () {
157
+ ApiManager.prototype._getInfos = function () {
146
158
  return this.values().map((d) => d.config);
147
159
  };
148
160
 
@@ -152,7 +164,7 @@ Api.prototype._getInfos = function () {
152
164
  * @returns {Array} 驱动实例数组
153
165
  * @private
154
166
  */
155
- Api.prototype._sortedDrives = function () {
167
+ ApiManager.prototype._sortedDrives = function () {
156
168
  var sort_key = this.config.sort_key || "sort";
157
169
  var drives = this.values().filter((d) => d.config.state === 1);
158
170
  drives.sort((m1, m2) => {
@@ -174,7 +186,7 @@ Api.prototype._sortedDrives = function () {
174
186
  * @param {string} pattern 模式
175
187
  * @returns {boolean} 是否匹配
176
188
  */
177
- Api.prototype.isMatch = function (str, pattern) {
189
+ ApiManager.prototype.isMatch = function (str, pattern) {
178
190
  if (str === pattern) {
179
191
  return true;
180
192
  } else if (pattern.startsWith("*")) {
@@ -193,32 +205,51 @@ Api.prototype.isMatch = function (str, pattern) {
193
205
  };
194
206
 
195
207
  /**
196
- * 执行Api
197
- * @param {object} ctx 请求上下文
198
- * @param {object} db 数据管理器
208
+ * 获取运行参数:兼容 run(ctx, options) 与历史写法 run(params, ctx, options)
209
+ * @param {object} first 请求上下文或参数对象
210
+ * @param {object} second 数据管理器或请求上下文
211
+ * @param {object} third 数据管理器
212
+ * @returns {object} 含 params、ctx、options 的参数集合
213
+ */
214
+ ApiManager.prototype._getRunArgs = function (first, second, third) {
215
+ if (first && first.request) {
216
+ return { params: first.params || {}, ctx: first, options: second || {} };
217
+ }
218
+ return { params: first || {}, ctx: second || {}, options: third || {} };
219
+ };
220
+
221
+ /**
222
+ * 执行ApiManager
223
+ * @param {object} ctx 请求上下文,或历史写法中的参数对象
224
+ * @param {object} options 数据管理器,如: { next: async function{}, ret: {} }
199
225
  * @returns {object | string} 返回一个对象或字符串
200
226
  */
201
- Api.prototype.run = async function (ctx, options = {}) {
202
- let path = ctx.request.path;
227
+ ApiManager.prototype.run = async function (ctx, options = {}) {
228
+ let args = this._getRunArgs(ctx, options, arguments[2]);
229
+ let path = args.ctx.request.path;
203
230
  let drives = this._sortedDrives();
231
+ let result = null;
204
232
 
205
233
  for (var i = 0; i < drives.length; i++) {
206
234
  var mod = drives[i];
207
235
  if (this.isMatch(path, mod.config.path)) {
208
- if (!mod.isLoaded()) {
236
+ if (!(await mod.isLoaded())) {
209
237
  await mod.load();
210
238
  }
211
- var ret = await mod.run(ctx, options);
239
+ var ret = await mod.run(args.params, args.ctx, args.options);
212
240
  if (ret) {
213
- db.ret = ret;
241
+ // 同时写入上下文与数据管理器,兼容两种结果读取方式
242
+ args.ctx.result = ret;
243
+ args.options.ret = ret;
244
+ result = ret;
214
245
  break;
215
246
  }
216
247
  }
217
248
  }
218
- return db.ret;
249
+ return result;
219
250
  };
220
251
 
221
252
  /**
222
253
  * @module 导出API管理器类
223
254
  */
224
- module.exports = Api;
255
+ module.exports = ApiManager;
package/lib/oauth.js CHANGED
@@ -1,59 +1,6 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
- const { string } = require("mm_helper");
4
-
5
- /**
6
- * 转换值类型(等价复刻 mm_expand 的 convertValue)
7
- * @param {any} target_val 目标值
8
- * @param {any} source_val 源值
9
- * @returns {any} 转换后的值
10
- */
11
- function _convertValue(target_val, source_val) {
12
- var target_type = typeof target_val;
13
- if (target_type === "number") {
14
- var num = Number(source_val);
15
- return isNaN(num) ? 0 : num;
16
- } else if (target_type === "boolean") {
17
- if (source_val === "0" || source_val === "false" || source_val === "False") {
18
- return false;
19
- } else if (source_val === "1" || source_val === "true" || source_val === "True") {
20
- return true;
21
- } else {
22
- return Boolean(source_val);
23
- }
24
- } else if (target_type === "string") {
25
- return String(source_val);
26
- } else {
27
- return source_val;
28
- }
29
- }
30
-
31
- /**
32
- * 添加对象属性(替代原全局 push,等价复刻 mm_expand 的 push)
33
- * @param {object} target 被添加的对象
34
- * @param {object} source 用作添加的对象
35
- * @param {boolean} supplement 是否补充没有的对象
36
- * @returns {object} 修改后的目标对象
37
- */
38
- function _push(target, source, supplement = false) {
39
- if (!target || !source || typeof target !== "object" || typeof source !== "object") {
40
- return target;
41
- }
42
- for (var key in source) {
43
- if (target.hasOwnProperty(key)) {
44
- var target_val = target[key];
45
- var source_val = source[key];
46
- if (typeof target_val === "object" && typeof source_val === "object") {
47
- _push(target_val, source_val, supplement);
48
- } else if (typeof target_val === typeof source_val) {
49
- target[key] = _convertValue(target_val, source_val);
50
- }
51
- } else if (supplement) {
52
- target[key] = source[key];
53
- }
54
- }
55
- return target;
56
- }
3
+ const { string, object } = require("mm_helper");
57
4
 
58
5
  /**
59
6
  * Oauth身份验证驱动类
@@ -108,14 +55,14 @@ Oauth.prototype.getDir = function () {
108
55
  * @param {object} obj 配置对象
109
56
  */
110
57
  Oauth.prototype.loadObj = function (obj) {
111
- _push(this.config, obj);
58
+ object.push(this.config, obj);
112
59
  var f = this.config.main;
113
60
  if (f) {
114
61
  var file = path.isAbsolute(f) ? f : path.resolve(this.getDir(), f);
115
62
  if (fs.existsSync(file)) {
116
63
  var cs = require(file);
117
64
  if (cs) {
118
- _push(this, cs, true);
65
+ object.push(this, cs, true);
119
66
  }
120
67
  }
121
68
  }
package/package.json CHANGED
@@ -1,29 +1,35 @@
1
1
  {
2
- "name": "mm_api",
3
- "version": "1.1.0",
4
- "description": "API框架,用于实现web api和mvc模式路由请求,负责配置路由路径、访问权限、校验机制;基于 mm_corejs,内置管理器池,无全局/原型扩展依赖",
5
- "main": "index.js",
6
- "keywords": [
7
- "mm_mod",
8
- "mm_port",
9
- "mm_in_port",
10
- "api",
11
- "mvc",
12
- "router",
13
- "oauth",
14
- "API管理器"
15
- ],
16
- "layer": "port",
17
- "group": "web",
18
- "author": "邱文武",
19
- "license": "ISC",
20
- "dependencies": {
21
- "mm_corejs": "^1.1.4",
22
- "mm_param": "^1.1.0",
23
- "mm_helper": "^1.0.0",
24
- "mm_sql": "^1.8.0"
25
- },
26
- "scripts": {
27
- "test": "node test.js"
28
- }
2
+ "name": "mm_api",
3
+ "version": "1.1.2",
4
+ "description": "API框架,用于实现web api和mvc模式路由请求,负责配置路由路径、访问权限、校验机制;基于 mm_corejs,内置管理器池,无全局/原型扩展依赖",
5
+ "main": "index.js",
6
+ "keywords": [
7
+ "mm_mod",
8
+ "mm_port",
9
+ "mm_in_port",
10
+ "api",
11
+ "mvc",
12
+ "router",
13
+ "oauth",
14
+ "API管理器"
15
+ ],
16
+ "layer": "port",
17
+ "group": "web",
18
+ "author": "邱文武",
19
+ "license": "ISC",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://gitee.com/qiuwenwu91/mm_api.git"
23
+ },
24
+ "dependencies": {
25
+ "mm_corejs": "^1.1.4",
26
+ "mm_helper": "^1.0.0",
27
+ "mm_machine": "^3.5.4",
28
+ "mm_param": "^1.1.0",
29
+ "mm_ret": "^1.4.8",
30
+ "mm_sql": "^1.8.0"
31
+ },
32
+ "scripts": {
33
+ "test": "node test.js && node --test tests/*.test.js"
34
+ }
29
35
  }
package/script.tpl.js CHANGED
@@ -6,7 +6,7 @@ module.exports = {
6
6
  * @param {object} db 数据管理器,如: { next: async function{}, ret: {} }
7
7
  * @returns {object} 执行结果
8
8
  */
9
- async main(ctx, options) {
9
+ async main(params, ctx, options) {
10
10
  // 获取请求参数
11
11
  var req = ctx.request;
12
12
  var {
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Api 接口驱动测试
5
+ * @description 覆盖脚本加载、请求执行、方法过滤、参数校验与异常路径
6
+ */
7
+
8
+ const test = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const Api = require('../lib/api.js');
11
+ const { makeCtx, makeOptions } = require('./helper.js');
12
+
13
+ test('load 加载脚本并挂载 main', () => {
14
+ const api = new Api({
15
+ name: 'hello',
16
+ path: '/hello',
17
+ dir: __dirname,
18
+ main: './fixtures/hello.js'
19
+ });
20
+ assert.strictEqual(api.isLoaded(), false, '初始未加载');
21
+ const mod = api.load();
22
+ assert.ok(mod, 'load 应返回脚本模块');
23
+ assert.strictEqual(api.isLoaded(), true, '加载后应标记已加载');
24
+ assert.strictEqual(typeof api.main, 'function', 'main 应挂载为函数');
25
+ });
26
+
27
+ test('load 不存在的脚本返回 null', () => {
28
+ const api = new Api({
29
+ name: 'miss',
30
+ path: '/miss',
31
+ dir: __dirname,
32
+ main: './fixtures/not_exist.js'
33
+ });
34
+ const mod = api.load();
35
+ assert.strictEqual(mod, null);
36
+ assert.strictEqual(api.isLoaded(), false);
37
+ });
38
+
39
+ test('run 执行脚本返回 JSON 字符串', async () => {
40
+ const api = new Api({
41
+ name: 'hello',
42
+ path: '/hello',
43
+ method: 'ALL',
44
+ dir: __dirname,
45
+ main: './fixtures/hello.js'
46
+ });
47
+ assert.ok(api.load(), '应先加载脚本');
48
+ const ctx = makeCtx({ path: '/hello', method: 'GET' });
49
+ const ret = await api.run({}, ctx, makeOptions());
50
+ assert.ok(ret, 'run 应返回结果');
51
+ assert.deepStrictEqual(JSON.parse(ret), {
52
+ code: 0,
53
+ message: '成功',
54
+ data: { hello: 'world' }
55
+ });
56
+ });
57
+
58
+ test('run 回显查询参数与正文', async () => {
59
+ const api = new Api({
60
+ name: 'echo',
61
+ path: '/echo',
62
+ method: 'ALL',
63
+ dir: __dirname,
64
+ main: './fixtures/echo.js'
65
+ });
66
+ assert.ok(api.load(), '应先加载脚本');
67
+ const ctx = makeCtx({
68
+ path: '/echo',
69
+ method: 'POST',
70
+ query: { a: '1' },
71
+ body: { b: '2' }
72
+ });
73
+ const ret = await api.run({}, ctx, makeOptions());
74
+ assert.deepStrictEqual(JSON.parse(ret), {
75
+ query: { a: '1' },
76
+ body: { b: '2' }
77
+ });
78
+ });
79
+
80
+ test('run 方法不匹配返回 null', async () => {
81
+ const api = new Api({
82
+ name: 'hello',
83
+ path: '/hello',
84
+ method: 'POST',
85
+ dir: __dirname,
86
+ main: './fixtures/hello.js'
87
+ });
88
+ const ctx = makeCtx({ path: '/hello', method: 'GET' });
89
+ const ret = await api.run({}, ctx, makeOptions());
90
+ assert.strictEqual(ret, null);
91
+ });
92
+
93
+ test('run 脚本抛错返回错误响应', async () => {
94
+ const api = new Api({
95
+ name: 'error',
96
+ path: '/error',
97
+ method: 'ALL',
98
+ dir: __dirname,
99
+ main: './fixtures/error.js'
100
+ });
101
+ const ctx = makeCtx({ path: '/error', method: 'GET' });
102
+ const ret = await api.run({}, ctx, makeOptions());
103
+ assert.ok(ret, 'run 应返回结果');
104
+ const obj = JSON.parse(ret);
105
+ assert.ok(obj.error, '应返回错误对象');
106
+ assert.strictEqual(obj.error.code, 10000);
107
+ });
108
+
109
+ test('main 未定义脚本返回错误响应', async () => {
110
+ const api = new Api({
111
+ name: 'hello',
112
+ path: '/hello',
113
+ method: 'ALL',
114
+ dir: __dirname,
115
+ main: './fixtures/hello.js'
116
+ });
117
+ // 覆盖 main 为 null,模拟脚本无主函数
118
+ api.main = null;
119
+ const ctx = makeCtx({ path: '/hello', method: 'GET' });
120
+ const ret = await api.run({}, ctx, makeOptions());
121
+ assert.ok(ret);
122
+ const obj = JSON.parse(ret);
123
+ assert.ok(obj.error, '应返回错误对象');
124
+ assert.strictEqual(obj.error.code, 10000);
125
+ });
126
+
127
+ test('加载参数配置并校验通过', () => {
128
+ const api = new Api({
129
+ name: 'param',
130
+ path: '/param',
131
+ method: 'ALL',
132
+ dir: __dirname,
133
+ param_path: './fixtures/param.json'
134
+ });
135
+ assert.ok(api.param, '应加载参数驱动');
136
+ const error = api.checkParam({ name: '张三' }, {}, 'GET');
137
+ assert.strictEqual(error, null, '参数正确时校验应通过');
138
+ });
139
+
140
+ test('参数校验失败返回错误', () => {
141
+ const api = new Api({
142
+ name: 'param',
143
+ path: '/param',
144
+ method: 'ALL',
145
+ dir: __dirname,
146
+ param_path: './fixtures/param.json'
147
+ });
148
+ assert.ok(api.param, '应加载参数驱动');
149
+ const error = api.checkParam({}, {}, 'GET');
150
+ assert.ok(error, '缺少必填参数应返回错误');
151
+ assert.ok(error.error, '错误应为响应格式');
152
+ assert.strictEqual(error.error.code, 30001, '必填缺失错误码应为 30001');
153
+ });
154
+
155
+ test('check 包含参数校验与身份校验', async () => {
156
+ const api = new Api({
157
+ name: 'hello',
158
+ path: '/hello',
159
+ method: 'ALL',
160
+ dir: __dirname,
161
+ main: './fixtures/hello.js',
162
+ check_param: false
163
+ });
164
+ const ctx = makeCtx({ path: '/hello', method: 'GET' });
165
+ const error = await api.check({}, ctx, makeOptions());
166
+ assert.strictEqual(error, null, '无需登录且无参数校验时应通过');
167
+ });
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ /**
5
+ * 接口主函数
6
+ * @param {object} ctx HTTP上下文
7
+ * @param {object} options 数据管理器
8
+ * @returns {object} 执行结果
9
+ */
10
+ async main(params, ctx, options) {
11
+ const req = ctx.request;
12
+ return { query: req.query, body: req.body };
13
+ }
14
+ };
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ /**
5
+ * 接口主函数(总是抛错,用于测试异常路径)
6
+ * @param {object} ctx HTTP上下文
7
+ * @param {object} options 数据管理器
8
+ * @returns {object} 执行结果
9
+ */
10
+ async main(params, ctx, options) {
11
+ throw new Error('模拟脚本异常');
12
+ }
13
+ };
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ /**
5
+ * 接口主函数
6
+ * @param {object} ctx HTTP上下文
7
+ * @param {object} options 数据管理器
8
+ * @returns {object} 执行结果
9
+ */
10
+ async main(params, ctx, options) {
11
+ return { code: 0, message: '成功', data: { hello: 'world' } };
12
+ }
13
+ };
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "param",
3
+ "title": "参数配置",
4
+ "get": {
5
+ "query": ["name"],
6
+ "query_required": ["name"]
7
+ },
8
+ "list": [
9
+ {
10
+ "name": "name",
11
+ "title": "名称",
12
+ "type": "string",
13
+ "string": {
14
+ "not_empty": true,
15
+ "max": 10
16
+ }
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 构造模拟 HTTP 请求上下文
5
+ * @param {object} cfg 配置项
6
+ * @param {string} cfg.path 请求路径
7
+ * @param {string} cfg.method 请求方法
8
+ * @param {object} cfg.query URL 参数
9
+ * @param {object} cfg.body 正文参数
10
+ * @param {object} cfg.headers 请求头
11
+ * @param {string} cfg.type 请求类型
12
+ * @returns {object} 请求上下文
13
+ */
14
+ function makeCtx(cfg = {}) {
15
+ const req = {
16
+ path: cfg.path || '/',
17
+ method: cfg.method || 'GET',
18
+ query: cfg.query || {},
19
+ body: cfg.body || {},
20
+ headers: cfg.headers || {},
21
+ type: cfg.type || 'json',
22
+ url: cfg.url || (cfg.path || '/')
23
+ };
24
+ return {
25
+ request: req,
26
+ response: { type: '' },
27
+ session: {},
28
+ headers: req.headers,
29
+ set(key, value) {
30
+ this.response[key] = value;
31
+ },
32
+ get status() {
33
+ return this.response.status;
34
+ },
35
+ set status(value) {
36
+ this.response.status = value;
37
+ }
38
+ };
39
+ }
40
+
41
+ /**
42
+ * 构造数据管理器
43
+ * @returns {object} 数据管理器
44
+ */
45
+ function makeOptions() {
46
+ return {
47
+ ret: null,
48
+ new() {
49
+ return this;
50
+ },
51
+ getObj() {
52
+ return null;
53
+ }
54
+ };
55
+ }
56
+
57
+ module.exports = { makeCtx, makeOptions };