|
@@ -0,0 +1,229 @@
|
|
|
+'use strict';
|
|
|
+/**
|
|
|
+ * 基于meta描述数据生成Controller类
|
|
|
+ * 按照描述信息将web请求中的数据提取出来,组织为service的调用参数
|
|
|
+ * {Controller名字}.json
|
|
|
+ * meta文件属性描述:
|
|
|
+ * service 字符串或者对象,字符串表示服务方法名,默认与action同名;
|
|
|
+ * 对象可包含name、func两个属性,func与使用字符串是含义相同,
|
|
|
+ * name表示服务名
|
|
|
+ * parameters 请求参数描述,对象类型,可以包含属性为:'query'、 'params'、 'requestBody'、'options',
|
|
|
+ * 分别对应eggjs中的三个请求参数来源和可选参数类型
|
|
|
+ * options 可选参数,对象类型,可以指定排序字段和一些常量参数值,具体内容格式随意,在服务中解析
|
|
|
+ * params 路由参数,数组类型,从ctx.params中提取数据
|
|
|
+ * requestBody 请求数据,数组类型,从ctx.request.body中提取数据
|
|
|
+ * query 查询参数,数组类型'options',从ctx.query中提取数据
|
|
|
+ * 完整格式:
|
|
|
+ "action": {
|
|
|
+ "parameters": {
|
|
|
+ "params": ["field1", "field2",...], // 可选
|
|
|
+ "query": ["field1", "field2",...], // 可选
|
|
|
+ "requestBody": ["field1", "field2",...], // 可选
|
|
|
+ "options": { "ext1": "value1"}, // 可选
|
|
|
+ },
|
|
|
+ "service": "query", // 可选
|
|
|
+ "options": { // 可选
|
|
|
+ "sort": ["field1", "field2",...]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ * 简单格式:
|
|
|
+ * 如果meta对象中没有parameters属性,则按简单格式处理,即整个meta的内容都作为parameters来处理
|
|
|
+ "action": {
|
|
|
+ "params": ["attr1", "attr2",...], // 可选
|
|
|
+ "query": ["attr1", "attr2",...], // 可选
|
|
|
+ "requestBody": ["attr1", "attr2",...], // 可选
|
|
|
+ "options": { "ext1": "value1"}, // 可选
|
|
|
+ },
|
|
|
+* meta实例:
|
|
|
+ {
|
|
|
+ "create": {
|
|
|
+ "requestBody": ["code","name","order"]
|
|
|
+ },
|
|
|
+ "delete": {
|
|
|
+ "query": { "id": "_id" }
|
|
|
+ },
|
|
|
+ "update": {
|
|
|
+ "query": ["_id"],
|
|
|
+ "requestBody": ["name","order"]
|
|
|
+ },
|
|
|
+ "list": {
|
|
|
+ "parameters": {},
|
|
|
+ "service": "query",
|
|
|
+ "options": {
|
|
|
+ "sort": ["order", "code"]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "fetch": {
|
|
|
+ "query": ["_id"]
|
|
|
+ "service": {
|
|
|
+ "name": "items",
|
|
|
+ "func": "query"
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+* 服务接口:
|
|
|
+* someService.someMethod(requestParams, requestBody, options)
|
|
|
+* 服务参数:
|
|
|
+* requestParams 请求参数,必须
|
|
|
+* requestBody 请求数据,可选
|
|
|
+* options 可选参数,可选
|
|
|
+*/
|
|
|
+const assert = require('assert');
|
|
|
+const _ = require('lodash');
|
|
|
+const is = require('is-type-of');
|
|
|
+const { trimData } = require('naf-core').Util;
|
|
|
+
|
|
|
+const key_reg = /^([!#]*)(.*)$/;
|
|
|
+
|
|
|
+const MapData = (data, names) => {
|
|
|
+ if (_.isArray(names)) {
|
|
|
+ // return names.reduce((p, c) => {
|
|
|
+ // p[c] = data[c];
|
|
|
+ // return p;
|
|
|
+ // }, {});
|
|
|
+ names = names.reduce((p, c) => {
|
|
|
+ // p[c] = c.charAt(0) === '!' ? c.substr(1) : c;
|
|
|
+ p[c] = key_reg.exec(c)[2];
|
|
|
+ return p;
|
|
|
+ }, {});
|
|
|
+ }
|
|
|
+ if (_.isObject(names)) {
|
|
|
+ // TODO: 参数映射方式,.eg { "id": "_id", "corp.id": "corpid", "src": "dest" }
|
|
|
+ return Object.entries(names).reduce((p, [ key, val ]) => {
|
|
|
+ // const required = key.charAt(0) === '!';
|
|
|
+ const [ , qualifier, _key ] = key_reg.exec(key);
|
|
|
+ key = _key;
|
|
|
+ const required = qualifier.includes('!');
|
|
|
+ const numeric = qualifier.includes('#');
|
|
|
+ let value = _.get(data, key);
|
|
|
+ if (required) {
|
|
|
+ // key = key.substr(1);
|
|
|
+ value = _.get(data, key);
|
|
|
+ assert(!_.isUndefined(value), `${key}不能为空`);
|
|
|
+ }
|
|
|
+ if (!_.isString(val)) {
|
|
|
+ val = key;
|
|
|
+ }
|
|
|
+ if (!_.isUndefined(value)) {
|
|
|
+ if (numeric) {
|
|
|
+ assert(_.isNumber(value) || (_.isString(value) && /^-?\d+$/.test(value)), `${key}必须为数字`);
|
|
|
+ value = Number(value);
|
|
|
+ }
|
|
|
+ p[val] = value;
|
|
|
+ }
|
|
|
+ return p;
|
|
|
+ }, {});
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+};
|
|
|
+let MapOptions;
|
|
|
+const MapParameters = (ctx, opts) => {
|
|
|
+ const include = [ 'parameters', 'query', 'params', 'requestBody', 'options' ];
|
|
|
+ const _opts = trimData({ ...opts }, null, include);
|
|
|
+ const keys = Object.keys(_opts);
|
|
|
+ return keys.map(key => {
|
|
|
+ // 嵌套调用
|
|
|
+ if (key === 'parameters') return MapParameters(ctx, opts[key]);
|
|
|
+ if (key === 'options') return MapOptions(ctx, opts[key]);
|
|
|
+
|
|
|
+ let data = ctx[key];
|
|
|
+ if (key === 'requestBody') data = ctx.request.body;
|
|
|
+ const names = opts[key];
|
|
|
+ // if (_.isArray(names)) {
|
|
|
+ // return names.reduce((p, c) => {
|
|
|
+ // p[c] = data[c];
|
|
|
+ // return p;
|
|
|
+ // }, {});
|
|
|
+ // }
|
|
|
+ // return data;
|
|
|
+ return MapData(data, names);
|
|
|
+ }).reduce((p, c) => {
|
|
|
+ if (c) {
|
|
|
+ p = { ...c, ...p };
|
|
|
+ }
|
|
|
+ return p;
|
|
|
+ }, {});
|
|
|
+};
|
|
|
+const MapRequestBody = (ctx, opts) => {
|
|
|
+ // if (_.isArray(opts)) {
|
|
|
+ // const data = ctx.request.body;
|
|
|
+ // return opts.reduce((p, c) => {
|
|
|
+ // p[c] = data[c];
|
|
|
+ // return p;
|
|
|
+ // }, {});
|
|
|
+ // }
|
|
|
+ // return MapParameters(ctx, opts) || {};
|
|
|
+
|
|
|
+ if (_.isObject(opts) && [ 'parameters', 'query', 'params', 'requestBody' ].some(p => Object.keys(opts).includes(p))) {
|
|
|
+ return MapParameters(ctx, opts) || {};
|
|
|
+ }
|
|
|
+ return MapData(ctx.request.body, opts);
|
|
|
+};
|
|
|
+MapOptions = (ctx, opts) => {
|
|
|
+ const exclude = [ 'parameters', 'query', 'params', 'requestBody' ];
|
|
|
+ const _opts = trimData({ ...opts }, exclude) || {};
|
|
|
+ const params = MapParameters(ctx, opts) || {};
|
|
|
+
|
|
|
+ if (!_.isUndefined(params.skip) && !_.isNumber(params.skip)) params.skip = Number(params.skip);
|
|
|
+ if (!_.isUndefined(params.limit) && !_.isNumber(params.limit)) params.limit = Number(params.limit);
|
|
|
+
|
|
|
+ return { ...params, ..._opts };
|
|
|
+};
|
|
|
+
|
|
|
+const CrudController = (cls, meta) => {
|
|
|
+ Object.keys(meta)
|
|
|
+ .forEach(key => {
|
|
|
+ // Do not override existing functions
|
|
|
+ if (!cls.prototype[key]) {
|
|
|
+ const { parameters, requestBody, options } = meta[key];
|
|
|
+ cls.prototype[key] = async function() {
|
|
|
+ const { ctx } = this;
|
|
|
+ const requestParams = MapParameters(ctx, parameters || meta[key]) || {};
|
|
|
+ const _requestBody = requestBody && MapRequestBody(ctx, requestBody);
|
|
|
+ const _options = options && MapOptions(ctx, options);
|
|
|
+ const serviceParams = [ requestParams ];
|
|
|
+ if (requestBody) {
|
|
|
+ serviceParams.push(_requestBody);
|
|
|
+ }
|
|
|
+ if (options) {
|
|
|
+ serviceParams.push(_options);
|
|
|
+ }
|
|
|
+
|
|
|
+ let { service } = meta[key];
|
|
|
+ // 修改service元数据的定义方式
|
|
|
+ // const funcName = (_.isObject(service) && service.action) || (_.isString(service) && service) || key;
|
|
|
+ // const provider = (_.isObject(service) && service.name && ctx.service[service.name]) || this.service;
|
|
|
+ if (_.isString(service)) { // TODO: 解析service为对象
|
|
|
+ const tokens = service.split('.');
|
|
|
+ service = { action: tokens.pop() };
|
|
|
+ if (tokens.length > 0) {
|
|
|
+ service.name = tokens;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const funcName = (_.isObject(service) && service.action) || key;
|
|
|
+ const provider = (_.isObject(service) && service.name && _.get(ctx.service, service.name)) || this.service;
|
|
|
+
|
|
|
+ const func = provider[funcName];
|
|
|
+ if (!is.asyncFunction(func)) {
|
|
|
+ throw new Error(`service not support function ${funcName}`);
|
|
|
+ }
|
|
|
+ const data = await func.call(provider, ...serviceParams);
|
|
|
+ let res = { data };
|
|
|
+ // 统计数据总数,处理分页
|
|
|
+ if (_options && _options.count) {
|
|
|
+ const funcName = _.isString(_options.count) ? _options.count : 'count';
|
|
|
+ const func = provider[funcName];
|
|
|
+ if (!is.asyncFunction(func)) {
|
|
|
+ throw new Error('not support count function');
|
|
|
+ }
|
|
|
+ const total = await func.call(provider, ...serviceParams);
|
|
|
+ res = { data, total };
|
|
|
+ }
|
|
|
+ this.ctx.ok(res);
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return cls;
|
|
|
+};
|
|
|
+
|
|
|
+module.exports = CrudController;
|