十一、通用控制器,Response中间件及错误日志

文章目录

  • Jwt 认证
    • authenticate.ts 问题
    • 已经登录用户缓存
  • Response 响应中间件
    • 响应中间件Amis封装
    • 响应中间件Amis使用
  • 错误日志 log4js
    • 安装
    • log4js配置
    • 重载nodejs的console.log
      • .gitignore
    • 封装Error
  • 通用控制器
    • 通用控制器实现
    • 完善Swagger
      • description
      • properties
      • 完整GeneralSwagger.ts
      • 修改config/routes.js 实现控制器和swagger关联
      • 修改swaggergenerator.js
    • Swagger 测试

Jwt 认证

我们通过api/policies/authenticated.ts 对大部分控制器进行身份认证,在身份认证里面,我们解密前端提交过来的token,如果解密成功,我们会去查询解密出来的用户信息数据库里面是否存在,如果不存在,就返回403禁止访问的状态。

authenticate.ts 问题

这个做法本身没有问题,问题是前端的每一个页面都可能包含多个api,这样前端一打开请求就需要多次进行身份认证,那就会产生多次数据库查询,这些查询都是重复的,应该是可以减少查询次数的。

也可以改造成只要通过jwt把token的用户信息解密出来,不需要到数据库里面进行再次查询比对的,因为jwt的加密是可靠的,我们解密出来的用户信息已经是和数据库里面的用户信息一样的。但是这样做有个毛病,就是token如果设置过期时间比较长,比如7天,那么我们后端删除掉一个用户,在这7天内,只要该用户没有退出登录TA就可以照常使用,这个就显得很奇怪。

已经登录用户缓存

为了解决上面这个问题,我们需要维护一个数组作为服务器的内存变量存在,对于已经解密token并且到数据库里面进行比对过的用户,我们把该用户的id缓存到内存数组中。下次该用户的token再次验证的时候,如果token解密成功并且内存数组中也有该用户的id,那我们就不需要再次查询数据库了,而是直接认证成功了。如果解密成功但是内存数组里面没有该用户id,那我们就再次到数据库里面去查询。
这样一来,数据库的查询次数就减少了许多。
内存数组的缓存,可以设置一个比较短的时间,比如1小时。一小时之后自动过期。那么如果我们后台删除掉一个用户,该用户最多还有一个小时可以正常使用,超过这一小时,该用户查询数据就会失败。
为此,在utils/wlSimulate.ts里面添加全局数组的缓存管理:

//已经登录用户信息缓存
var userLogged: Array<{ userId: number; dt: number; }> = [];
class wlSimulate extends wlBase {
  /**
   * 缓存过期时间1小时
   */
  private cacheExpire: number = 3600 * 1000;
  //#region 已登录用户id的全局数组
  /**
   * 检查全局变量缓存里面是否有uId
   * @param uId 用户id
   * @returns 
   */
  checkUserIdInCache(uId: number): boolean {
    let userDataIndex = userLogged.findIndex((x: any) => {
      return x.userId == uId;
    })
    if (userDataIndex < 0) return false;
    let userData = userLogged[userDataIndex];
    if (!userData.dt) return false;
    let timeSpan = new Date().getTime() - userData.dt;
    if (timeSpan > this.cacheExpire) {
      userLogged.splice(userDataIndex, 1);//删除      
      return false;
    }
    return true;
  }
  addUserId2Cache(uId: number) {
    let userDataIndex = userLogged.findIndex((x: any) => {
      return x.userId == uId;
    })
    if (userDataIndex < 0) userLogged.push({ userId: uId, dt: new Date().getTime() });
  }
  //#endregion
  ....
}

修改api/policies/authenticated.ts 如下:

import Api from "typing/Api";
import wlSimulate from "utils/wlSimulate";
var jwt = require("jsonwebtoken");
declare var sails: any;
declare var _: any;
async function authenticated(req: Api.SailsRequest, res: Api.Response, next: Function): Promise<any> {
  var bearerToken;
  var bearerHeader = req.headers['authorization'];
  if (!bearerHeader) return res.status(403).send('您没有权限访问本页面');
  var bearer = bearerHeader.split(" ");
  if (bearer.length < 2) return res.status(401).send('authorization格式错误');
  if (bearer[0] !== "Bearer") return res.status(401).send('authorization应该以Bearer为开头');
  bearerToken = bearer[1];
  let wl = new wlSimulate(sails);
  try {
    var decoded = await jwt.verify(bearerToken, sails.config.models.dataEncryptionKeys.default);
    let User = null
    if (wl.checkUserIdInCache(decoded.id)) {
      User = decoded;
    } else {
      User = await wl.findOne('user', { id: decoded.id });
      if (!User) return res.status(403).send('找不到用户信息');
      wl.addUserId2Cache(decoded.id)
    }
    delete User.password;
    req.user = User;
    next();
    return true;
  } catch (err: any) {
    if (err.name == 'TokenExpiredError') {
      return res.status(403).send('登录超期,请重新登录后再试');
    }
    return res.status(401).send(err);
  }
}
export = authenticated

Response 响应中间件

在 Sails 应用程序中编写的大部分代码都是中间件,它在请求和响应之间运行,即在请求/响应堆栈的 “中间”。在 MVC 框架中,"中间件"一词通常更具体地指在路由处理代码(即控制器操作)之前或之后运行的代码,使得同一段代码可以应用于多个路由或操作。Sails 具有强大的中间件设计模式支持。

在Sails的这些中间件中,Custom responses是一个自定义响应的中间件,Sails应用程序附带了一些预先配置的响应,可以从动作代码调用。这些默认响应可以处理“资源未找到”(notFound响应)和“内部服务器错误”(serverError响应)等情况。如果应用程序需要修改默认响应的工作方式,或者创建新的响应,可以通过将文件添加到api /responses文件夹来实现这一点。

响应中间件Amis封装

因为我们前端使用amis库,这个库对请求的api是有一定格式要求的,因此我们需要对Sails的响应做一个满足amis库要求的中间件封装,具体封装代码如下:

import { AmisResponse, ResponseHandlerFn, TableData } from "typing/Api";

function amis(this: any, data: Record<string, unknown> | TableData, status?: number, msg?: string): ResponseHandlerFn {
  let statusCode = status || <number>this.res.statusCode;
  let dataObject = <Record<string, unknown> | TableData>data;
  if (typeof dataObject === 'string') {
    dataObject = { text: data };    
  }
  let resData: AmisResponse = {
    status: statusCode == 200 ? 0 : -1,
    msg: msg || '',
    data: dataObject
  }
  return this.res.send(resData);
}
export = amis;

其中typeing/Api里面对相关数据类型进行定义:

/**
     * Amis 表格组件需要的返回格式
     */
    interface TableData {
        status?: number;
        msg?: string;
        /**
         * 本次查询总共可以查到的数量
         */
        count: number;
        /**
         * 用于返回数据源数据,格式是数组
         */
        rows: Array;
        /**
         * 当前执行的sql,调试的时候使用
         */
        sql?: string;
    }
    /**
     * Amis 专用返回格式
     */
    interface AmisResponse {
        status: number;
        msg: string;
        data: Object | TableData;
    }

响应中间件Amis使用

定义好Amis响应之后,原来我们在控制器里面的代码是直接使用:

 res.status(200).send(result);

其中status后面可以定义要返回给前端的状态码,观察api/response/amis.ts代码里面的封装,可以看到amis函数的第一个参数this: any, 这个参数获取调用者的实例对象,可以读取res的statusCode状态码,也可以通过第二个参数传递状态码,因此调用amis响应可以有两种方式:

res.status(200).amis(result);
//或是
res.amis(result, 200);
//因为默认状态下状态码是200,所以也可以不写,直接这样调用
res.amis(result);

错误日志 log4js

作为服务端,程序的稳健是非常重要的事情。通常我们会通过try/catch来捕获错误,捕获到错误的时候,可以选择发送到前端,也可以选择console.log输出到控制台(通常二者兼做)。nodejs的控制台输出有一个问题是如果输出的内容太多或是服务重启,这些错误信息可能就会消失或丢失一部分。为了可以日后倒查错误日志,我们需要把捕获到的错误信息写入到服务器文件中,最好可以每天自动产生一个系统错误日志文件,以系统日期为文件名称方便查询。

这个可以自己写代码完成,也可以采用第三方库,log4js是一个比较理想的选择。

log4js是一种JavaScript日志库,它可以帮助您在Node.js应用程序中记录日志。它可以将日志记录到文件、控制台、远程服务器等多种位置。它还支持多种日志级别,如调试、信息、警告和错误。

安装

npm install log4js --save

log4js配置

Log4js config下面有三个重要属性:
appenders:记录器对象,自定义不同的记录器(log输出位置)。
categories:log 类型,自定义log不同输出方式。
level:log输出等级,大于某等级的log才会输出。

const log4js = require('log4js') // 加载log4js模块
const path = require('path')
log4js.configure({
  appenders: {
    // 控制台输出
    console: { type: 'console' },
    // 全部日志文件
    app: {
      type: 'file',
      filename: path.join(__dirname, './logs/app'),
      maxLogSize: 1024 * 500, //一个文件的大小,超出后会自动新生成一个文件
      backups: 2, // 备份的文件数量
      pattern: "yyyy-MM-dd.log",
      encoding: 'utf-8',
      alwaysIncludePattern: true,
    },
    // 错误日志文件
    errorFile: {
      type: 'file',
      filename: path.join(__dirname, './logs/error'),
      maxLogSize: 1024 * 500, // 一个文件的大小,超出后会自动新生成一个文件
      backups: 2, // 备份的文件数量
      pattern: "yyyy-MM-dd.log",
      alwaysIncludePattern: true,
    }
  },
  categories: {
    // 默认日志,输出debug 及以上级别的日志
    default: {
      appenders: [
        'app'
      ], level: 'debug'
    },
    // 错误日志,输出error 及以上级别的日志
    error: { appenders: ['errorFile'], level: 'error' },
  },
  replaceConsole: false,   // 替换console.log  
});
// 获取默认日志
const Logger = log4js.getLogger();
// 获取错误级别日志
const errorLogger = log4js.getLogger('error');

在 log4js 中,appenders 是日志输出的目标地。例如,内置的 appenders 包括 “console” appender,它将日志消息输出到控制台,以及 “file” appender,它将日志消息写入文件。
在 log4js 中的 “file” appender中,可以配置文件名模式,其中包括日期、日志级别和应用程序名称等信息。其中 pattern: “yyyy-MM-dd.log”,实现在文件名称后面添加当前系统日期的功能。

现在我们可以通过Logger实现错误日志输出了。

重载nodejs的console.log

为了全局使用方便,我们可以重新定义nodejs的console,这样我们可以保持原来代码中的诸如console.log或console.error等内容不变,实现nodejs控制台输出之外,另外输出一份到服务器的系统日志文件中。
为此,在项目根目录中添加log2file.js (如果有同步github代码,原来就有这个文件)把上面的配置写到该文件,并增加重载console.log和console.error功能,代码如下:

/**
 * 用log4js 补充console.log 可以把错误写入日志文件
 */
const log4js = require('log4js') // 加载log4js模块
const path = require('path')
var util = require('util')
log4js.configure({
  appenders: {
    // 控制台输出
    console: { type: 'console' },
    // 全部日志文件
    app: {
      type: 'file',
      filename: path.join(__dirname, './logs/app'),
      maxLogSize: 1024 * 500, //一个文件的大小,超出后会自动新生成一个文件
      backups: 2, // 备份的文件数量
      pattern: "yyyy-MM-dd.log",
      encoding: 'utf-8',
      alwaysIncludePattern: true,
    },
    // 错误日志文件
    errorFile: {
      type: 'file',
      filename: path.join(__dirname, './logs/error'),
      maxLogSize: 1024 * 500, // 一个文件的大小,超出后会自动新生成一个文件
      backups: 2, // 备份的文件数量
      pattern: "yyyy-MM-dd.log",
      alwaysIncludePattern: true,
    }
  },
  categories: {
    // 默认日志,输出debug 及以上级别的日志
    default: {
      appenders: [
        'app'
      ], level: 'debug'
    },
    // 错误日志,输出error 及以上级别的日志
    error: { appenders: ['errorFile'], level: 'error' },
  },
  replaceConsole: false,   // 替换console.log  
});

// 获取默认日志
const Logger = log4js.getLogger();
// 获取错误级别日志
const errorLogger = log4js.getLogger('error');
/**
 * 重载console.log
 */
console.log = function () {
  //log4js的app输出(输出到文件app.xxx.log)
  Logger.info(util.format.apply(null, arguments) + '\n');
  //原始控制台输出
  process.stdout.write(util.format.apply(null, arguments) + '\n');
}

console.error = function () {
  // log4js的error输出(输出到文件error.xxx.log)
  errorLogger.error(util.format.apply(null, arguments) + '\n');
  //原始控制台输出
  process.stderr.write(util.format.apply(null, arguments) + '\n');
}

完成后,修改app.js,添加require(‘./log2file’); 实现项目启动就运行log2file的效果。

保存,并重新启动项目,可以看到项目根目录里面增加了一个logs文件夹,并且该文件夹里面多出了两个文件分别是app.xxxxx.log和error.xxxx.log 这两个文件就是log4js根据配置生成的日志文件。

.gitignore

系统启动后,我们可以打开app.xxx.log文件,应该可以看到里面已经有一些输出,这些输出是原来输出到控制台的内容,现在多输出一份到该文件了。

如果我们现在调整windows系统日期,重新启动项目后,可以发现logs文件夹里面会多出新日期命名的日志文件。log4js会每天产生两个日志文件。

这些文件显然是不需要提交到git上面的,所以在.gitignore里面添加:logs,添加后代码片段如下:

# config/local.js
logs
dist

可以看到vscode左边栏的logs变成灰色了。

做到这一步,请用postman发个userDel请求给sails,body里面什么都不要写,这个时候控制器会产生一个错误,观察该错误是否保存到文件中

封装Error

我们在wlSimulate.ts里面用存储过程模拟Waterline实现的数据库操作。在try/catch的时候,如果捕获到错误,我们一般的做法是throw (error);然后在userController里面再一次try/catch,捕获wlSimulate抛出来的错误。并且res.serverError(error); 发送status为500 的响应状态给到前端。

但是并不是所以错误都应该发送500 给前端我们需要一定的区分,有的错误是我们抛出只是要做提示,不需要返回500,依然返回200 然后前端做提示就可以了。比如userDel 这个api,如果前端提交过来的body是空白的,数据库的存储过程是会抛出错误的,“msg”: “不可以无条件删除”, 这个错误应该正常返回200给前端 而不是返回500。

并且捕获到的错误,发送到前端的时候,也应该有一定的格式,为此,我们需要统一捕获到的错误进行封装。并且捕获到的所有错误都应该输出到文件(前面我们改造成功之后直接console.log就可以)

在utils文件夹里面添加wrapError.ts ,代码如下:

import type { ErrorData } from "typing/Error";

/**
 * 分析异常对象,根据情况返回给前端
 * @param error 异常对象
 */
