From 3b7c59dfed1cfd1fb248a06ff1ccccf2a40028a3 Mon Sep 17 00:00:00 2001 From: luowenwei Date: Mon, 2 May 2022 20:20:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(build):=E5=9F=BA=E4=BA=8Eexpress=E7=BA=AF?= =?UTF-8?q?=E6=89=8B=E5=B7=A5=E6=9E=84=E5=BB=BA=E6=95=B4=E4=BD=93=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=A1=86=E6=9E=B6=EF=BC=88=E6=89=8B=E5=86=99egg.js?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E3=80=82=E4=B9=9F=E5=8F=AF=E4=BB=8Enode.js?= =?UTF-8?q?=20http=E6=A8=A1=E5=9D=97=E4=B8=BA=E5=9F=BA=E7=A1=80=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E6=9E=84=E5=BB=BAegg.js=E6=A1=86=E6=9E=B6=EF=BC=89?= =?UTF-8?q?=E3=80=82=E5=AE=8C=E6=88=90restful=E8=A7=84=E8=8C=83player?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + app/.env | 9 +++ app/.test.env | 8 +++ app/app.ts | 19 ++++++ app/base/controller.ts | 79 ++++++++++++++++++++++ app/base/server.ts | 9 +++ app/cli/controller.ts.hbs | 27 ++++++++ app/cli/route-config.hbs | 5 ++ app/cli/service.ts.hbs | 6 ++ app/controller/api/player.ts | 51 ++++++++++++++ app/controller/home.ts | 3 + app/middleware/authorized.ts | 3 + app/model/index.ts | 48 +++++++++++++ app/model/mysql/Player.ts | 33 +++++++++ app/package.json | 35 ++++++++-- app/plopfile.js | 62 +++++++++++++++++ app/router/config.ts | 19 ++++++ app/router/index.ts | 38 +++++++++++ app/server.js | 11 --- app/service/index.ts | 12 ++++ app/service/player.ts | 40 +++++++++++ app/test/common/http.ts | 94 ++++++++++++++++++++++++++ app/test/common/request.ts | 29 ++++++++ app/test/controller/api/player.test.ts | 92 +++++++++++++++++++++++++ app/test/controller/home.test.ts | 3 + app/tools/pureFunc.ts | 18 +++++ app/tsconfig.json | 69 +++++++++++++++++++ 27 files changed, 807 insertions(+), 16 deletions(-) create mode 100644 app/.env create mode 100644 app/.test.env create mode 100644 app/app.ts create mode 100644 app/base/controller.ts create mode 100644 app/base/server.ts create mode 100644 app/cli/controller.ts.hbs create mode 100644 app/cli/route-config.hbs create mode 100644 app/cli/service.ts.hbs create mode 100644 app/controller/api/player.ts create mode 100644 app/controller/home.ts create mode 100644 app/middleware/authorized.ts create mode 100644 app/model/index.ts create mode 100644 app/model/mysql/Player.ts create mode 100644 app/plopfile.js create mode 100644 app/router/config.ts create mode 100644 app/router/index.ts delete mode 100755 app/server.js create mode 100644 app/service/index.ts create mode 100644 app/service/player.ts create mode 100644 app/test/common/http.ts create mode 100644 app/test/common/request.ts create mode 100644 app/test/controller/api/player.test.ts create mode 100644 app/test/controller/home.test.ts create mode 100644 app/tools/pureFunc.ts create mode 100644 app/tsconfig.json diff --git a/.gitignore b/.gitignore index 3b9771e..ec2b34c 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ bin/Release # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git **/node_modules +dist #Webstorm metadata .idea diff --git a/app/.env b/app/.env new file mode 100644 index 0000000..bd8457d --- /dev/null +++ b/app/.env @@ -0,0 +1,9 @@ +NODE_ENV=development +SERVER_PORT=3000 + +MYSQL_HOST=127.0.0.1 +MYSQL_PORT=3306 +MYSQL_USER=root +MYSQL_PASSWORD=your_password +MYSQL_DATABASE=player +PRINT_MYSQL_SQL=1 diff --git a/app/.test.env b/app/.test.env new file mode 100644 index 0000000..155f834 --- /dev/null +++ b/app/.test.env @@ -0,0 +1,8 @@ +NODE_ENV=unittest +SERVER_PORT=3000 + +MYSQL_HOST=127.0.0.1 +MYSQL_PORT=3306 +MYSQL_USER=root +MYSQL_PASSWORD=your_password +MYSQL_DATABASE=player_unittest diff --git a/app/app.ts b/app/app.ts new file mode 100644 index 0000000..84f006d --- /dev/null +++ b/app/app.ts @@ -0,0 +1,19 @@ + + +import express from 'express'; +import bodyParser from 'body-parser'; +import initRouters from './router'; +import initORM from './model'; + +const app = express(); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); +initORM(); +initRouters(app); + + +app.listen(process.env.SERVER_PORT, () => { + console.log("Server is listening on port 3000"); +}); + +export { app }; diff --git a/app/base/controller.ts b/app/base/controller.ts new file mode 100644 index 0000000..b32de95 --- /dev/null +++ b/app/base/controller.ts @@ -0,0 +1,79 @@ + + +import { Request, Response } from 'express'; +import { join } from 'path'; +import { readdirSync } from 'fs'; +import { camelCase, upperFirst } from 'lodash'; +import { CustomSequelize, sequelize } from '../model'; +import IRegisterService from '../service'; + + +export interface IContext { + request: Request; + response: Response; + model: CustomSequelize; + service: IRegisterService; +} + + +export default class BaseController { + public ctx: IContext; + + constructor(req: Request, res: Response) { + const context = { + request: req, + response: res, + model: sequelize.models as any, + }; + let service = {} as any; + const path = join(__dirname, '../service'); + const serviceFiles = readdirSync(path).filter(x => x !== 'index.js').filter(x => x !== 'index.ts'); + serviceFiles.map(fileName => { + const name = upperFirst(camelCase(fileName.split('.')[0] + 'Service')); + const cotr = require(join(path, fileName)).default; + service = { + ...service, + get [name]() { + return new cotr(context); + } + } + }); + + this.ctx = { + ...context, + service, + }; + } + + success(data: any, msg?: string) { + const body = { + errcode: 0, + msg: msg || 'success', + retcode: 0, + data, + }; + this.ctx.response.setHeader('Content-Type', 'application/json'); + this.ctx.response.send(body); + return; + } + + error( + errcode: number, + retcode: number, + msg: Error, + data?: any, + ) { + const body = { + errcode, msg, retcode, data, + }; + this.ctx.response.send(body); + } + + get request() { + return this.ctx.request; + } + + get response() { + return this.ctx.response; + } +} diff --git a/app/base/server.ts b/app/base/server.ts new file mode 100644 index 0000000..dd95955 --- /dev/null +++ b/app/base/server.ts @@ -0,0 +1,9 @@ + + +import { IContext } from './controller'; + +export default class BaseService { + constructor( + public ctx: Omit, + ) { } +} diff --git a/app/cli/controller.ts.hbs b/app/cli/controller.ts.hbs new file mode 100644 index 0000000..a372d94 --- /dev/null +++ b/app/cli/controller.ts.hbs @@ -0,0 +1,27 @@ + + +import BaseController from "../../base/controller"; + + +export default class {{upperFirst controller_name}}Controller extends BaseController { + // restful规范接口 + index() { + this.success('index'); + } + + show() { + this.success('show'); + } + + async create() { + this.success('create'); + } + + update() { + this.success('update'); + } + + destroy() { + this.success('destroy'); + } +} diff --git a/app/cli/route-config.hbs b/app/cli/route-config.hbs new file mode 100644 index 0000000..5293900 --- /dev/null +++ b/app/cli/route-config.hbs @@ -0,0 +1,5 @@ +// RESTFUL + '/api/{{controller_name}}': { + name: '{{upperFirst controller_name}}', + path: '/api/{{controller_name}}' + }, \ No newline at end of file diff --git a/app/cli/service.ts.hbs b/app/cli/service.ts.hbs new file mode 100644 index 0000000..7968cd4 --- /dev/null +++ b/app/cli/service.ts.hbs @@ -0,0 +1,6 @@ +import BaseService from "../base/server"; + + +export default class {{upperFirst service_name}}Service extends BaseService { + +} diff --git a/app/controller/api/player.ts b/app/controller/api/player.ts new file mode 100644 index 0000000..e3cbe0e --- /dev/null +++ b/app/controller/api/player.ts @@ -0,0 +1,51 @@ +/** + * 一个好习惯ajax的调用均使用protocol://hostname/api/** 等的url形式 + * 接口异常均可用 this.error 函数返回格式化数据到前端。本例子暂未使用 + */ +import BaseController from "../../base/controller"; + + +export default class PlayerController extends BaseController { + // restful规范接口 + async index() { + const { ctx } = this; + const result = await ctx.service.PlayerService.publicAll(); + this.success(result); + } + + async show() { + const { ctx } = this; + const { id } = ctx.request.params; + const result = await ctx.service.PlayerService.fetchOne(Number(id)); + this.success(result); + } + + async create() { + const { ctx } = this; + const { name, position } = ctx.request.body; + const result = await ctx.service.PlayerService.create({ name, position }); + this.success(result); + } + + async update() { + const { ctx } = this; + const { id } = ctx.request.params; + const { name, position } = ctx.request.body; + const result = await ctx.service.PlayerService.modify(Number(id), { name, position }); + this.success(result.length ? '修改成功' : '未知错误'); + } + + async destroy() { + const { ctx } = this; + const { id } = ctx.request.params; + const result = await ctx.service.PlayerService.delete(Number(id)); + this.success(result); + } + + /** + * 非restful规范接口. test + */ + async test() { + this.success('result test'); + } +} diff --git a/app/controller/home.ts b/app/controller/home.ts new file mode 100644 index 0000000..5c22db0 --- /dev/null +++ b/app/controller/home.ts @@ -0,0 +1,3 @@ +/** + * 可以处理非ajax之类的调用,如模板渲染等的前置加工。未处理 + */ \ No newline at end of file diff --git a/app/middleware/authorized.ts b/app/middleware/authorized.ts new file mode 100644 index 0000000..67aad4c --- /dev/null +++ b/app/middleware/authorized.ts @@ -0,0 +1,3 @@ +/** + * 中间件,可用于鉴权以及数据类型校验。时间关系暂不实现 + */ \ No newline at end of file diff --git a/app/model/index.ts b/app/model/index.ts new file mode 100644 index 0000000..2f1d710 --- /dev/null +++ b/app/model/index.ts @@ -0,0 +1,48 @@ + + +import { Model, Sequelize } from 'sequelize'; +import { Player } from './mysql/Player'; +import { join } from 'path'; +import { readdirSync } from 'fs'; + + + +/** + * 注册到框架上下文。key值请与文件名保持一致 + */ +export interface CustomSequelize { + Player: typeof Player; +} + + +export const sequelize = new Sequelize( + process.env.MYSQL_DATABASE!, + process.env.MYSQL_USER!, + process.env.MYSQL_PASSWORD!, + { + host: process.env.MYSQL_HOST!, + logging: false, + dialect: 'mysql', + } +); + +const initORM = async () => { + const modelObj: Record = {}; + try { + await sequelize.authenticate(); + console.log('Connection has been established successfully.'); + } catch (error) { + console.error('Unable to connect to the database:', error); + } + const path = join(__dirname, 'mysql'); + const modelFiles = readdirSync(path); + modelFiles.length && modelFiles.map(fileName => { + const key = fileName.split('.')[0]; + const model = require(join(path, fileName)).default; + modelObj[key] = model(); + }); + await sequelize.sync(); + return modelObj; +}; + +export default initORM; diff --git a/app/model/mysql/Player.ts b/app/model/mysql/Player.ts new file mode 100644 index 0000000..3938237 --- /dev/null +++ b/app/model/mysql/Player.ts @@ -0,0 +1,33 @@ + + +import { Model, INTEGER, STRING } from 'sequelize'; +import { sequelize } from '../index'; + + +export class Player extends Model { + declare id: number; + name: string; + position: 'C' | 'PF' | 'SF' | 'PG' | 'SG'; +} + +export default () => { + return Player.init({ + id: { + field: 'FPlayerId', + type: INTEGER, + primaryKey: true, + autoIncrement: true, + }, + name: { + field: 'FPlayerName', + type: STRING(20), + }, + position: { + field: 'FPlayerPosition', + type: STRING(2), + } + }, { + sequelize, + tableName: 'T_Player', + }); +} diff --git a/app/package.json b/app/package.json index 6b480b3..7f4bbb7 100755 --- a/app/package.json +++ b/app/package.json @@ -5,16 +5,36 @@ "description": "Building a RESTful CRUD API with Node.js, Express/Koa and MongoDB.", "main": "server.js", "scripts": { - "start": "NODE_ENV=development node server.js", + "start": "tsc && node -r dotenv/config ./dist/app.js", "start:prod": "NODE_ENV=production node server.js", - "test": "echo \"Error: no test specified\" && exit 1" + "create:service": "plop create:service", + "create:restful": "plop create:api-controller", + "test": "env-cmd -f ./.test.env --use-shell \"ts-mocha -p ./tsconfig.json test/**/*.test.ts --timeout 60000\"" }, "dependencies": { + "body-parser": "^1.20.0", + "dotenv": "^16.0.0", "express": "^4.17.1", - "mongoose": "^5.9.2" + "lodash": "^4.17.21", + "mongoose": "^5.9.2", + "mysql2": "^2.3.3", + "sequelize": "^6.19.0", + "typescript": "^4.6.4" }, "devDependencies": { - "chai": "^4.2.0" + "@types/chai": "^4.3.1", + "@types/express": "^4.17.13", + "@types/lodash": "^4.14.182", + "@types/mocha": "^9.1.1", + "@types/supertest": "^2.0.12", + "chai": "^4.2.0", + "env-cmd": "^10.1.0", + "mocha": "^10.0.0", + "plop": "^3.1.0", + "supertest": "^6.2.3", + "ts-mocha": "^9.0.2", + "ts-node": "^10.7.0", + "tsconfig-paths": "^3.14.1" }, "engines": { "node": ">=10.15.0" @@ -24,5 +44,10 @@ "not dead", "not ie <= 11", "not op_mini all" - ] + ], + "mocha": { + "require": [ + "tsconfig-paths/register" + ] + } } diff --git a/app/plopfile.js b/app/plopfile.js new file mode 100644 index 0000000..99e58da --- /dev/null +++ b/app/plopfile.js @@ -0,0 +1,62 @@ + + +const upperFirst = require('lodash').upperFirst; + + +module.exports = (plop) => { + plop.setHelper('upperFirst', (x) => { + return upperFirst(x); + }); + plop.setGenerator('create:service', { + description: '创建一个服务', + prompts: [ + { + type: 'input', + name: 'service_name', + message: '请输入创建的服务名:', + } + ], + actions: [ + { + type: 'add', + path: './service/{{service_name}}.ts', + templateFile: './cli/service.ts.hbs', + }, + { + path: './service/index.ts', + pattern: /(\/\/ SERVICE_INPUT)/g, + template: 'import {{upperFirst service_name}}Service from \'./{{service_name}}\';\n$1', + type: 'modify', + }, + { + path: './service/index.ts', + pattern: /(\/\/ SERVICE_REGISTER)/g, + template: '{{upperFirst service_name}}Service: {{upperFirst service_name}}Service;\n $1', + type: 'modify' + } + ] + }); + + plop.setGenerator('create:api-controller', { + prompts: [ + { + type: 'input', + name: 'controller_name', + message: '请输入创建的控制名:', + } + ], + actions: [ + { + type: 'add', + path: './controller/api/{{controller_name}}.ts', + templateFile: './cli/controller.ts.hbs', + }, + { + path: './router/config.ts', + pattern: /(\/\/ RESTFUL)/g, + templateFile: './cli/route-config.hbs', + type: 'modify', + }, + ] + }); +} diff --git a/app/router/config.ts b/app/router/config.ts new file mode 100644 index 0000000..cb5d124 --- /dev/null +++ b/app/router/config.ts @@ -0,0 +1,19 @@ +/** + * 路由配置文件 注意:请勿删除全大写注释。 + * 一般情况下保持restful风格的接口规范。但特殊情况仍然支持自定义形式。如上传文件等方案 + */ + export default { + controller: { + '/api/test/player': { + path: '/api/player/test', + method: 'get', + } + }, + restful: { + // RESTFUL + '/api/player': { + name: 'Player', + path: '/api/player' + }, + } +} diff --git a/app/router/index.ts b/app/router/index.ts new file mode 100644 index 0000000..5a10793 --- /dev/null +++ b/app/router/index.ts @@ -0,0 +1,38 @@ + + +import express, { Request, Response, Express } from 'express'; +import { map } from 'lodash'; +import routeConf from './config'; +import { mapObjIndexed } from '../tools/pureFunc'; + + + +const { restful, controller } = routeConf; + +const getHandleFunc = (path: string, action: string) => { + const Constr = require('../controller' + path).default; + const handleFactory = (action: string) => (req: Request, res: Response) => { + const ctorObj = new Constr(req, res); + ctorObj[action](); + }; + return handleFactory(action); +} + +const initRouters = (app: Express) => { + mapObjIndexed((v, k) => { + const idUrl = k + '/:id'; + app.get(k, getHandleFunc(v.path, 'index')); + app.post(k, getHandleFunc(v.path, 'create')); + app.get(idUrl, getHandleFunc(v.path, 'show')); + app.put(idUrl, getHandleFunc(v.path, 'update')); + app.delete(idUrl, getHandleFunc(v.path, 'destroy')); + }, restful); + mapObjIndexed((v, k) => { + const paths = v.path.split('/'); + const action = paths.pop()!; + const path = paths.join('/'); + (app as any)[v.method](k, getHandleFunc(path, action)); + }, controller); +} + +export default initRouters; diff --git a/app/server.js b/app/server.js deleted file mode 100755 index 72e5b39..0000000 --- a/app/server.js +++ /dev/null @@ -1,11 +0,0 @@ -const express = require('express'); - -const app = express(); - -app.get('/', (req, res) => { - res.json({"message": "Building a RESTful CRUD API with Node.js, Express/Koa and MongoDB."}); -}); - -app.listen(3000, () => { - console.log("Server is listening on port 3000"); -}); \ No newline at end of file diff --git a/app/service/index.ts b/app/service/index.ts new file mode 100644 index 0000000..e4fa5e6 --- /dev/null +++ b/app/service/index.ts @@ -0,0 +1,12 @@ +/** + * 警告:请勿删除全大写注释 + */ +import PlayerService from "./player"; +// SERVICE_INPUT + +interface IRegisterService { + PlayerService: PlayerService; + // SERVICE_REGISTER +} + +export default IRegisterService; diff --git a/app/service/player.ts b/app/service/player.ts new file mode 100644 index 0000000..44741cb --- /dev/null +++ b/app/service/player.ts @@ -0,0 +1,40 @@ +import BaseService from "../base/server"; + + +export default class PlayerService extends BaseService { + + get model() { + return this.ctx.model.Player; + } + + publicAll() { + return this.model.findAll(); + } + + fetchOne(id: number) { + return this.model.findByPk(id); + } + + // 暂不过多做数据验证。设计框架花费了大量时间 + create(obj: Record) { + return this.model.create(obj); + } + + // 暂不过多做数据验证。 + modify(id: number, obj: Record) { + return this.model.update(obj, { + where: { + id, + }, + }); + } + + // 根据情况可做软删除。这里直接删 + delete(id: number) { + return this.model.destroy({ + where: { + id, + }, + }); + } +} diff --git a/app/test/common/http.ts b/app/test/common/http.ts new file mode 100644 index 0000000..d9ba9bb --- /dev/null +++ b/app/test/common/http.ts @@ -0,0 +1,94 @@ +/** + * 原计划用于test的http请求。目前暂未使用 + */ +import { request } from 'http'; +import { equal } from 'assert'; + + +interface IResult { + errcode: number; + msg: string; + retcode: number; + data: any; +} + +class HttpTest { + private config = { + host: '127.0.0.1', + // port: process.env.SERVER_PORT, + port: 3000, + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': 0, + } + }; + private url: string; + private data: string | object | undefined; + private result: IResult; + private statusCode: number | undefined; + + static httpRequest() { + return new HttpTest(); + } + + post(url: string) { + this.url = url; + this.config.method = 'POST'; + return this; + } + get(url: string) { + this.url = url; + this.config.method = 'GET'; + return this; + } + put(url: string) { + this.url = url; + this.config.method = 'PUT'; + return this; + }; + delete(url: string) { + this.url = url; + this.config.method = 'DELETE'; + return this; + }; + send(data?: string | object | undefined) { + this.data = data; + return this; + } + expect(param: number | ((result: IResult) => void)) { + let data = ''; + if (!this.statusCode) { + new Promise(resolve => { + + }).then(); + const dataStr = JSON.stringify(this.data); + this.data && (this.config.headers['Content-Length'] = dataStr.length); + const req = request(this.config, res => { + this.statusCode = res.statusCode!; + res.setEncoding('utf8'); + res.on('data', d => { + data += d; + }); + res.on('end', () => { + this.result = JSON.parse(data); + if (typeof param === 'number') { + equal(this.statusCode, param); + } else { + param(this.result); + } + }); + }); + req.on('error', e => console.error(e)); + req.write(dataStr); + req.end(); + } else if (typeof param === 'number') { + equal(this.statusCode, param); + } else { + param(this.result); + } + return this; + } +} + +export default HttpTest; diff --git a/app/test/common/request.ts b/app/test/common/request.ts new file mode 100644 index 0000000..bf19044 --- /dev/null +++ b/app/test/common/request.ts @@ -0,0 +1,29 @@ + + +import { Express } from 'express'; +import request, { CallbackHandler } from "supertest"; + + +const supRequest = (app: Express) => { + const superRequest = request(app); + const baseGet: Function = superRequest.get; + const basePost: Function = superRequest.post; + const basePut: Function = superRequest.put; + const baseDelete: Function = superRequest.delete; + superRequest.get = function (url: string, callback?: CallbackHandler | undefined) { + return baseGet.apply(superRequest, [url, callback]).set('Accept', 'application/json'); + } + superRequest.post = function (url: string, callback?: CallbackHandler | undefined) { + return basePost.apply(superRequest, [url, callback]).set('Accept', 'application/json'); + } + superRequest.put = function (url: string, callback?: CallbackHandler | undefined) { + return basePut.apply(superRequest, [url, callback]).set('Accept', 'application/json'); + } + superRequest.delete = function (url: string, callback?: CallbackHandler | undefined) { + return baseDelete.apply(superRequest, [url, callback]).set('Accept', 'application/json'); + } + return superRequest; +} + +export default supRequest; + diff --git a/app/test/controller/api/player.test.ts b/app/test/controller/api/player.test.ts new file mode 100644 index 0000000..240243a --- /dev/null +++ b/app/test/controller/api/player.test.ts @@ -0,0 +1,92 @@ + + +import { Op } from 'sequelize'; +import { expect } from 'chai'; +import supRequest from '../../common/request'; +import { app } from '../../../app'; +import initORM from '../../../model'; + + +describe.only('test/controller/api/player.test.ts', () => { + + let model: any; + + before(async () => { + const mapModel = await initORM(); + model = mapModel['Player'] as any; + }) + + beforeEach(async () => { + // 清除测试数据库中数据,防止数据影响测试脚本 + await model.destroy({ where: { id: { [Op.gt]: 0 } } }); + }); + + it('should GET /api/test/player', () => { + return supRequest(app) + .get('/api/test/player') + .expect(200) + .then(res => { + expect(res.body.data).to.equal('result test'); + }); + }); + + it('shold GET /api/player', async () => { + const entity = await model.create({ name: 'unittest', position: 'SG' }); + return supRequest(app) + .get('/api/player') + .expect(200) + .then(res => { + expect(res.body.data.length).to.equal(1); + }); + }); + + it('shold GET /api/player/:id', async () => { + const entity = await model.create({ name: 'unittest', position: 'SG' }); + return supRequest(app) + .get(`/api/player/${entity.id}`) + .expect(200) + .then(res => { + expect(res.body.data.id).to.equal(entity.id); + }); + }); + + it('shold POST /api/player', async () => { + const body = { name: 'postName', position: 'SG' }; + return supRequest(app) + .post('/api/player') + .send(body) + .expect(200) + .then(res => { + expect(res.body.data.name).to.equal('postName'); + }); + }); + + it('shold DELETE /api/player', async () => { + const entity = await model.create({ name: 'unittest', position: 'SG' }); + return supRequest(app) + .delete(`/api/player/${entity.id}`) + .expect(200) + .then(res => { + expect(res.body.data).to.equal(1); + }); + }); + + it('shold PUT /api/player', async () => { + const entity = await model.create({ name: 'unittest', position: 'SG' }); + return supRequest(app) + .put(`/api/player/${entity.id}`) + .send({ + name: 'updateName', + }) + .expect(200) + .then(res => { + expect(res.body).to.deep.equals({ + errcode: 0, + msg: 'success', + retcode: 0, + data: '修改成功', + }); + }); + }); + +}); diff --git a/app/test/controller/home.test.ts b/app/test/controller/home.test.ts new file mode 100644 index 0000000..2920858 --- /dev/null +++ b/app/test/controller/home.test.ts @@ -0,0 +1,3 @@ +/** + * 非Ajax接口的测试。暂无 + */ \ No newline at end of file diff --git a/app/tools/pureFunc.ts b/app/tools/pureFunc.ts new file mode 100644 index 0000000..f3f9144 --- /dev/null +++ b/app/tools/pureFunc.ts @@ -0,0 +1,18 @@ +/** + * 用于写一些常用的纯函数 + */ +import { map } from 'lodash'; + + +type TKey> = T extends Record ? K : any; +type TVal> = T extends Record ? V : any; + +type TMapObjFunc = (v: TVal, k: TKey, obj: T) => any; + +export const mapObjIndexed = >(func: TMapObjFunc, obj: T): T => { + const result = {} as any; + map(obj, (val: any, key: any) => { + result[key] = func(val, key, obj) + }); + return result; +} diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..06f4309 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,69 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + "strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +}