diff --git a/app/cms/model/User.js b/app/cms/model/User.js index 808aeac..2d2196d 100644 --- a/app/cms/model/User.js +++ b/app/cms/model/User.js @@ -1,8 +1,11 @@ module.exports = { - doc : "用户", - api : true,//是否需要生成api接口 - model: { - uid: {type: "STRING", comment: '用户id'}, - age: {type: "INTEGER", comment: '年龄', defaultValue: 0,}, - } + doc: "用户", + api: true,//是否需要生成api接口 + model: { + uid: {type: "STRING", comment: '用户id'}, + age: {type: "INTEGER", comment: '年龄', defaultValue: 0}, + age2: {type: "INTEGER", comment: '年龄', defaultValue: 0}, + }, } + + diff --git a/extend/mysql/api.js b/extend/mysql/api.js new file mode 100644 index 0000000..00750ce --- /dev/null +++ b/extend/mysql/api.js @@ -0,0 +1,677 @@ +//封装了一些Sequelize的api +const Sequelize = require("sequelize"); +const xe = require("xe-utils") +module.exports = class Api { + constructor(sequelize,models) { + this.sequelize = sequelize + this.models = models + this.init() + } + + init() { + this.tableName = "" + this.tableWhere = "" + this.tableData = "" + this.tablePage = "" + this.tableLimit = "" + this.tableOrder = "" + this.tableGroup = "" + this.tableAttributes = "" + this.t = "" + } + + /** + * 指定表名 + * @param {object} value 表名. + */ + table(value) { + this.tableName = value + return this + } + + /** + * 设置数据 + * @param {any} value 数据. + */ + data(value) { + this.tableData = value + return this + } + + /** + * 设置筛选条件 + * @param {object} value 筛选条件对象. + */ + where(value) { + this.tableWhere = value + return this + } + + /** + * 设置页数 + * @param {int} value 页数从0开始. + */ + page(value) { + this.tablePage = value + return this + } + + + /** + * 设置条数 + * @param {int} value 条数. + */ + limit(value) { + this.tableLimit = value + return this + } + + /** + * 设置排序 + * @param {array} value 排序(默认按更新时间排序). + */ + order(value) { + this.tableOrder = value + return this + } + + /** + * 设置数据分组 + * @param {string|array} value 传需要分组的字段['createdAt']. + */ + group(value) { + this.tableGroup = value + return this + } + + /** + * 常用时间筛选 + * @param {string|array|Number} value 时间内容:按时间段:['2000-1-1','2000-1-2'],按常用时间:day,按最近60分钟:60. + * @param {string} field 时间字段(默认createdAt字段) + */ + time(value, field) { + if (!this.tableName) { + throw "请传表名" + } + if (!field) { + field = "createdAt" + } + if (!this.tableWhere) { + this.tableWhere = {} + } + if (!this.tableWhere['$and']) { + this.tableWhere['$and'] = [] + } + const {fn, col, where, literal} = Sequelize + switch (value) { + case 'yday': // 昨天 + this.tableWhere['$and'].push(where(fn('TO_DAYS', col(this.tableName + '.' + field)), '-', fn('TO_DAYS', fn('NOW')), '<=', 1)) + break; + case 'day': //当天 + this.tableWhere['$and'].push(where(fn('TO_DAYS', col(this.tableName + '.' + field)), '=', fn('TO_DAYS', fn('NOW')))) + break; + case 'week': //本周 + this.tableWhere['$and'].push(where(fn('YEARWEEK', fn('date_format', col(this.tableName + '.' + field), '%Y-%m-%d')), '=', fn('YEARWEEK', fn('now')))) + break; + case 'month': //当月 + this.tableWhere['$and'].push(where(fn('DATE_FORMAT', col(this.tableName + '.' + field), '%Y%m'), '=', fn('DATE_FORMAT', fn('CURDATE'), '%Y%m'))) + break; + case 'lmonth': //上个月 + this.tableWhere['$and'].push(where(fn('PERIOD_DIFF', fn('date_format', fn('now'), '%Y%m'), fn('date_format', col(this.tableName + '.' + field), '%Y%m')), '=', 1)) + break; + case 'year': //当年 + this.tableWhere['$and'].push(where(fn('YEAR', col(this.tableName + '.' + field)), '=', fn('YEAR', fn('NOW')))) + break; + default: + if (xe.isArray(value)) { //时间范围筛选 + const data = {} + data[field] = {"$between": value} + this.tableWhere['$and'].push(data) + } + if (xe.isNumber(value)) { + const minute = {} + minute[field] = {"$lt": new Date(), "$gt": new Date(new Date() - value * 60 * 1000)} + this.tableWhere['$and'].push(minute) + } + break; + } + + return this + } + + /** + * 设置事务 + */ + async setTransaction() { + this.t = await this.sequelize.transaction() + return this.t + } + + /** + * 仅选择某些属性(只返回指定字段) + * @param {array} value 仅选择某些属性['foo', ['bar', 'baz'], 'qux']. + */ + attributes(value) { + this.tableAttributes = value + return this + } + + /** + * 查询1条数据 + * @param {Transaction} options.transaction 运行查询的事务. + */ + async find(options) { + if (!this.tableName) { + throw "请传表名" + } + const res = await this.models[this.tableName].findOne( + { + where: this.tableWhere, + transaction: this.t + } + ) + this.init() + return res && res.dataValues || null + } + + /** + * 查询所有符合条件的数据 + * @return {array} dataValues 查询结果. + */ + async findAll() { + if (!this.tableName) { + throw "请传表名" + } + const res = await this.models[this.tableName].findAll( + { + where: this.tableWhere, + offset: (this.tablePage && this.tableLimit) ? this.tablePage - 1 * this.tableLimit : null, + limit: this.tableLimit, + order: this.tableOrder || [['updatedAt', 'DESC']], // 时间排序 + group: this.tableGroup, + attributes: this.tableAttributes, + transaction: this.t + } + ) + this.init() + return res.map(item => item.dataValues) + } + + /** + * 如果数据不存在就创建数据,否则反回查询结果 + * @return {object} dataValues 查询结果. + */ + async findOrCreate() { + if (!this.tableName) { + throw "请传表名" + } + if (!this.tableData) { + throw "请设置data" + } + const res = await this.models[this.tableName].findOrCreate( + { + defaults: this.tableData, + where: this.tableWhere, + transaction: this.t + } + ) + this.init() + return res + } + + + /** + * 分页查询数据 + * @param {array} args.include 关联查询. + * @return {int} count 总行数. + * @return {array} rows 数据列表. + */ + async findAndCountAll(args = {}) { + if (!this.tableName) { + throw "请传表名" + } + const {count, rows} = await this.models[this.tableName].findAndCountAll( + { + where: this.tableWhere, + offset: (this.tablePage && this.tableLimit) ? this.tablePage - 1 * this.tableLimit : null, + limit: this.tableLimit, + order: this.tableOrder || [['updatedAt', 'DESC']], // 时间排序 + group: this.tableGroup, + attributes: this.tableAttributes, + transaction: this.t + } + ) + this.init() + return {count, rows: rows.map(item => item.dataValues)} + } + + /** + * 字段值减小 + * @param {string} data 要减小的字段和值{xxx:1,xxxx:2}. + * @param {number} min 字段减小后的值不能小于最小值 + * @param {number} setValue 如果减小后的字段小于min,设置字段值为n + */ + async setDec(min, setValue) { + if (!this.tableName) { + throw "请传表名" + } + if (!this.tableData) { + throw"请数据" + } + if (!this.tableWhere) { + throw"请筛选值" + } + + let res = await this.models[this.tableName].findOne( + { + where: this.tableWhere, + transaction: this.t + } + ) + + for (let key of Object.keys(this.tableData)) { + if (res[key] - this.tableData[key] < min) { + if (setValue === 0 || setValue) { + const data = {} + Object.keys(this.tableData).map(item => { + data[item] = setValue + }) + await this.models[this.tableName].update( + data, + { + where: this.tableWhere, + transaction: this.t + } + ) + res = { + ...res.dataValues, + ...data + } + return {res: true, value: res} + } + return {res: false, value: res} + } + } + + await this.models[this.tableName].decrement( + this.tableData, + { + where: this.tableWhere, + transaction: this.t + } + ) + + res = await this.models[this.tableName].findOne( + { + where: this.tableWhere, + transaction: this.t + } + ) + this.init() + return {res: true, value: res.dataValues} + } + + /** + * 字段值增加 + * @param {string} data 要增加的字段和值{xxx:1,xxxx:2}. + * @param {number} max 字段增加后的值不能大于最大值 + * @param {number} setValue 如果增加后的字段大于max,设置字段值为n + */ + async setInc(max, setValue) { + if (!this.tableName) { + throw "请传表名" + } + if (!this.tableData) { + throw"请数据" + } + if (!this.tableWhere) { + throw"请筛选值" + } + let res = await this.models[this.tableName].findOne( + { + where: this.tableWhere, + transaction: this.t + } + ) + + for (let key of Object.keys(this.tableData)) { + if (res[key] + this.tableData[key] > max) { + if (setValue === 0 || setValue) { + const data = {} + Object.keys(this.tableData).map(item => { + data[item] = setValue + }) + await this.models[this.tableName].update( + data, + { + where: this.tableWhere, + transaction: this.t + } + ) + res = { + ...res.dataValues, + ...data + } + return {res: true, value: res} + } + return {res: false, value: res} + } + } + + await this.models[this.tableName].increment( + this.tableData, + { + where: this.tableWhere, + transaction: this.t + } + ) + + res = await this.models[this.tableName].findOne( + { + where: this.tableWhere, + transaction: this.t + } + ) + this.init() + return {res: true, value: res.dataValues} + + } + + /** + * 统计查询结果数 + */ + async count() { + if (!this.tableName) { + throw "请传表名" + } + const res = await this.models[this.tableName].count( + this.tableData, + { + where: this.tableWhere, + order: this.tableOrder || [['updatedAt', 'DESC']], // 时间排序 + group: this.tableGroup, + attributes: this.tableAttributes, + transaction: this.t + } + ) + this.init() + return res + } + + /** + * 求和 + */ + async sum() { + if (!this.tableName) { + throw "请传表名" + } + const res = await this.models[this.tableName].sum( + this.tableData, + { + where: this.tableWhere, + order: this.tableOrder || [['updatedAt', 'DESC']], // 时间排序 + group: this.tableGroup, + attributes: this.tableAttributes, + transaction: this.t + } + ) + this.init() + return res + } + + /** + * 查询最大值 + */ + async max() { + if (!this.tableName) { + throw "请传表名" + } + const res = await this.models[this.tableName].max( + this.tableData, + { + where: this.tableWhere, + order: this.tableOrder || [['updatedAt', 'DESC']], // 时间排序 + group: this.tableGroup, + attributes: this.tableAttributes, + transaction: this.t + } + ) + this.init() + return res + } + + /** + * 查询最小值 + */ + async min() { + if (!this.tableName) { + throw "请传表名" + } + const res = await this.models[this.tableName].min( + this.tableData, + { + where: this.tableWhere, + order: this.tableOrder || [['updatedAt', 'DESC']], // 时间排序 + group: this.tableGroup, + attributes: this.tableAttributes, + transaction: this.t + } + ) + this.init() + return res + } + + /** + * 更新数据 + * @param {object} data 数据. + * @param {boolean} options.paranoid 如果为 true,则只会更新未删除的记录。如果为 false,将更新已删除和未删除的记录。仅适用于模型的 options.paranoid 为真。. + * @param {Array} options.fields 要更新的字段(默认为所有字段) + * @param {boolean} options.validate 每一行在插入之前是否应该经过验证。如果一行未通过验证,则整个插入将失败 + * @param {boolean} options.hooks 在批量更新挂钩之后运行? + * @param {boolean} options.sideEffects 是否更新任何虚拟二传手的副作用。 + * @param {boolean} options.individualHooks 在更新挂钩之前运行?如果为真,这将执行一个 SELECT,然后执行单独的 UPDATE。需要一个选择,因为需要将行数据传递给钩子 + * @param {boolean | Array} options.returning 如果为真,则附加 RETURNING 以取回所有定义的值;如果是列名数组,则附加 RETURNING 以获取特定列(仅限 Postgres) + * @param {number} options.limit 要更新多少行(仅适用于 mysql 和 mariadb,对于 MSSQL 实现为 TOP(n);对于 sqlite,仅当存在 rowid 时才支持) + * @param {Function} options.logging在运行查询以记录 sql 时执行的函数。 + * @param {boolean} options.benchmark 将查询执行时间(以毫秒为单位)作为第二个参数传递给日志记录函数(options.logging)。 + * @param {Transaction} options.transaction 运行查询的事务 + * @param {boolean} options.silent 如果为 true,则不会更新 updatedAt 时间戳。 + */ + async update(options) { + if (!this.tableName) { + throw "请传表名" + } + if (!this.tableData) { + throw"请传数据" + } + if (!this.tableWhere) { + throw"请传where" + } + const res = await this.models[this.tableName].update( + this.tableData, + { + where: this.tableWhere, + ...options + } + ) + return res + } + + /** + * 删除数据 + * @param {object} options 参数. + * @param {boolean} options.hooks 在批量销毁挂钩之前运行. + * @param {boolean} options.individualHooks 如果设置为 true,destroy 将选择与 where 参数匹配的所有记录,并将在每行上的 destroy 钩子之前执行. + * @param {number} options.limit 要删除多少行. + * @param {boolean} options.force 删除而不是将 deletedAt 设置为当前时间戳(仅在启用偏执狂时适用). + * @param {boolean} options.truncate 如果设置为 true,支持它的方言将使用 TRUNCATE 而不是 DELETE FROM。如果表被截断,则忽略 where 和 limit 选项. + * @param {boolean} options.cascade 仅与 TRUNCATE 一起使用。截断所有具有对命名表的外键引用的表,或者截断由于 CASCADE 而添加到组中的任何表. + * @param {transaction} options.transaction 运行查询的事务. + * @param {Function} options.logging 在运行查询以记录 sql 时执行的函数。 + * @param {boolean} options.benchmark 将查询执行时间(以毫秒为单位)作为第二个参数传递给日志记录函数(options.logging)。 + */ + async delete(options) { + if (!this.tableName) { + throw "请传表名" + } + if (!this.tableWhere) { + throw"请传where" + } + const res = await this.models[this.tableName].destroy( + { + where: this.tableWhere, + ...options + } + ) + return res + } + + /** + * 保存数据,如果数据已存在就更新,否则创建数据,可以传对象或数组,如果是需要更新数据,必须包含id + */ + async save() { + if (!this.tableName) { + throw "请传表名" + } + if (!this.tableData) { + throw"请传要保存的数据" + } + let data = this.tableData + let updateOnDuplicate = [] + let keyData = {} + if (xe.isArray(data)) { + if (!data.length) { + throw"请传要保存的数据" + } + keyData = data[0] + } else { + keyData = data + data = [data] + } + + for (let key of Object.keys(keyData)) { + if (key !== 'id') { + updateOnDuplicate.push(key) + } + } + + const res = await this.models[this.tableName].bulkCreate(data, + {returning: true, updateOnDuplicate: updateOnDuplicate, transaction: this.t} + ) + this.init() + return res + } + + /** + * 通过json执行多个数据库操作任务 + * @param {array} taskList json格式的任务列表. + */ + + /*jsonTasks([ + {"table": "User", "where": {id: 999}, "find": "", "as": "user_data"},//as 设置结果别名 + { + "table" : "UserInfo", + "data" : { + "phone": "13126000000", + "uid": "$as.user_data.id",//根据别名 user_data 返回的结果中的id值作为数据保存 + }, "save": "" + }, + { + "as": "user_info", + "table" : "UserInfo", + "where" : { + "uid": "$as.user_data.id",//根据别名 user_data 返回的结果中的id值作为条件查询 + }, "find": "" + }, + + {"table": "User", "data": "age", "min": ""}, + { + "as": "user_list", "jsonTasks": [ //整个任务设置别名 + {"setTransaction": ""}, // 开始事务 + {"table": "User", "data": "age", "sum": ""}, + {"table": "User", "data": "age", "max": ""}, + {"table": "User", "data": "age", "min": ""}, + {"commitTransaction": ""}, //提交事务 + ] + }, + ])*/ + async jsonTasks(taskList) { + const res = [] + for (let tk of taskList) { + const tkKeys = Object.keys(tk) + for (let key of tkKeys) { + if (key === "as") { + continue; + } + if (key === "where" || key === "data") { + const findJsonRes = this.findJsonValue(tk[key], (n) => { + if (typeof n === "string") { + // console.log(tk[key],n); + if (n.indexOf("$as") !== -1) { + return true + } + } + return false + }) + if (findJsonRes) { + const jsonPath = findJsonRes.path.join(".") + const jsonPath2 = objectPath.get(tk[key], jsonPath);//$as.user_data.id + const asName = jsonPath2.split(".")[1] + const asFilter = res.filter(item => { + // console.log(Object.keys(item), asName); + return Object.keys(item)[0] === asName + }) + // console.log(asFilter); + // console.log(jsonPath2); + if (asFilter.length > 0) { + // console.log(objectPath.get(asFilter[0], jsonPath2.split(".").slice(1).join("."))); //999 + // console.log(jsonPath2.split(".").slice(1).join("."));//user_data.id + // console.log(tk[key]);//user_data.id + objectPath.set(tk[key], jsonPath, objectPath.get(asFilter[0], jsonPath2.split(".").slice(1).join("."))); + } + } + } + // console.log(tk[key]); + const tkRes = await this[key](tk[key]) + if ( + key === "save" || + key === "count" || + key === "sum" || + key === "max" || + key === "min" || + key === "find" || + key === "findAll" || + key === "findOrCreate" || + key === "jsonTasks" || + key === "findAndCountAll" + ) { + // console.log(tk["as"]); + res.push(tk["as"] ? {[tk["as"]]: tkRes} : tkRes) + } + + } + } + return res + } + + //深度遍历JSON,搜索值 + findJsonValue(n, value, path) { + if (n === value || (xe.isFunction(value) && value(n))) { + return {value: n, path} + } + path = path || [] + // 获取所有的子节点,并遍历 + if (typeof n === "object") { + const nkeys = Object.keys(n) + for (let k of nkeys) { + // concat() 方法用于连接两个或多个数组 + const res = this.findJsonValue(n[k], value, path.concat(k)); + if (res) { + return res + } + } + } + + } +} + diff --git a/extend/mysql/config.js b/extend/mysql/config.js index eee7bfd..e1c2a6f 100644 --- a/extend/mysql/config.js +++ b/extend/mysql/config.js @@ -43,4 +43,5 @@ module.exports = { }, }, }, + path:"app/*/model/*.js", } diff --git a/extend/mysql/index.js b/extend/mysql/index.js index b04dfb9..e5ab0b8 100644 --- a/extend/mysql/index.js +++ b/extend/mysql/index.js @@ -2,83 +2,75 @@ * mysql/sequelize * * 参考文档: + * https://sequelize.org/ + * https://sequelize.org/api/v6/identifiers * https://www.sequelize.com.cn/ */ const config = require("./config") const Sequelize = require("sequelize"); +const operatorsAliases = require("./operatorsAliases"); +const Api = require("./api"); module.exports = async (app) => { - const { - database, - username, - password, - options, - } = config - + const {database, username, password, options,} = config const sequelize = new Sequelize(database, username, password, { ...options, operatorsAliases, }); - try { - await sequelize.authenticate(); - console.log('数据库连接成功'); - } catch (error) { - console.error('数据库连接失败', error); - } - app.sql = sequelize + // 加载model文件 + let list = await app.load(config.path) + list.forEach(item => { + const {res, parse} = item + const model = {} + for (let key of Object.keys(res.model)) { + res.model[key].type = Sequelize[res.model[key].type] + model[key] = res.model[key] + } + model['id'] = { + type: Sequelize.INTEGER, + comment: '表自增id', + allowNull: false, + unique: 'id', + primaryKey: true, + autoIncrement: true, + } + sequelize.define(parse.name, model) + }) + + + app.models = sequelize.models + const api = new Api(sequelize, sequelize.models) + app.db = api + // console.log(api); + console.log(await app.db.table("User").find()); //等待所有插件载入完成后,自动更新数据库字段 //把所有模型的md5值存入到redis里面去,哪个文件的md5有变动就更新哪个表的字段. - //todo - app.willReadyList.push(()=>{ - + app.willReadyList.push(async () => { + list.forEach(async item => { + const {res, parse} = item + const md5 = await app.redis.get("md5:" + parse.name) + const newMd5 = getFileMd5(app.root + '/' + parse.dir + "/" + parse.base) + if (!md5 || newMd5 !== md5) { + //自动更新表字段 + await app.models[parse.name].sync({alter: true}); + console.log("自动更新表:", parse.name); + //创建新的md5 + await app.redis.mset("md5:" + parse.name, newMd5) + } + + }) + }) } -//定义别名 -function operatorsAliases() { - const Op = Sequelize.Op; - //操作符别名 - const operatorsAliases = { - $eq: Op.eq, - $ne: Op.ne, - $gte: Op.gte, - $gt: Op.gt, - $lte: Op.lte, - $lt: Op.lt, - $not: Op.not, - $in: Op.in, - $notIn: Op.notIn, - $is: Op.is, - $like: Op.like, - $notLike: Op.notLike, - $iLike: Op.iLike, - $notILike: Op.notILike, - $regexp: Op.regexp, - $notRegexp: Op.notRegexp, - $iRegexp: Op.iRegexp, - $notIRegexp: Op.notIRegexp, - $between: Op.between, - $notBetween: Op.notBetween, - $overlap: Op.overlap, - $contains: Op.contains, - $contained: Op.contained, - $adjacent: Op.adjacent, - $strictLeft: Op.strictLeft, - $strictRight: Op.strictRight, - $noExtendRight: Op.noExtendRight, - $noExtendLeft: Op.noExtendLeft, - $substring: Op.substring, - $startsWith: Op.startsWith, - $endsWith: Op.endsWith, - $and: Op.and, - $or: Op.or, - $any: Op.any, - $all: Op.all, - $values: Op.values, - $col: Op.col - }; - return operatorsAliases +//文件md5值 +function getFileMd5(url) { + const buffer = require('fs').readFileSync(url); + const hash = require('crypto').createHash('md5'); + hash.update(buffer, 'utf8'); + const md5 = hash.digest('hex'); + return md5 } diff --git a/extend/mysql/operatorsAliases.js b/extend/mysql/operatorsAliases.js new file mode 100644 index 0000000..01fb063 --- /dev/null +++ b/extend/mysql/operatorsAliases.js @@ -0,0 +1,46 @@ +//定义别名 +const Sequelize = require("sequelize"); +module.exports = () => { + const Op = Sequelize.Op; + //操作符别名 + const operatorsAliases = { + $eq: Op.eq, + $ne: Op.ne, + $gte: Op.gte, + $gt: Op.gt, + $lte: Op.lte, + $lt: Op.lt, + $not: Op.not, + $in: Op.in, + $notIn: Op.notIn, + $is: Op.is, + $like: Op.like, + $notLike: Op.notLike, + $iLike: Op.iLike, + $notILike: Op.notILike, + $regexp: Op.regexp, + $notRegexp: Op.notRegexp, + $iRegexp: Op.iRegexp, + $notIRegexp: Op.notIRegexp, + $between: Op.between, + $notBetween: Op.notBetween, + $overlap: Op.overlap, + $contains: Op.contains, + $contained: Op.contained, + $adjacent: Op.adjacent, + $strictLeft: Op.strictLeft, + $strictRight: Op.strictRight, + $noExtendRight: Op.noExtendRight, + $noExtendLeft: Op.noExtendLeft, + $substring: Op.substring, + $startsWith: Op.startsWith, + $endsWith: Op.endsWith, + $and: Op.and, + $or: Op.or, + $any: Op.any, + $all: Op.all, + $values: Op.values, + $col: Op.col + }; + return operatorsAliases +} diff --git a/lib/bamboo/index.js b/lib/bamboo/index.js index 538b542..6710bc2 100644 --- a/lib/bamboo/index.js +++ b/lib/bamboo/index.js @@ -26,7 +26,7 @@ module.exports = class Bamboo extends Koa { for (let listElement of list) { await listElement.res(this) } - this.willReadyList.push(()=>{ + this.willReadyList.push(() => { this.startServer() }) await this.willReady() @@ -36,7 +36,7 @@ module.exports = class Bamboo extends Koa { /*扩展已经加载完成触发*/ async willReady() { for (let WRL of this.willReadyList) { - WRL() + await WRL() } } @@ -56,7 +56,7 @@ module.exports = class Bamboo extends Koa { if (er) { reject(er) } - console.log("获取到的文件:", files); + // console.log("获取到的文件:", files); files = files.map(item => { const parse = path.parse(item); return { diff --git a/lib/bamboo2/index.js b/lib/bamboo2/index.js index fc4c10c..d46abbf 100644 --- a/lib/bamboo2/index.js +++ b/lib/bamboo2/index.js @@ -483,8 +483,8 @@ module.exports = class Bamboo extends Koa { } /** - * 数据分组 - * @param {string|array} value 传需要分组的字段['createdAt']. + * 仅选择某些属性 + * @param {array} value 仅选择某些属性['foo', ['bar', 'baz'], 'qux']. */ attributes(value) { this.tableAttributes = value