export function parseError(error: any): ErrorData {

    let resulst: ErrorData = {
        status: -1,
        needThrow: true,
        message: ""
    }
    //输出到控制台
    console.log('[原始信息error]:', error);
    // Logger.info('[原始信息error]:', error);
    let type = typeof error;
    if (type !== 'string') {
        let keys = [];
        for (const key in error) { keys.push(key); }
        console.log('[keys of error]:', keys.join(','))
    } else resulst.message = error;
    // sql 存储过程自定义错误
    if (error.raw) error = error.raw.error;
    resulst.needThrow = !(error.sqlState >= 45001 && error.sqlState <= 45999 || error.errno == 1062 && error.sqlState == 23000);
    resulst.code = error.sqlState;
    resulst.message = error.sqlMessage;
    if (error.sqlState == 45001) {
        resulst.message = '数据已经被修改过';
        resulst.remark = `修改过的id为${error.sqlMessage}`;
    } else if (error.sqlState == 45002) {
        resulst.message = '数据已经被修改过';
        resulst.remark = `当前where为${error.sqlMessage}`;
    }
    // sqlBuilder 错误
    if (error.details) {
        resulst.code = error.code;
        resulst.message = error.details;
    } else if (!resulst.message) resulst.message = error;
    if (resulst.needThrow) console.error(error);
    return resulst;
}

修改wlSimulate.ts 中所有函数捕获到错误的处理方式:

import { parseError } from "./wrapError";
.....
try{
} 
catch (error) {
  throw parseError(error);//抛出封装后的错误信息
}

在控制器中,对needThrow进行判断:

try{
} 
catch (error: any) {
	if (!error.needThrow) {//如果不需要抛出错误
    	  res.amis(error, -1, error.message);//调用amis中间件,封装响应内容
	} else res.serverError(error);//需要抛出错误的,发送服务器错误的500状态码给前端
}

保存重启后,用postman测试,可以看到:
十一、通用控制器,Response中间件及错误日志_第1张图片
这个错误信息已经是前端可以比较方便地处理的了。

通用控制器

我们的UserController已经实现了对user表的增删改查,实际的系统不可能只有一个表,如果按照UserController控制器的这种思路,那么一个表就需要一个控制器,并且不同的控制器功能大致相同,这种设计显然是不合理的。

应该是有一个通用的控制器,这个控制器通过前端传递过来的表名称就可以执行相应的增删改查动作。对于其他有特殊要求的(比如user表有登录功能)不方便通过通用控制器实现的,我们再专门有针对性的做一些专用的控制器和函数来实现。

通用控制器实现

通用控制器的实现是比较简单的,只需要在原来user控制器的基础上,添加一个对前端提交过来的tableName进行处理就可以了,其他增删改查都是一样的:
在api/controllers/里面添加GeneralController.ts 如下:

import Api from "typing/Api";
import { query2Sails } from "utils/Criteria";
import wlSimulate from "utils/wlSimulate";
import { StatisMethods } from "typing/OrmMethods";
import amis from "api/responses/amis";

//#region 基本CRUD
/**
 * 增加
 * @param req api请求
 * @param res api响应
 * @param next 回调函数
 */
export async function create(req: Api.SailsRequest, res: Api.Response, next: Function): Promise<any> {
    try {
        let wl = new wlSimulate(req._sails);//我们创建的模拟类
        let result = await wl.create(req.body.tableName, req.body, req.body.fetch);//调用模拟类里面的ctreate函数,调用存储过程
        result.status = 0;
        result.msg = '新增成功';
        res.amis(result);
    } catch (error: any) {
        if (!error.needThrow) {
            res.amis(error, -1, error.message);
        } else res.serverError(error);
    }
};
/**
 * 查询
 * @param req 
 * @param res 
 * @param next 
 */
export async function findOne(req: Api.SailsRequest, res: Api.Response, next: Function): Promise<any> {
    try {
        let wl = new wlSimulate(req._sails);
        let query = query2Sails(req.body.tableName, req._sails, req.body);
        if (query == false) {
            res.serverError("查询参数有误,请检查字段名称是否正确");
            return;
        }
        if (query.select && query.omit) {
            res.serverError("查询参数里面不能同时出现select和omit");
            return;
        }
        let result = await wl.findOne(req.body.tableName, query);
        res.amis(result);
    } catch (error: any) {
        if (!error.needThrow) {
            res.amis(error, -1, error.message);
        } else res.serverError(error);
    }
}
/**
 * 查询
 * @param req 
 * @param res 
 * @param next 
 */
export async function find(req: Api.SailsRequest, res: Api.Response, next: Function): Promise<any> {
    try {
        let wl = new wlSimulate(req._sails);

        let query = query2Sails(req.body.tableName, req._sails, req.body);

        if (query == false) {
            res.serverError("查询参数有误,请检查字段名称是否正确");
            return;
        }
        if (query.select && query.omit) {
            res.serverError("查询参数里面不能同时出现select和omit");
            return;
        }
        let result = await wl.find(req.body.tableName, query);
        res.amis(result);
    } catch (error: any) {
        if (!error.needThrow) {
            res.amis(error, -1, error.message);
        } else res.serverError(error);
    }
}
/**
 * 求平均
 * @param req 
 * @param res 
 * @param next 
 */
export async function avg(req: Api.SailsRequest, res: Api.Response, next: Function): Promise<any> {
    /**
    * 前端post示例
    {
        "tableName":"user",
        "where":
        {
            id: { '>=': 1 }
        },
        "avgField":"age"
    }
    */
    try {
        let wl = new wlSimulate(req._sails);
        let query = query2Sails(req.body.tableName, req._sails, req.body.where);
        let avg = await wl.Statistical(req.body.tableName, StatisMethods.avg, query, req.body.avgField);
        res.amis(avg);
    } catch (error: any) {
        if (!error.needThrow) {
            res.amis(error, -1, error.message);
        } else res.serverError(error);
    }

}
/**
 * 求和
 * @param req 
 * @param res 
 * @param next 
 */
export async function sum(req: Api.SailsRequest, res: Api.Response, next: Function): Promise<any> {
    /**
    * 前端post示例
    {
        "tableName":"user",
        "where":
        {
            id: { '<=': 10 }
        },
        "sumField":"age"
    }    
        let count = await wl.Statistical(req.body.tableName, StatisMethods.count, { id: { '>=': 1000000 } });
    */
    try {
        let wl = new wlSimulate(req._sails);
        let query = query2Sails(req.body.tableName, req._sails, req.body.where);
        let sum = await wl.Statistical(req.body.tableName, StatisMethods.sum, query, req.body.sumField);
        res.amis(sum);
    } catch (error: any) {
        if (!error.needThrow) {
            res.amis(error, -1, error.message);
        } else res.serverError(error);
    }

}
/**
 * 计数
 * @param req 
 * @param res 
 * @param next 
 */
export async function count(req: Api.SailsRequest, res: Api.Response, next: Function): Promise<any> {
    /**
    * 前端post示例
    {
        "tableName":"user",
        "where":
        {
            id: { '<=': 10 }
        }
    }   
    */
    try {
        let wl = new wlSimulate(req._sails);
        let query = query2Sails(req.body.tableName, req._sails, req.body.where);
        let count = await wl.Statistical(req.body.tableName, StatisMethods.count, query, req.body.sumField);
        res.amis(count);
    } catch (error: any) {
        if (!error.needThrow) {
            res.amis(error, -1, error.message);
        } else res.serverError(error);
    }

}
/**
 * 修改单条记录
 * @param req 
 * @param res 
 * @param next 
 */
export async function updateOne(req: Api.SailsRequest, res: Api.Response, next: Function): Promise<any> {
    try {
        let wl = new wlSimulate(req._sails);
        if (!req.body.where) {
            res.status(500).send('缺少where');
            return;
        } else if (!req.body.oldVer && req.body.oldVer != 0) {
            res.status(500).send('缺少oldVer');
            return;
        } else if (!req.body.valuesToSet) {
            res.status(500).send('缺少valuesToSet');
            return;
        }
        let result = await wl.updateOne(req.body.tableName, req.body.oldVer, req.body.where, req.body.valuesToSet);
        res.amis(result);
    } catch (error: any) {
        if (!error.needThrow) {
            res.amis(error, -1, error.message);
        } else res.serverError(error);
    }
}
/**
 * 检查一系列记录版本号如果成功则按条件修改
 * @param req 
 * @param res 
 * @param next 
 */
export async function checkThenUpdate(req: Api.SailsRequest, res: Api.Response, next: Function): Promise<any> {
    try {
        let wl = new wlSimulate(req._sails);
        if (!req.body.oldDatas) {
            res.status(500).send('缺少oldDatas');
            return;
        } else if (!req.body.valuesToSet) {
            res.status(500).send('缺少valuesToSet');
            return;
        }
        let result = await wl.checkThenUpdate(req.body.tableName, req.body.oldDatas, req.body.valuesToSet);
        res.amis(result);
    } catch (error: any) {
        if (!error.needThrow) {
            res.amis(error, -1, error.message);
        } else res.serverError(error);
    }
}
/**
 * 删除
 * @param req 
 * @param res 
 * @param next 
 */
export async function del(req: Api.SailsRequest, res: Api.Response, next: Function): Promise<any> {
    let wl = new wlSimulate(req._sails);
    let query = query2Sails(req.body.tableName, req._sails, req.body);

    if (query == false) {
        res.serverError("查询参数有误,请检查字段名称是否正确");
        return;
    } else if (query.select && query.omit) {
        res.serverError("查询参数里面不能同时出现select和omit");
        return;
    } else {
        try {
            let result = await wl.destroy(req.body.tableName, query);
            if (typeof result == "string") {
                return res.amis({ text: '没有可删除的数据' }, -1, "");
            } else {
                result.status = 0;
                result.msg = '删除成功';
            }
            res.amis(result);
        }
        catch (error: any) {
            if (!error.needThrow) {
                res.amis(error, -1, error.message);
            } else res.serverError(error);
        }
    }
}
//#endregion


/**
 * 把token解析成当前用户,这个在api/policies/authentiated.ts里面已经做了,这里就直接返回就可以了
 * @param req 
 * @param res 
 * @returns 
 */
export async function currentUser(req: Api.SailsRequest, res: Api.Response): Promise<any> {
    return res.amis(req.user);
}

这个控制器,一共有9个action (9个对外函数):

/api/gereral/create
/api/gereral/findone
/api/gereral/find
/api/gereral/avg
/api/gereral/sum
/api/gereral/count
/api/gereral/del
/api/gereral/updateOne
/api/gereral/checkThenUpdate

这几个控制器将会是未来前端使用比较多的,一个好的软件项目,应该要有完善的文档,我们用Swagger,必须清楚的描述所有这几个控制器的使用说明,调用的Request body示例,以及其他注意事项,因此我们需要新建一个Swagger

完善Swagger

在swagger文件夹里面添加GeneralSwagger.ts,关于swagger以前的文章里面有讲,这个地方我们重点再讲一下description 和properties部分

description

description是对api的详细描述,该详描对应位置如下图:
十一、通用控制器,Response中间件及错误日志_第2张图片

description 支持Markdown格式

为了更好的描述,我们使用Markdown格式书写详描,其中

  • 用``(esc按键下面的标点符号)把需要描述的内容包含起来,这个比单引号好用。
  • \n表示换行,增加\n的行渲染出来的html代码里面会多出一个

  • ``` 对包裹起来的内容会当做代码处理,渲染出来会产生
  • 在详描里面要添加``` 应该用转义符,比如\`\`\`
  • 3个- 符号会渲染成横线

更多Markdown 语法 可查阅:https://markdown.com.cn/

详描示例:

  ### 更新满足给定查询条件的记录
  ### Request body包含四个部分:
  - tableName:要更新的表的名称\n
  - where:查询条件,满足Waterline条件要求并做简单封装(见后面说明)
  - valuesToSet:要更新的字段名称和值组成的键值对\n
  - oldVer:要更新的数据的未更新之前的版本号
  ### Request body示例:   
  \r
  \`\`\`
  {
    "tableName":"user",      
    "where":{"id":1},    
    "valuesToSet":{
      "email":"updateTest",
      "age":38
    },
    "oldVer":0    
  }
  \`\`\`  
  ### 以上把user表里面id值为1 version版本号为0的记录的age更新为38  email更新为:updateTest
  
  ---\r\n
  ### 为了让前端操作更加简便,后端对Waterline查询条件做了一些简化处理:
  - 后端只解析plain object (如果是包含二级对象的复杂对象,不做解析,比如"id": {">": 30,"<": 100},直接post到Waterline) \n    
  - 每一组key:value键值代表一个查询。\n
    + 比如 {"age":18}代表要查询的条件是age=18\n
  - 后端根据key对应属性,如果是字符型,默认为contains操作,即包含给定值的模糊查询\n
    + 比如 {"name":"cai"} 表示要查询的是name里面包含'cai'的数据name like '%cai%' \n
  - 多组键值对默认都是与(and)的关系\n
    + 比如 {"name":"cai","age":18} 相当于name like '%cai%' and age=18  \n
  - 前端的value里面如果出现$,那么$前面是操作符后面是值\n
    + 比如{"id":">$3"},表示要查询id大于3 \n
  - 如果字符型需要精确查找,应该多一个'=='操作符\n
    + 比如 {"name":"==$cai"} 相当于name = 'cai'\n
  - 操作符只支持:'!=' | '<' | '<=' | '>' | '>=' | 'nin' | 'in' | 'contains' | 'startsWith' | 'endsWith' |'=='; \n
  - 多个键值对如果是or关系,需要写成数组,用"or"作为key\n
    + 比如 {"name":"cai","or":[{"age":18},{"name":"li"}]} 相当于(name like '%cai%') and (age=18 or name like '%li%')\n    
  ---\r\n
  

properties

requestBody 是swagger里面一个很重要的内容,这块内容描述了前端再post的时候需要在body里面携带的内容,包含Schema描述和示例数据,示例如下:

tableName: { type: 'string', required: true, description: '要查询的表名称', example: 'user' }

渲染出来的内容如下图所示:
十一、通用控制器,Response中间件及错误日志_第3张图片

完整GeneralSwagger.ts

import { SwaggerActionAttribute } from "sails-hook-swagger-generator/lib/interfaces"
const Tag1 = "通用控制器";
/**
 * 新增数据
 */
const generalCreate: SwaggerActionAttribute = {
  summary: '新增数据',
  description: `### 在知道数据表中新增一条记录
  ### Request body 包含三个部分内容:
  - tableName:要添加数据的表的名称\n
  - 表字段和值组成键值对,比如:"email":"[email protected]"\n
  - fetch:如果fetch参数为true 则返回当前插入的记录,如果是false 则返回当前插入数据的自增长id值
  ### Request body示例: 
  #### 假设user表里面有email,password,nickname,age等四个字段,可以通过如下示例添加一条用户记录 
  \r
  \`\`\`
  {
    "tableName":"user",
    "email":"[email protected]",
    "nickname":"nickName test",
    "age":22,
    "password":"123456",
    "fetch":true
  }
  \`\`\` 
  ### 注意,新增记录里面不要写id和id值,id值让数据库里面自动填写比较不会出错,除非是要做数据迁移 
  `,
  tags: [Tag1],
  requestBody: {
    content: {
      'application/json': {
        schema: {
          properties: {
            tableName: { type: 'string', required: true, example: 'user' },
            fetch: { type: 'boolean', request: true, example: true }
          }
        }
      }
    }
  },
  responses: {
    '400': { description: '验证错误' },
  },
  externalDocs: false,
};
/**
 * 返回满足条件的特定数据(一条记录)
 */
const generalFindOne: SwaggerActionAttribute = {
  summary: '特定记录查询',
  description: `### 返回满足条件的一条记录,如果满足条件有多条记录则返回第一条
  ### Request body包含四个部分:
  - tableName:要查询的表的名称\n
  - 表字段和值组成键值对为查询条件,比如:"email":"[email protected]",但是不能添加表里面没有的字段名称\n  
  - select:要返回的字段,数组类型,可空,默认为表中全部字段(敏感字段比如password自动剔除)
  - omit:要剔除的字段,数组类型,可空。
  #### 注意:select和omit不能同时出现,也就是说这两部分只能出现一个
  ### Request body示例: 
  #### 假设要根据user表里面id字段查询,可以通过如下示例实现(不同字段之间是and关系,具体查询条件可参考Waterline文档)
  \r
  \`\`\`
  {
    "tableName":"user",
    "id":2
  }
  \`\`\`  
  ### 以上查询user表里面id值为2的数据
  ### findOne不允许查询条件为空
  ---\r\n
  ### 为了让前端操作更加简便,后端对Waterline查询条件做了一些简化处理:
  - 后端只解析plain object (如果是包含二级对象的复杂对象,不做解析,比如"id": {">": 30,"<": 100},直接post到Waterline ) \n  
  - 参数筛选:不能添加表里面没有的字段名称,比如user表里面没有title这个字段,如果增加了这个字段,就会出错。
  而其他非表里面的字段名称的查询参数只能是page,pageSize,sort,select,omit 这几个关键词。\n  
  - 每一组key:value键值代表一个查询。\n
    + 比如 {"age":18}代表要查询的条件是age=18\n
  - 后端根据key对应属性,如果是字符型,默认为contains操作,即包含给定值的模糊查询\n
    + 比如 {"name":"cai"} 表示要查询的是name里面包含'cai'的数据name like '%cai%' \n
  - 多组键值对默认都是与(and)的关系\n
    + 比如 {"name":"cai","age":18} 相当于name like '%cai%' and age=18  \n
  - 前端的value里面如果出现$,那么$前面是操作符后面是值\n
    + 比如{"id":">$3"},表示要查询id大于3 \n
  - 如果字符型需要精确查找,应该多一个'=='操作符\n
    + 比如 {"name":"==$cai"} 相当于name = 'cai'\n
  - 操作符只支持:'!=' | '<' | '<=' | '>' | '>=' | 'nin' | 'in' | 'contains' | 'startsWith' | 'endsWith' |'=='; \n
  - 多个键值对如果是or关系,需要写成数组,用"or"作为key\n
    + 比如 {"name":"cai","or":[{"age":18},{"name":"li"}]} 相当于(name like '%cai%') and (age=18 or name like '%li%')\n    
  ---\r\n
  `,
  tags: [Tag1],
  requestBody: {
    content: {
      'application/json': {
        schema: {
          properties: {
            tableName: { type: 'string', required: true, example: 'user' },
            select: { type: 'Array', nullable: true, required: false, description: '要查询的字段,默认全部字段', example: '["id","email"]' },
            omit: { type: 'Array', nullable: true, required: false, description: '要剔除的字段', example: '["age"]' },
          }
        }
      }
    }
  },
  externalDocs: { description: 'Waterline 查询条件文档', url: "https://sailsjs.com/documentation/concepts/models-and-orm/query-language" },
  security: [{ APIKeyHeader: [] }],  //注意,此处APIKeyHeader属性名称是大小写敏感的
}
/**
 * 返回满足条件的所有数据支持分页查询和排序
 */
const generalFind: SwaggerActionAttribute = {
  summary: '数据查询',
  description: `### 返回满足条件的记录,支持分页查询,如果没有提交分页参数,默认页码为1,每页记录数为20条
  ### Request body包含七个部分:
  - tableName:要查询的表的名称\n
  - 表字段和值组成键值对为查询条件,可空,如果空白则查询所以记录,如果非空,参考findOne控制器
  - page:要查询的页码,可空,默认值为1
  - pageSize:每页返回记录数量,可空,默认值为20  
  - sort:排序,语法同MariaDB 比如 id desc 表示按照id降序排列
  - select:要返回的字段,数组类型,可空,默认为表中全部字段(敏感字段比如password自动剔除)
  - omit:要剔除的字段,数组类型,可空。
  #### 注意:select和omit不能同时出现,也就是说这两部分只能出现一个
  ### Request body示例: 
  #### 假设要查询user表里面id大于2的数据,并且查询结果不要age字段,可以通过如下示例实现 
  \r
  \`\`\`
  {
    "tableName":"user",
    "id":{">":2},
    "page":1,
    "pageSize":5,
    "sort":"id desc",
    "omit":["age"]
  }
  \`\`\`  
  ### 以上查询user表里面id值大于20的数据,查询结果按照id降序排列
  ### find查询条件为空表示查询所有数据
  ---\r\n
  ### 为了让前端操作更加简便,后端对Waterline查询条件做了一些简化处理:
  - 后端只解析plain object (如果是包含二级对象的复杂对象,不做解析,比如"id": {">": 30,"<": 100},直接post到Waterline) \n    
  - 参数筛选:不能添加表里面没有的字段名称,比如user表里面没有title这个字段,如果增加了这个字段,就会出错。
  而其他非表里面的字段名称的查询参数只能是page,pageSize,sort,select,omit 这几个关键词。\n
  - 每一组key:value键值代表一个查询。\n
    + 比如 {"age":18}代表要查询的条件是age=18\n
  - 后端根据key对应属性,如果是字符型,默认为contains操作,即包含给定值的模糊查询\n
    + 比如 {"name":"cai"} 表示要查询的是name里面包含'cai'的数据name like '%cai%' \n
  - 多组键值对默认都是与(and)的关系\n
    + 比如 {"name":"cai","age":18} 相当于name like '%cai%' and age=18  \n
  - 前端的value里面如果出现$,那么$前面是操作符后面是值\n
    + 比如{"id":">$3"},表示要查询id大于3 \n
  - 如果字符型需要精确查找,应该多一个'=='操作符\n
    + 比如 {"name":"==$cai"} 相当于name = 'cai'\n
  - 操作符只支持:'!=' | '<' | '<=' | '>' | '>=' | 'nin' | 'in' | 'contains' | 'startsWith' | 'endsWith' |'=='; \n
  - 多个键值对如果是or关系,需要写成数组,用"or"作为key\n
    + 比如 {"name":"cai","or":[{"age":18},{"name":"li"}]} 相当于(name like '%cai%') and (age=18 or name like '%li%')\n    
  ---\r\n
  `,
  tags: [Tag1],
  requestBody: {
    content: {
      'application/json': {
        schema: {
          properties: {
            tableName: { type: 'string', required: true, description: '要查询的表名称', example: 'user' },
            page: { type: 'number', required: false, description: '当前页码', example: 1 },
            pageSize: { type: 'number', required: false, description: '每页记录数量', example: 15 },
            sort: { type: 'string', required: false, description: '排序字段和排序方向', example: 'id desc' },
            select: { type: 'Array', nullable: true, required: false, description: '要查询的字段,默认全部字段', example: '["id","email"]' },
            omit: { type: 'Array', nullable: true, required: false, description: '要剔除的字段', example: '["age"]' },
          }
        }
      }
    }
  },
  externalDocs: { description: 'Waterline 查询条件文档', url: "https://sailsjs.com/documentation/concepts/models-and-orm/query-language" },
  security: [{ APIKeyHeader: [] }],  //注意,此处APIKeyHeader属性名称是大小写敏感的
}
/**
 * 请平均
 */
const generalAvg: SwaggerActionAttribute = {
  summary: '求平均',
  description: `### 返回满足条件的记录中指定字段的平均值
  ### Request body包含三个部分:
  - tableName:要求平均的表的名称\n
  - where:查询条件,满足Waterline条件要求并做简单封装(见后面说明)
  - avgField:要计算平均值的字段  
  ### Request body示例:  
  \r
  \`\`\`
  {
      "tableName":"user",
      "where":
      {
          "id": { ">": 5 }
      },
      "avgField":"age"  
  }
  \`\`\`  
  ### 以上查询user表里面id值大于5的数据中age(年龄)的平均值
  
  ---\r\n
  ### 为了让前端操作更加简便,where的查询条件除了符合Waterline规定外,后端对Waterline查询条件做了一些简化处理:
  - 后端只解析plain object (如果是包含二级对象的复杂对象,不做解析,比如"id": {">": 30,"<": 100},直接post到Waterline) \n
  - 参数筛选:不能添加表里面没有的字段名称,比如user表里面没有title这个字段,如果增加了这个字段,就会出错。
  而其他非表里面的字段名称的查询参数只能是page,pageSize,sort,select,omit 这几个关键词。\n    
  - 每一组key:value键值代表一个查询。\n
    + 比如 {"age":18}代表要查询的条件是age=18\n
  - 后端根据key对应属性,如果是字符型,默认为contains操作,即包含给定值的模糊查询\n
    + 比如 {"name":"cai"} 表示要查询的是name里面包含'cai'的数据name like '%cai%' \n
  - 多组键值对默认都是与(and)的关系\n
    + 比如 {"name":"cai","age":18} 相当于name like '%cai%' and age=18  \n
  - 前端的value里面如果出现$,那么$前面是操作符后面是值\n
    + 比如{"id":">$3"},表示要查询id大于3 \n
  - 如果字符型需要精确查找,应该多一个'=='操作符\n
    + 比如 {"name":"==$cai"} 相当于name = 'cai'\n
  - 操作符只支持:'!=' | '<' | '<=' | '>' | '>=' | 'nin' | 'in' | 'contains' | 'startsWith' | 'endsWith' |'=='; \n
  - 多个键值对如果是or关系,需要写成数组,用"or"作为key\n
    + 比如 {"name":"cai","or":[{"age":18},{"name":"li"}]} 相当于(name like '%cai%') and (age=18 or name like '%li%')\n    
  ---\r\n
  `,
  tags: [Tag1],
  requestBody: {
    content: {
      'application/json': {
        schema: {
          properties: {
            tableName: { type: 'string', required: true, description: '要查询的表名称', example: 'user' },
            where: { type: 'object', required: false, description: '查询条件', example: { "id": ">$5" } },
            avgField: { type: 'string', required: false, description: '要计算平均值的字段名称', example: "age" },
          }
        }
      }
    }
  },
  externalDocs: { description: 'Waterline 查询条件文档', url: "https://sailsjs.com/documentation/concepts/models-and-orm/query-language" },
  security: [{ APIKeyHeader: [] }],  //注意,此处APIKeyHeader属性名称是大小写敏感的
}
/**
 * 请和
 */
const generalSum: SwaggerActionAttribute = {
  summary: '求和',
  description: `### 返回满足条件的记录中指定字段的总和
  ### Request body包含三个部分:
  - tableName:要求和的表的名称\n
  - where:查询条件,满足Waterline条件要求并做简单封装(见后面说明)
  - sumField:要求和的字段  
  ### Request body示例:   
  \r
  \`\`\`
  {
      "tableName":"user",
      "where":
      {
          "id": { ">": 5 }
      },
      "sumField":"age"  
  }
  \`\`\`  
  ### 以上查询user表里面id值大于5的数据中age(年龄)的总和
  
  ---\r\n
  ### 为了让前端操作更加简便,where的查询条件除了符合Waterline规定外,后端对Waterline查询条件做了一些简化处理:  
  - 后端只解析plain object (如果是包含二级对象的复杂对象,不做解析,比如"id": {">": 30,"<": 100},直接post到Waterline) \n    
  - 参数筛选:不能添加表里面没有的字段名称,比如user表里面没有title这个字段,如果增加了这个字段,就会出错。
  而其他非表里面的字段名称的查询参数只能是page,pageSize,sort,select,omit 这几个关键词。\n
  - 每一组key:value键值代表一个查询。\n
    + 比如 {"age":18}代表要查询的条件是age=18\n
  - 后端根据key对应属性,如果是字符型,默认为contains操作,即包含给定值的模糊查询\n
    + 比如 {"name":"cai"} 表示要查询的是name里面包含'cai'的数据name like '%cai%' \n
  - 多组键值对默认都是与(and)的关系\n
    + 比如 {"name":"cai","age":18} 相当于name like '%cai%' and age=18  \n
  - 前端的value里面如果出现$,那么$前面是操作符后面是值\n
    + 比如{"id":">$3"},表示要查询id大于3 \n
  - 如果字符型需要精确查找,应该多一个'=='操作符\n
    + 比如 {"name":"==$cai"} 相当于name = 'cai'\n
  - 操作符只支持:'!=' | '<' | '<=' | '>' | '>=' | 'nin' | 'in' | 'contains' | 'startsWith' | 'endsWith' |'=='; \n
  - 多个键值对如果是or关系,需要写成数组,用"or"作为key\n
    + 比如 {"name":"cai","or":[{"age":18},{"name":"li"}]} 相当于(name like '%cai%') and (age=18 or name like '%li%')\n    
  ---\r\n
  `,
  tags: [Tag1],
  requestBody: {
    content: {
      'application/json': {
        schema: {
          properties: {
            tableName: { type: 'string', required: true, description: '要查询的表名称', example: 'user' },
            where: { type: 'object', required: false, description: '查询条件', example: { "id": ">$5" } },
            sumField: { type: 'string', required: false, description: '要计算平均值的字段名称', example: "age" },
          }
        }
      }
    }
  },
  externalDocs: { description: 'Waterline 查询条件文档', url: "https://sailsjs.com/documentation/concepts/models-and-orm/query-language" },
  security: [{ APIKeyHeader: [] }],  //注意,此处APIKeyHeader属性名称是大小写敏感的
}
/**
 * 计数
 */
const generalCount: SwaggerActionAttribute = {
  summary: '计数',
  description: `### 返回满足条件的记录数
  ### Request body包含二个部分:
  - tableName:要计数的表的名称\n
  - where:查询条件,满足Waterline条件要求并做简单封装(见后面说明)  
  ### Request body示例:   
  \r
  \`\`\`
  {
      "tableName":"user",
      "where":
      {
          "id": { ">": 5 }
      } 
  }
  \`\`\`  
  ### 以上查询user表里面id值大于5的记录共有多少条
  
  ---\r\n
  ### 为了让前端操作更加简便,where的查询条件除了符合Waterline规定外,后端对Waterline查询条件做了一些简化处理:
  - 后端只解析plain object (如果是包含二级对象的复杂对象,不做解析,比如"id": {">": 30,"<": 100},直接post到Waterline) \n   
  - 参数筛选:不能添加表里面没有的字段名称,比如user表里面没有title这个字段,如果增加了这个字段,就会出错。
  而其他非表里面的字段名称的查询参数只能是page,pageSize,sort,select,omit 这几个关键词。\n 
  - 每一组key:value键值代表一个查询。\n
    + 比如 {"age":18}代表要查询的条件是age=18\n
  - 后端根据key对应属性,如果是字符型,默认为contains操作,即包含给定值的模糊查询\n
    + 比如 {"name":"cai"} 表示要查询的是name里面包含'cai'的数据name like '%cai%' \n
  - 多组键值对默认都是与(and)的关系\n
    + 比如 {"name":"cai","age":18} 相当于name like '%cai%' and age=18  \n
  - 前端的value里面如果出现$,那么$前面是操作符后面是值\n
    + 比如{"id":">$3"},表示要查询id大于3 \n
  - 如果字符型需要精确查找,应该多一个'=='操作符\n
    + 比如 {"name":"==$cai"} 相当于name = 'cai'\n
  - 操作符只支持:'!=' | '<' | '<=' | '>' | '>=' | 'nin' | 'in' | 'contains' | 'startsWith' | 'endsWith' |'=='; \n
  - 多个键值对如果是or关系,需要写成数组,用"or"作为key\n
    + 比如 {"name":"cai","or":[{"age":18},{"name":"li"}]} 相当于(name like '%cai%') and (age=18 or name like '%li%')\n   
  
  ---\r\n  
  `,
  tags: [Tag1],
  requestBody: {
    content: {
      'application/json': {
        schema: {
          properties: {
            tableName: { type: 'string', required: true, description: '要查询的表名称', example: 'user' },
            where: { type: 'object', required: false, description: '查询条件', example: { "id": ">$5" } }
          }
        }
      }
    }
  },
  externalDocs: { description: 'Waterline 查询条件文档', url: "https://sailsjs.com/documentation/concepts/models-and-orm/query-language" },
  security: [{ APIKeyHeader: [] }],  //注意,此处APIKeyHeader属性名称是大小写敏感的
}
/**
 * 根据条件删除
 */
const generalDel: SwaggerActionAttribute = {
  summary: '数据删除',
  description: `### 根据给定查询条件删除数据,并写入Archive(归档)表,查询条件和find参数一样
  ### Request body包含二个部分:
  - tableName:要删除的表的名称\n
  - 表字段和值组成键值对为查询条件,可空,如果空白则查询所以记录,如果非空,参考findOne控制器  
  ### Request body示例: 
  #### 假设要查询user表里面id等于2的数据
  \r
  \`\`\`
  {
    "tableName":"user",
    "id":2    
  }
  \`\`\`    
  ---\r\n
  ### 为了让前端操作更加简便,后端对Waterline查询条件做了一些简化处理:
  - 后端只解析plain object (如果是包含二级对象的复杂对象,不做解析,比如"id": {">": 30,"<": 100},直接post到Waterline) \n    
  - 参数筛选:不能添加表里面没有的字段名称,比如user表里面没有title这个字段,如果增加了这个字段,就会出错。
  而其他非表里面的字段名称的查询参数只能是page,pageSize,sort,select,omit 这几个关键词。\n
  - 每一组key:value键值代表一个查询。\n
    + 比如 {"age":18}代表要查询的条件是age=18\n
  - 后端根据key对应属性,如果是字符型,默认为contains操作,即包含给定值的模糊查询\n
    + 比如 {"name":"cai"} 表示要查询的是name里面包含'cai'的数据name like '%cai%' \n
  - 多组键值对默认都是与(and)的关系\n
    + 比如 {"name":"cai","age":18} 相当于name like '%cai%' and age=18  \n
  - 前端的value里面如果出现$,那么$前面是操作符后面是值\n
    + 比如{"id":">$3"},表示要查询id大于3 \n
  - 如果字符型需要精确查找,应该多一个'=='操作符\n
    + 比如 {"name":"==$cai"} 相当于name = 'cai'\n
  - 操作符只支持:'!=' | '<' | '<=' | '>' | '>=' | 'nin' | 'in' | 'contains' | 'startsWith' | 'endsWith' |'=='; \n
  - 多个键值对如果是or关系,需要写成数组,用"or"作为key\n
    + 比如 {"name":"cai","or":[{"age":18},{"name":"li"}]} 相当于(name like '%cai%') and (age=18 or name like '%li%')\n    
  ---\r\n
  `,
  tags: [Tag1],
  requestBody: {
    content: {
      'application/json': {
        schema: {
          properties: {
            tableName: { type: 'string', required: true, description: '要查询的表名称', example: 'user' }
          }
        }
      }
    }
  },
  externalDocs: { description: 'Waterline 查询条件文档', url: "https://sailsjs.com/documentation/concepts/models-and-orm/query-language" },
  security: [{ APIKeyHeader: [] }],  //注意,此处APIKeyHeader属性名称是大小写敏感的
}
/**
 * 更新多条记录
 */
const generalCheckThenUpdate: SwaggerActionAttribute = {
  summary: '更新多条记录',
  description: `### 根据给定的旧数据的id和版本号数组更新
  ### Request body包含三个部分:
  - tableName:要更新的表的名称\n
  - oldDatas:要更新的所有记录的id值和version版本号数组\n
  - valuesToSet:要更新的字段名称和值组成的键值对\n
  ### Request body示例:   
  \r
  \`\`\`
  {
    "tableName":"user",
    "oldDatas": [
      {"id":1,"version":0},
      {"id":2,"version":0}
    ],
    "valuesToSet": {
      "age": 58
    }
  }
  \`\`\`  
  ### 以上把user表里面id值分别为1和2 version版本号为分别0和0的记录的age更新为58  
  
  `,
  tags: [Tag1],
  requestBody: {
    content: {
      'application/json': {
        schema: {
          properties: {
            tableName: { type: 'string', required: true, description: '要查询的表名称', example: 'user' },
            oldDatas: { type: 'Array', required: false, description: '旧数据id和版本号数组', example: [{ "id": 1, "version": 0 }, { "id": 2, "version": 0 }] },
            valuesToSet: { type: 'object', required: false, description: '要更新的内容', example: { "age": 58 } }
          }
        }
      }
    }
  },
  externalDocs: { description: 'Waterline 查询条件文档', url: "https://sailsjs.com/documentation/concepts/models-and-orm/query-language" },
  security: [{ APIKeyHeader: [] }],  //注意,此处APIKeyHeader属性名称是大小写敏感的
}
/**
 * 更新一条记录
 */
const generalUpdateOne: SwaggerActionAttribute = {
  summary: '更新一条记录',
  description: `### 更新满足给定查询条件的记录
  ### Request body包含四个部分:
  - tableName:要更新的表的名称\n
  - where:查询条件,满足Waterline条件要求并做简单封装(见后面说明)
  - valuesToSet:要更新的字段名称和值组成的键值对\n
  - oldVer:要更新的数据的未更新之前的版本号
  ### Request body示例:   
  \r
  \`\`\`
  {
    "tableName":"user",      
    "where":{"id":1},    
    "valuesToSet":{
      "email":"updateTest",
      "age":38
    },
    "oldVer":0    
  }
  \`\`\`  
  ### 以上把user表里面id值为1 version版本号为0的记录的age更新为38  email更新为:updateTest
  
  ---\r\n
  ### 为了让前端操作更加简便,后端对Waterline查询条件做了一些简化处理:
  - 后端只解析plain object (如果是包含二级对象的复杂对象,不做解析,比如"id": {">": 30,"<": 100},直接post到Waterline) \n    
  - 每一组key:value键值代表一个查询。\n
    + 比如 {"age":18}代表要查询的条件是age=18\n
  - 后端根据key对应属性,如果是字符型,默认为contains操作,即包含给定值的模糊查询\n
    + 比如 {"name":"cai"} 表示要查询的是name里面包含'cai'的数据name like '%cai%' \n
  - 多组键值对默认都是与(and)的关系\n
    + 比如 {"name":"cai","age":18} 相当于name like '%cai%' and age=18  \n
  - 前端的value里面如果出现$,那么$前面是操作符后面是值\n
    + 比如{"id":">$3"},表示要查询id大于3 \n
  - 如果字符型需要精确查找,应该多一个'=='操作符\n
    + 比如 {"name":"==$cai"} 相当于name = 'cai'\n
  - 操作符只支持:'!=' | '<' | '<=' | '>' | '>=' | 'nin' | 'in' | 'contains' | 'startsWith' | 'endsWith' |'=='; \n
  - 多个键值对如果是or关系,需要写成数组,用"or"作为key\n
    + 比如 {"name":"cai","or":[{"age":18},{"name":"li"}]} 相当于(name like '%cai%') and (age=18 or name like '%li%')\n    
  ---\r\n
  `,
  tags: [Tag1],
  requestBody: {
    content: {
      'application/json': {
        schema: {
          properties: {
            tableName: { type: 'string', required: true, description: '要查询的表名称', example: 'user' },
            where: { type: 'object', required: false, description: '查询条件', example: { "id": 1 } },
            valuesToSet: { type: 'object', required: false, description: '要更新的内容', example: { "email": "updateTest", "age": 38 } },
            oldVer: { type: 'number', required: false, description: '未更新前版本号', example: 0 },
          }
        }
      }
    }
  },
  externalDocs: { description: 'Waterline 查询条件文档', url: "https://sailsjs.com/documentation/concepts/models-and-orm/query-language" },
  security: [{ APIKeyHeader: [] }],  //注意,此处APIKeyHeader属性名称是大小写敏感的
}
export { generalCreate, generalFindOne, generalFind, generalAvg, generalSum, generalCount, generalDel, generalCheckThenUpdate, generalUpdateOne };

externalDocs 可以做为扩展阅读使用,用超级链接把需要了解更多的前端用户引导到相关文档

修改config/routes.js 实现控制器和swagger关联

/**
 * Route Mappings
 * (sails.config.routes)
 *
 * Your routes tell Sails what to do each time it receives a request.
 *
 * For more information on configuring custom routes, check out:
 * https://sailsjs.com/anatomy/config/routes-js
 */
import { userCreate, userFind, userFindOne, userLogin, statistics,userCurrentUser } from 'swagger/UserSwagger';
import { generalCreate, generalFindOne, generalFind, generalAvg, generalSum, generalCount, generalDel, generalCheckThenUpdate, generalUpdateOne } from 'swagger/GeneralSwagger'
module.exports.routes = {
  /***************************************************************************
  *                                                                          *
  * Make the view located at `views/homepage.ejs` your home page.            *
  *                                                                          *
  * (Alternatively, remove this and add an `index.html` file in your         *
  * `assets` directory)                                                      *
  *                                                                          *
  ***************************************************************************/

  '/': { view: 'pages/homepage' },
  // 'POST /api/login/account': { action: 'users/check' },
  // 'GET /api/currentUser': { action: 'users/curUser' },
  'GET /api/notices': { action: 'users/notices' },
  // 'POST /api/login/outLogin': { action: 'users/logout' },
  // 'GET /api/crudDemo': { action: 'Amis/crudDemo' },
  // 'post /api/crudnew': { action: 'Amis/crudNew' },
  // 'delete /api/crudDelete': { action: 'Amis/crudDelete' },
  'POST /api/userDel': {
    action: 'User/del'
  },
  'POST /api/userCreate': {
    swagger: userCreate,
    action: 'User/create'
  },
  'POST /api/user/find': {
    action: 'USer/find', swagger: userFind
  },
  'POST /api/user/findOne': {
    action: 'USer/findOne', swagger: userFindOne
  },
  'POST /api/user/updateOne': {
    action: 'USer/updateOne',
  },
  'POST /api/user/checkThenUpdate': {
    action: 'USer/checkThenUpdate',
  },
  'POST /api/user/login': {
    action: 'USer/login',
    swagger: userLogin
  },
  'POST /api/user/currentUser': {
    action: 'USer/currentUser',
    swagger:userCurrentUser
  },
  'POST /api/user/statistical': {
    action: 'USer/statistical',
    swagger: statistics
  },
  //#region 通用控制器
  'POST /api/gereral/create': {
    action: 'General/Create',
    swagger: generalCreate
  },
  'POST /api/gereral/findone': {
    action: 'General/findOne',
    swagger: generalFindOne
  },
  'POST /api/gereral/find': {
    action: 'General/find',
    swagger: generalFind
  },
  'POST /api/gereral/avg': {
    action: 'General/avg',
    swagger: generalAvg
  },
  'POST /api/gereral/sum': {
    action: 'General/sum',
    swagger: generalSum
  },
  'POST /api/gereral/count': {
    action: 'General/count',
    swagger: generalCount
  },
  'POST /api/gereral/del': {
    action: 'General/del',
    swagger: generalDel
  },
  'POST /api/gereral/updateOne': {
    action: 'General/updateOne',
    swagger: generalUpdateOne
  },
  'POST /api/gereral/checkThenUpdate': {
    action: 'General/checkThenUpdate',
    swagger: generalCheckThenUpdate
  },

  //#endregion

  /***************************************************************************
  *                                                                          *
  * More custom routes here...                                               *
  * (See https://sailsjs.com/config/routes for examples.)                    *
  *                                                                          *
  * If a request to a URL doesn't match any of the routes in this file, it   *
  * is matched against "shadow routes" (e.g. blueprint routes).  If it does  *
  * not match any of those, it is matched against static assets.             *
  *                                                                          *
  ***************************************************************************/


};

修改routes.js之后,保存并且重启sails,同时在apiDoc界面上面按Ctrl+F5 强制重启,可以看到新的swagger内容

修改swaggergenerator.js

走到这里,应该可以看到http://localhost:1898/apidoc/比较完整的api文档了。还需要做一点小的删除:swagger生成了许多我们不需要的内容,包括整个项目所有models的schema 这块我们需要做到descript里面。还有一些配合前端的测试api,日后是要删除的,user控制器里面也有一些日后不需要的内容(部分user控制器可以使用通用控制器)。
因此需要调整config/swaggergenerator.js修改includeRoute代码如下:

 includeRoute: (routeInfo) => {
    if (routeInfo.path == '/api/user/login') return true;
    if (routeInfo.path == '/api/user/currentUser') return true;
    if (!routeInfo.path.startsWith('/api/gereral')) return false;  
    return true;
  },

修改postProcess代码如下,增加一个对:specifications.components.schemas的删除(不需要显示)

let sch = specifications.components;
    //在Top-Level Swagger Defintions中添加身份认证项Authorize
    sch.securitySchemes = {
      APIKeyHeader: {
        type: "apiKey",
        in: "header",
        name: "Authorization",
        description: "token前面需要有Bearer前缀,例如:Bearer xxxx"
      }
    }    
    // 因为不需要引用schema,所以可以删除掉这个:
    delete specifications.components.schemas;    
    //在Top-Level Swagger 中添加安全选项,如果在顶级属性中添加安全现,则每一个path上都可以执行Authorize
    //如果此处没有添加,可以在每个action中的swagger里面添加security,有添加的path才能进行身份认证
    //大小写敏感
    specifications.security = [
      {
        APIKeyHeader: []
      }
    ];

  },
  excludeDeprecatedPutBlueprintRoutes: false,

至此,swagger 界面如下:
十一、通用控制器,Response中间件及错误日志_第4张图片

十一、通用控制器,Response中间件及错误日志_第5张图片

Swagger 测试

有了完善的swagger,前端程序员可以配合postman实现对api更加清晰的认识。swagger 里面有测试的示例数据,如果有需要,也可以直接在swagger里面进行测试,具体做法前面文章有。需要再提一下的就是JWT认证问题,swagger可以通过Authorize按钮输入请求头的token,需要在swagger进行直接测试的,请先用user/login控制器获取token(这个也可以在swagger里面获取)

如果源代码里面对swagger有修改,重启sails会重新生成新的swagger文档,但是在浏览器界面有可能会看不到最新版,这个是浏览器缓存的原因,解决办法是在浏览器端按"Ctrl+F5" 进行强制刷新。

强烈建议前端开发人员测试swagger的提交(try it out),如果提交有问题,可以反馈给后端人员好进行调整,或者是测试过程不清楚,说明文档描述还可以再改。这个测试过程是完善整个项目非常重要的一个环节。

你可能感兴趣的:(Sailsjs从零开始,中间件,数据库)