├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── index.ts ├── package.json ├── src ├── const.ts ├── engine │ ├── index.ts │ └── main.ts ├── errors │ ├── application.exception.ts │ ├── component.exception.ts │ ├── filter.exception.ts │ └── general.exception.ts ├── util.ts └── web │ ├── annotation.ts │ ├── config.ts │ ├── main.ts │ ├── router.ts │ └── type.ts ├── test ├── app │ ├── assets │ │ └── favicon.jpg │ ├── component │ │ └── db.ts │ ├── config │ │ ├── app.config │ │ └── app.dev.config │ ├── controller │ │ ├── HomeController.js.map │ │ └── HomeController.ts │ ├── filter │ │ ├── Auth.js.map │ │ ├── Auth.ts │ │ ├── Trace.js.map │ │ └── Trace.ts │ ├── index.js.map │ ├── index.ts │ ├── interfaces │ │ └── httpInterface.ts │ ├── package.json │ ├── repository │ │ ├── dao │ │ │ ├── App_info.js.map │ │ │ └── App_info.ts │ │ ├── dto │ │ │ ├── App_info.js.map │ │ │ └── App_info.ts │ │ └── resource │ │ │ └── App_Info_Map.xml │ ├── service │ │ └── main.ts │ ├── templates │ │ └── user.ejs │ └── utils │ │ └── fs.ts ├── fakeServer │ ├── FakeServer.ts │ └── common.ts ├── mvc.spec.ts └── test.spec.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .idea/ 3 | .DS_Store 4 | node_modules/* 5 | **/*.js 6 | tags/* 7 | .history 8 | .vscode/* 9 | .nyc_output/ 10 | coverage/ 11 | dist/* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .idea/ 3 | .DS_Store 4 | node_modules/* 5 | tags/* 6 | .history 7 | .vscode/ 8 | coverage/ 9 | demo/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 8 4 | script: 5 | - npm run lint 6 | - npm run cover 7 | after_success: 8 | - cat ./dist/coverage/lcov.info | coveralls -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | ### 编译 4 | 5 | ```javascript 6 | npm run build 7 | ``` 8 | 编译 TypeScript 9 | 10 | ### 测试 11 | 12 | 语法检查 13 | 14 | ```javascript 15 | npm run lint 16 | ``` 17 | 18 | 单元测试检查 19 | 20 | ```javascript 21 | npm run test 22 | ``` 23 | 24 | ### 规范 25 | 26 | - 编写时请遵循 `tslint` 中的配置。 27 | - 提交代码时 git commit message 请遵循[格式化规范](http://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html),加入 [fix]、[feat]、[chore] 等标记 28 | - 发起 Pull Request 之前先用 lint 和 test 做检查,确保发 request 时尽量为无 warning 无 error 状态。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rockerjs MVC 2 | [![Build Status](https://travis-ci.org/weidian-inc/rockerjs-core.svg?branch=dev)](https://travis-ci.org/weidian-inc/rockerjs-core) 3 | [![Coverage Status](https://coveralls.io/repos/github/weidian-inc/rockerjs-core/badge.svg?branch=dev)](https://coveralls.io/github/weidian-inc/rockerjs-core?branch=dev) 4 | [![npm package](https://img.shields.io/npm/v/@rockerjs/core.svg)](https://www.npmjs.org/package/@rockerjs/core) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 6 | 7 | - [项目主页](https://rockerjs.weidian.com/compass/mvc.html) 8 | - [实例尝鲜](https://github.com/weidian-inc/rockerjs-demo) 9 | 10 | ## 简介 11 | 12 | Rockerjs-MVC是一套基于配置、具有轻量级容器特性且集成了链路追踪功能的Node.js Web应用框架。 13 | 14 | Rockerjs-MVC的容器特性可极致简化你的代码,帮助你快速构建高效、可扩展的应用程序。它采用DI(Dependency Inject)和OOP(Object Oriented Programming)的理念,遵循 **“配置大于约定”** 的规范,同时提供 **Starter机制** 实现“模块粒度下配置与约定的融合” ,使用TypeScript强类型语言构建你的应用。 15 | 16 | > Rockerjs-MVC的所有功能都是基于一个遵循**ini配置规范**的 "app.${env}.config" 文件来实现的,可提供四种不同环境的配置文件:dev、daily、pre、prod 17 | 18 | > 轻量级容器特性意味着Rockerjs-MVC可管理所有注解标识类的实例化对象,并管理其生命周期、对象间的依赖关系;当使用这些对象时可通过注解直接引用,无需手动实例化或建立对象间依赖。 19 | 20 | > Starter机制提供某些模块约定俗成的配置并自动初始化,无需开发者在程序中显式操作。当默认配置无法满足时,可通过配置文件配置该Starter相关参数。Starter机制采用基于约定的准则实现,但可基于配置文件进行扩展。 21 | 22 | ## 安装 23 | 24 | NPM: 25 | ```shell 26 | $ npm i --save @rockerjs/mvc 27 | ``` 28 | 29 | ## 使用 30 | 31 | ```typescript 32 | import { Logger } from "@rockerjs/common"; 33 | import { Application, AbstractApplication } from "@rockerjs/mvc"; 34 | 35 | @Application 36 | class App extends AbstractApplication { 37 | public async beforeServerStart(server, args: RockerConfig.Application) { 38 | Logger.info('beforeServerStart hook ' + args.name + args.uploadDir); 39 | } 40 | public static async main(args: RockerConfig.Application) { 41 | Logger.info('main bussiness ' + args.name + args.uploadDir); 42 | } 43 | } 44 | ``` 45 | 46 | ## 文档 47 | 48 | [Rockerjs-MVC使用教程](https://rockerjs.weidian.com/compass/mvc.html) 49 | 50 | ## Contribute 51 | 52 | 请参考 [Contribute Guide](https://github.com/weidian-inc/rockerjs-mvc/blob/master/CONTRIBUTING.md) 后提交 Pull Request。 53 | 54 | ## License 55 | 56 | MIT 57 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { Container, Inject } from "@rockerjs/core"; 2 | import { bootstrap } from "./src/engine"; 3 | import { AbstractComponent, Component, AbstractApplication, Application } from "./src/engine/main"; 4 | import { Filter, AbstractFilter, Plugin, AbstractPlugin, Controller, Request, Response, Param, Head, Get, Post } from "./src/web/annotation"; 5 | import { RedirectResp, DownloadResp } from "./src/web/main"; 6 | import { NODE_STARTER } from "./src/const"; 7 | 8 | // auto start if doesn't use rockerjs command tool 9 | if (process.env["NODE_STARTER"] !== NODE_STARTER) { 10 | (async () => { 11 | await bootstrap(process.cwd()); 12 | })(); 13 | } 14 | 15 | export { bootstrap, Inject, Container, AbstractComponent, Component, AbstractApplication, Application, 16 | Filter, AbstractFilter, Plugin, AbstractPlugin, Controller, Request, Response, Param, Head, Get, Post, RedirectResp, DownloadResp }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rockerjs/mvc", 3 | "version": "1.0.12", 4 | "description": "A MVC framework based on Rockerjs/core used by node.js", 5 | "author": { 6 | "name": "yangli", 7 | "email": "yl.yuxiu@foxmail.com", 8 | "url": "https://github.com/royalrover" 9 | }, 10 | "scripts": { 11 | "build": "npm run clean && (tsc || true)", 12 | "clean": "rimraf ./dist", 13 | "cover": "npm run build && cd dist && cross-env NODE_STARTER=rockerjs istanbul cover _mocha -x src/errors/**/*.js -- test/test.spec.js --timeout 10000 --exit --reporter spec --recursive", 14 | "dev": "npm run clean && tsc -w", 15 | "lint": "tslint ./src/**/*.ts ./src/*.ts", 16 | "prepublish": "npm run build", 17 | "start": "tsc -w", 18 | "test": "cd dist && npm run build && cross-env NODE_STARTER=rockerjs deco dist/test/test.spec.js" 19 | }, 20 | "dependencies": { 21 | "@rockerjs/common": "^1.0.1", 22 | "@rockerjs/core": "^1.0.2", 23 | "ejs": "^2.6.1", 24 | "ini": "^1.3.5", 25 | "koa": "^2.7.0", 26 | "koa-compress": "^3.0.0", 27 | "mime": "^2.4.2", 28 | "reflect-metadata": "^0.1.13", 29 | "sb-scandir": "^2.0.0" 30 | }, 31 | "devDependencies": { 32 | "@rockerjs/dao": "^1.0.0", 33 | "@rockerjs/rpc-starter": "^1.0.0", 34 | "@rockerjs/midlog-starter": "^1.0.0", 35 | "@rockerjs/tsunit": "^1.0.0", 36 | "@rockerjs/tracer": "1.0.1", 37 | "@types/koa": "^2.0.48", 38 | "@types/node": "^7.10.5", 39 | "coveralls": "^2.13.0", 40 | "cross-env": "^5.2.0", 41 | "istanbul": "^0.4.5", 42 | "md5": "^2.2.1", 43 | "moment": "^2.24.0", 44 | "request": "^2.88.0", 45 | "request-promise": "^4.2.4", 46 | "rimraf": "^2.6.3", 47 | "tslint": "^5.14.0", 48 | "typescript": "^2.7.2" 49 | }, 50 | "keywords": [ 51 | "ioc", 52 | "di", 53 | "javascript", 54 | "typescript", 55 | "node", 56 | "dependency injection", 57 | "dependency inversion", 58 | "inversion of control container", 59 | "AOP", 60 | "Aspect Oriented Program" 61 | ], 62 | "contributors": [ 63 | { 64 | "name": "chemingjun", 65 | "email": "chemingjun@weidian.com" 66 | }, 67 | { 68 | "name": "kangzhe", 69 | "email": "kangzhe@weidian.com" 70 | }, 71 | { 72 | "name": "dingjunjie", 73 | "email": "dingjunjie@weidian.com" 74 | } 75 | ], 76 | "license": "MIT", 77 | "directories": { 78 | "doc": "doc" 79 | }, 80 | "main": "./dist/index.js" 81 | } 82 | -------------------------------------------------------------------------------- /src/const.ts: -------------------------------------------------------------------------------- 1 | export const ENV = { 2 | DEV: "dev", 3 | DAILY: "daily", 4 | PRE: "pre", 5 | PROD: "prod", 6 | }; 7 | 8 | export const CONTAINER_TAG = { 9 | COMPONENT_TAG: "Component", 10 | APPLICATION_TAG: "Application", 11 | FILTER_TAG: "Filter", 12 | CONTROLLER_TAG: "Controller", 13 | PLUGIN_TAG: "Plugin", 14 | }; 15 | 16 | export const CONTROLLER = { 17 | CLASS: "class", 18 | REQUEST_MAPPING: "requestMapping", 19 | }; 20 | 21 | export const FILTER_CONFIG_SECTION = `filter`; 22 | // export const FILTER_CONFIG_ARRAY = `filters`; 23 | export const NODE_STARTER = `rockerjs`; 24 | export const APP_TAG = `application`; 25 | export const STARTER = `starter`; 26 | export const CONFIG_FILE_ENV = "config-env"; 27 | -------------------------------------------------------------------------------- /src/engine/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as fs from "fs"; 3 | import { Container } from "@rockerjs/core"; 4 | import { Logger } from "@rockerjs/common"; 5 | import scandir from "sb-scandir"; 6 | import * as ini from "ini"; 7 | import { IComponentCanon, AbstractApplication } from "./main"; 8 | import { AbstractFilter, AbstractPlugin } from "../web/annotation"; 9 | import { pipe, route, plugin, config } from "../web/main"; 10 | import { ApplicationException } from "../errors/application.exception"; 11 | import { separatorGenerator } from "../util"; 12 | import { ENV, CONFIG_FILE_ENV, CONTAINER_TAG, APP_TAG, STARTER } from "../const"; 13 | const { APPLICATION_TAG, COMPONENT_TAG, PLUGIN_TAG } = CONTAINER_TAG; 14 | const CONFIG_REG = /^app\.(?:(\w+?)\.)?config$/i; 15 | // kv file, JSON format, use key "env" get enviroment 16 | const PROPERTY_REG = "properties.config"; 17 | const CONFIG_TABLE = {}; 18 | let CURRENT_ENV = process.env.NODE_ENV || ENV.PROD; 19 | // if file has the magic alphabet, the scanner can't find it 20 | const SHADOW_FILE = "/// shadow"; 21 | const SHADOW_FILE1 = "///shadow"; 22 | const EXCLUDE_DIR = "excludesDir"; 23 | 24 | function scan(rootPath: string = process.cwd()) { 25 | let excludesDir = null; 26 | if (CONFIG_TABLE[CURRENT_ENV] && Array.isArray(CONFIG_TABLE[CURRENT_ENV][EXCLUDE_DIR])) { 27 | excludesDir = CONFIG_TABLE[CURRENT_ENV][EXCLUDE_DIR]; 28 | } 29 | return scandir(rootPath, true, function(pth) { 30 | const stat = fs.statSync(pth), 31 | extName = path.extname(pth), 32 | baseName = path.basename(pth), 33 | content = stat.isDirectory() ? "" : fs.readFileSync(pth, "utf8"); 34 | let isExcludeDir = false; 35 | 36 | if (excludesDir) { 37 | for (let i = 0, len = excludesDir.length; i < len; i++) { 38 | const it = excludesDir[i]; 39 | if (it && pth.includes(it)) { 40 | isExcludeDir = true; 41 | break; 42 | } 43 | } 44 | } 45 | 46 | return !isExcludeDir && baseName !== "node_modules" && (extName === ".js" && (!content.includes(SHADOW_FILE) && !content.includes(SHADOW_FILE1)) || stat.isDirectory()); 47 | }); 48 | } 49 | 50 | function parseConfigFile(rootPath: string, componentName?: string) { 51 | return scandir(rootPath, true, function(pth) { 52 | const stat = fs.statSync(pth), 53 | baseName = path.basename(pth); 54 | return baseName !== "node_modules" && (baseName.match(CONFIG_REG) || baseName === PROPERTY_REG || stat.isDirectory()); 55 | }).then((result) => { 56 | // config file in ascending order, like app.daily.config < app.dev.config < app.pre.config < app.prod.config < properties.config 57 | // so, kv pairs in properties.config have the highest priority 58 | result.files.sort((a, b) => { 59 | const aBaseName = path.basename(a), 60 | bBaseName = path.basename(b); 61 | return aBaseName > bBaseName; 62 | }); 63 | result.files.forEach((fpath: string) => { 64 | const baseName = path.basename(fpath); 65 | if (baseName === PROPERTY_REG) { 66 | try { 67 | const props = JSON.parse(fs.readFileSync(fpath, "utf8")); 68 | if (props && props.env) { 69 | const env = props.env; 70 | // env in properties.config is the latest env 71 | CURRENT_ENV = env; 72 | delete props.env; 73 | CONFIG_TABLE[env] ? Object.assign(CONFIG_TABLE[env], props) : CONFIG_TABLE[env] = props; 74 | } else { 75 | throw new ApplicationException(`Properties.config must have "env" key, it should be one of the items ["dev", "daily", "pre", "prod"]`); 76 | } 77 | } catch (e) { 78 | throw new ApplicationException(`Parse properties.config error`, e.stack); 79 | } 80 | } else { 81 | const matchArray = baseName.match(CONFIG_REG), 82 | configEnv = matchArray && matchArray[1]; 83 | configEnv ? (CONFIG_TABLE[configEnv] ? Object.assign(CONFIG_TABLE[configEnv], ini.parse(fs.readFileSync(fpath, "utf-8"))) : CONFIG_TABLE[configEnv] = ini.parse(fs.readFileSync(fpath, "utf-8"))) 84 | : (CONFIG_TABLE[ENV.PROD] ? Object.assign(CONFIG_TABLE[ENV.PROD], ini.parse(fs.readFileSync(fpath, "utf-8"))) : CONFIG_TABLE[ENV.PROD] = ini.parse(fs.readFileSync(fpath, "utf-8"))); 85 | } 86 | }); 87 | return CONFIG_TABLE; 88 | }); 89 | } 90 | 91 | export async function bootstrap(rootPath: string) { 92 | try { 93 | const scanErrorFiles = []; 94 | let modules = []; 95 | 96 | // 1st step: parse configuration 97 | // Logger.info(separatorGenerator("step 1. parse configuration")); 98 | await parseConfigFile(rootPath); 99 | const appConfig = CONFIG_TABLE[CURRENT_ENV] && CONFIG_TABLE[CURRENT_ENV][APP_TAG]; 100 | // 2nd step: scan files and load them, but can't find the starter configured in app.config 101 | // Logger.info(separatorGenerator("step 2. files scanning & loading")); 102 | await scan(rootPath).then((result) => { 103 | return result.files.forEach((f: string) => { 104 | try { 105 | require(f); 106 | } catch (e) { 107 | // deps haven't init already, reload them after modules initation 108 | scanErrorFiles.push(f); 109 | CURRENT_ENV === ENV.DEV && Logger.warn(`scan exception in path ${f}, ${e.message}`); 110 | } 111 | }); 112 | }); 113 | 114 | // 3th step: init all components and starters 115 | // 3.1 init starter 116 | const componentsInitialArray = [], componentNames = []; 117 | if (CONFIG_TABLE[CURRENT_ENV]) { 118 | for (const section in CONFIG_TABLE[CURRENT_ENV]) { 119 | if (CONFIG_TABLE[CURRENT_ENV].hasOwnProperty(section)) { 120 | const sectionConfig = CONFIG_TABLE[CURRENT_ENV][section]; 121 | if (sectionConfig && sectionConfig[STARTER]) { 122 | try { 123 | require(sectionConfig[STARTER]); 124 | } catch (e) { 125 | throw new ApplicationException(`${section} starter run error, ${e.message}`, e.stack); 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | // init MidLoggerStarter first, to use Logger's speciality 133 | const components = Container.getTypedHashmap().get(COMPONENT_TAG); 134 | const constructors = components && components.keys(); 135 | if (constructors) { 136 | for (const cons of constructors) { 137 | const componentName = cons.name.substring(0, 1).toLowerCase() + cons.name.substring(1); 138 | if (componentName === "midLogger") { 139 | const curretEnvConfig = CONFIG_TABLE[CURRENT_ENV] && CONFIG_TABLE[CURRENT_ENV][componentName]; 140 | const object = Container.getObject(componentName); 141 | curretEnvConfig && (curretEnvConfig[CONFIG_FILE_ENV] = CURRENT_ENV); 142 | await object.start(curretEnvConfig, appConfig); 143 | break; 144 | } 145 | } 146 | } 147 | 148 | Logger.info(separatorGenerator("step 1. discover starters & components and collocate them")); 149 | 150 | Container.getTypedHashmap().get(COMPONENT_TAG) && Container.getTypedHashmap().get(COMPONENT_TAG).forEach((v, constructor) => { 151 | const componentName = constructor.name.substring(0, 1).toLowerCase() + constructor.name.substring(1); 152 | const curretEnvConfig = CONFIG_TABLE[CURRENT_ENV] && CONFIG_TABLE[CURRENT_ENV][componentName]; 153 | const object = Container.getObject(componentName); 154 | curretEnvConfig && (curretEnvConfig[CONFIG_FILE_ENV] = CURRENT_ENV); 155 | componentNames.push(componentName); 156 | // component的初始化函数传递两个参数:componentConfig和appConfig 157 | componentsInitialArray.push(object.start(curretEnvConfig, appConfig)); 158 | }); 159 | 160 | let initialOutcome = await Promise.all(componentsInitialArray); 161 | initialOutcome = initialOutcome.map((val, index) => { 162 | return val ? val : (Container.getObject(componentNames[index])); 163 | }); 164 | 165 | // 4th step: reload error files 166 | Logger.info(separatorGenerator("step 2. reloading & sweeping js modules")); 167 | scanErrorFiles.forEach((f) => { 168 | try { 169 | require(f); 170 | } catch (e) { 171 | throw new ApplicationException(`File ${f}'s dependences can't load normally, ${e.message}`, e.stack); 172 | } 173 | }); 174 | 175 | // 4.1: start components when first init error 176 | const componentsRestartArray = []; 177 | Container.getTypedHashmap().get(COMPONENT_TAG) && Container.getTypedHashmap().get(COMPONENT_TAG).forEach((v, constructor) => { 178 | const componentName = constructor.name.substring(0, 1).toLowerCase() + constructor.name.substring(1); 179 | const object = Container.getObject(componentName); 180 | if (object.status !== "on") { 181 | const curretEnvConfig = CONFIG_TABLE[CURRENT_ENV] && CONFIG_TABLE[CURRENT_ENV][componentName]; 182 | curretEnvConfig && (curretEnvConfig[CONFIG_FILE_ENV] = CURRENT_ENV); 183 | componentsRestartArray.push(object.start(curretEnvConfig, appConfig)); 184 | } 185 | }); 186 | // restart components 187 | await Promise.all(componentsRestartArray); 188 | 189 | modules = Object.keys(require("module")._cache).filter((name) => { 190 | return name.indexOf("/node_modules") === -1; 191 | }); 192 | 193 | // 5th: web server start 194 | Logger.info(separatorGenerator("step 3.1. init and compose filters")); 195 | // 5.1: collect filters 196 | const filters: Array = []; 197 | const filtersConfig = {}; 198 | CONFIG_TABLE[CURRENT_ENV] && Object.keys(CONFIG_TABLE[CURRENT_ENV]).map((it) => { 199 | if (it.indexOf("filter:") !== -1) { 200 | filtersConfig[it.slice(7)] = CONFIG_TABLE[CURRENT_ENV][it]; 201 | } 202 | }); 203 | // CONFIG_TABLE[CURRENT_ENV][FILTER_CONFIG_SECTION] && CONFIG_TABLE[CURRENT_ENV][FILTER_CONFIG_SECTION][FILTER_CONFIG_ARRAY]; 204 | 205 | // if (!Array.isArray(filtersConfig)) { 206 | // throw new ApplicationException(`Filter's config must be an array, like "filters[]=filterName:arg1,arg2"`); 207 | // } 208 | 209 | Object.keys(filtersConfig).forEach((filterName) => { 210 | try { 211 | const filter = Container.getObject(filterName); 212 | // filter的初始化函数传递两个参数 213 | filter.init(filtersConfig[filterName], appConfig); 214 | filters.push(filter); 215 | } catch (e) { 216 | throw new ApplicationException(`Filter error duaring its lifetime, ${e.message}`, e.stack); 217 | } 218 | }); 219 | 220 | // beforeServerStart hook 221 | Logger.info(separatorGenerator("step 3.2. trigger hook beforeServerStart")); 222 | if (Container.getTypedHashmap().get(APPLICATION_TAG)) { 223 | if (Container.getTypedHashmap().get(APPLICATION_TAG).size !== 1) { 224 | throw new ApplicationException(`Application must have only one entry method`); 225 | } else { 226 | const applicationArray = []; 227 | Container.getTypedHashmap().get(APPLICATION_TAG).forEach((v, clazz) => { 228 | applicationArray.push(Container.injectClazzManually(clazz, APPLICATION_TAG).beforeServerStart({ 229 | config, 230 | }, CONFIG_TABLE[CURRENT_ENV] && CONFIG_TABLE[CURRENT_ENV][APP_TAG])); 231 | }); 232 | await Promise.all(applicationArray); 233 | } 234 | } else { 235 | throw new ApplicationException(`Must have Application annotation`); 236 | } 237 | 238 | // 5.2: mvc runner 239 | Logger.info(separatorGenerator("step 3.3. run MVC framework")); 240 | // 5.2.1: amount filters 241 | filters.forEach((filter) => { 242 | pipe(filter.doFilter.bind(filter)); 243 | }); 244 | // 5.2.2: parse controller 245 | const rockerjsHandler = route(modules); 246 | // 5.2.3: start mvc 247 | const serverHandler = rockerjsHandler.start({ 248 | port: +(CONFIG_TABLE[CURRENT_ENV][APP_TAG]["port"] || CONFIG_TABLE[CURRENT_ENV]["port"] || 8080), 249 | }); 250 | // 5.2.4: add render plugins 251 | Container.getTypedHashmap().get(PLUGIN_TAG) && Container.getTypedHashmap().get(PLUGIN_TAG).forEach((v, clazz) => { 252 | const pl: AbstractPlugin = Container.injectClazzManually(clazz, PLUGIN_TAG); 253 | const clazzName = clazz.name; 254 | const pluginName = clazzName.substring(0, 1).toLowerCase() + clazzName.substring(1); 255 | // 调用plugin 256 | plugin(pl.do(CONFIG_TABLE[CURRENT_ENV] && CONFIG_TABLE[CURRENT_ENV][pluginName])); 257 | }); 258 | 259 | // 6th: start engine 260 | setTimeout(async () => { 261 | Logger.info(separatorGenerator("step 4. invoke main function, welcome to @rockerjs/mvc!")); 262 | if (Container.getTypedHashmap().get(APPLICATION_TAG).size !== 1) { 263 | throw new ApplicationException(`Application must have only one entry method`); 264 | } else { 265 | const applicationArray = []; 266 | Container.getTypedHashmap().get(APPLICATION_TAG).forEach((v, clazz) => { 267 | applicationArray.push((clazz as any).main(CONFIG_TABLE[CURRENT_ENV] && CONFIG_TABLE[CURRENT_ENV][APP_TAG])); 268 | }); 269 | await Promise.all(applicationArray); 270 | } 271 | }); 272 | } catch (e) { 273 | throw e; 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/engine/main.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "@rockerjs/core"; 2 | import { ApplicationException } from "../errors/application.exception"; 3 | import { ComponentException } from "../errors/component.exception"; 4 | import { CONTAINER_TAG } from "../const"; 5 | import { ServerFacade } from "../web/type"; 6 | import * as Util from "../util"; 7 | const { APPLICATION_TAG, COMPONENT_TAG } = CONTAINER_TAG; 8 | const ComponentRelationContainer: Map = new Map(); 9 | 10 | export abstract class AbstractComponent { 11 | public static Module: any; 12 | // private static _moduleObject: any; 13 | // public static _module(config: any): any { 14 | // if (config.driver.match(PATH_REG)) { 15 | // this._moduleObject = require(path.join(process.cwd(), `${config.driver}`)); 16 | // } else { 17 | // this._moduleObject = require(`${config.driver}`); 18 | // } 19 | // } 20 | // public static module(): T { 21 | // return this._moduleObject; 22 | // } 23 | 24 | public abstract start(config: any, appConfig?: any): Promise | any; 25 | } 26 | 27 | export interface IComponentCanon { 28 | name: string; 29 | status: "on" | "pending" | "off"; 30 | start: (args: any, appConfig?: any) => Promise; 31 | export: () => object; 32 | } 33 | 34 | /** 35 | * @description the factory method of decorator, put the Class's instance into the container 36 | * @param args decorator's params 37 | * @returns void | any 38 | */ 39 | export function Component(...args: any[]): ClassDecorator | any { 40 | const fn: ClassDecorator = function(target: Function) { 41 | // define class module 42 | if (target !== undefined) { 43 | const clazz = target as FunctionConstructor; // target is class constructor 44 | Container.provides([ COMPONENT_TAG, clazz, function() { 45 | try { 46 | const component = new (class implements IComponentCanon { 47 | public name: string; 48 | public status: "on" | "pending" | "off" = "off"; 49 | public dependences = []; 50 | private _export: AbstractComponent; 51 | 52 | constructor(name: string) { 53 | this.name = name; 54 | try { 55 | this._export = (new (clazz)(...args)) as any; 56 | // set prototype 57 | // const originProto = (this as any).__proto__; 58 | // (this as any).__proto__ = this._export; 59 | // (this as any).__proto__.constructor = originProto.constructor; 60 | // (this as any).__proto__.__proto__.__proto__ = originProto; 61 | this.status = "pending"; 62 | ComponentRelationContainer.set(clazz, this); 63 | } catch (e) { 64 | this.status = "off"; 65 | e.target = this; 66 | ComponentRelationContainer.set(clazz, e); 67 | } 68 | } 69 | 70 | // args should be injected from config file 71 | public start(config: any, appConfig: any): Promise { 72 | if (this._export && typeof this._export.start === "function") { 73 | try { 74 | const sret = this._export.start(config, appConfig); 75 | return Util.isPromise(sret) ? sret.then((result) => { 76 | this.status = "on"; 77 | return result ? result : this; 78 | }).catch((err) => { 79 | this.status = "off"; 80 | throw new ComponentException(err.message, err.stack); 81 | }) : (this.status = "on", Promise.resolve(sret ? sret : this)); 82 | } catch (err) { 83 | this.status = "off"; 84 | throw new ComponentException(err.message, err.stack); 85 | } 86 | } else { 87 | // do not have start 88 | this.status = "on"; 89 | return Promise.resolve(this); 90 | } 91 | } 92 | 93 | public export() { 94 | return this._export; 95 | } 96 | })(clazz.name); 97 | 98 | const clazzName = clazz.name; 99 | Container.setObject(clazzName.substring(0, 1).toLowerCase() + clazzName.substring(1), component); 100 | return component; 101 | } catch (e) { 102 | throw new ComponentException(`Module factory error, ${e.message}`); 103 | } 104 | }]); 105 | 106 | args.unshift(COMPONENT_TAG); 107 | Container.injectClazzManually(clazz, ...args); 108 | } 109 | }; 110 | return fn.apply(null, args); 111 | } 112 | 113 | export abstract class AbstractApplication { 114 | public static async main(this: new (someVar: any) => T, config: any): Promise { 115 | throw new ApplicationException(`Application must override the static method "main"`); 116 | } 117 | 118 | public abstract async beforeServerStart(server: ServerFacade, config: any): Promise; 119 | } 120 | 121 | export function Application(...args: any[]): ClassDecorator | any { 122 | return (function(target: Function) { 123 | // define class module 124 | if (target !== undefined) { 125 | const clazz = target as FunctionConstructor; // target is class constructor 126 | Container.provides([APPLICATION_TAG, clazz, function() { 127 | return new clazz(); 128 | }]); 129 | args.unshift(APPLICATION_TAG); 130 | Container.injectClazzManually(clazz, ...args); 131 | } 132 | }).apply(this, args); 133 | } 134 | -------------------------------------------------------------------------------- /src/errors/application.exception.ts: -------------------------------------------------------------------------------- 1 | import { GeneralException } from "./general.exception"; 2 | 3 | export class ApplicationException extends GeneralException { 4 | constructor(msg?: string, stack?: string) { 5 | super(` 6 | ApplicationException has been detected, ${msg ? msg + "," : ""} 7 | `, stack); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/component.exception.ts: -------------------------------------------------------------------------------- 1 | import { GeneralException } from "./general.exception"; 2 | 3 | export class ComponentException extends GeneralException { 4 | constructor(msg?: string, stack?: string) { 5 | super(` 6 | ComponentException has been detected, ${msg ? msg + "," : ""} 7 | `, stack); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/filter.exception.ts: -------------------------------------------------------------------------------- 1 | import { GeneralException } from "./general.exception"; 2 | 3 | export class FilterException extends GeneralException { 4 | constructor(msg?: string, stack?: string) { 5 | super(` 6 | FilterException has been detected, ${msg ? msg + "," : ""} 7 | `, stack); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/general.exception.ts: -------------------------------------------------------------------------------- 1 | export declare class Error { 2 | public name: string; 3 | public message: string; 4 | public stack: string; 5 | constructor(message?: string); 6 | } 7 | 8 | export class GeneralException extends Error { 9 | constructor(private readonly msg = ``, stack?: string) { 10 | super(msg); 11 | stack ? this.stack = stack : null; 12 | } 13 | 14 | public desc() { 15 | return this.msg; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export function isEmpty(v: any): boolean { 2 | return typeof v === "undefined" || v == null; 3 | } 4 | 5 | export function isPromise(obj): boolean { 6 | if (obj && typeof obj.then === "function" && typeof obj.catch === "function") { 7 | return true; 8 | } else { 9 | return false; 10 | } 11 | } 12 | 13 | export function isFunction(fn): boolean { 14 | return !isEmpty(fn) && Object.prototype.toString.call(fn) === "[object Function]"; 15 | } 16 | 17 | export function isGenerator(obj) { 18 | return "function" === typeof obj.next && "function" === typeof obj.throw; 19 | } 20 | 21 | export function isGeneratorFunction(obj) { 22 | const constructor = obj.constructor; 23 | if (!constructor) { 24 | return false; 25 | } 26 | if ("GeneratorFunction" === constructor.name || "GeneratorFunction" === constructor.displayName) { 27 | return true; 28 | } 29 | return isGenerator(constructor.prototype); 30 | } 31 | 32 | export function getLocalIp() { 33 | const os = require("os"); 34 | let localIp = "127.0.0.1"; 35 | const interfaces = os.networkInterfaces(); 36 | for (const devName in interfaces) { 37 | const devInterface = interfaces[devName]; 38 | for (let i = 0; i < devInterface.length; i++) { 39 | const iface = devInterface[i]; 40 | if (iface.family === "IPv4" && !iface.internal && iface.address !== localIp && iface.address.indexOf(":") < 0) { 41 | localIp = iface.address; 42 | return localIp; 43 | } 44 | } 45 | } 46 | return localIp; 47 | } 48 | 49 | export function separatorGenerator(msg: string) { 50 | const separatorArr = new Array(120); 51 | let tmp = null; 52 | return (separatorArr.splice(15, 0, msg), tmp = separatorArr.join("-"), tmp.slice(0, 120)); 53 | } 54 | -------------------------------------------------------------------------------- /src/web/annotation.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "@rockerjs/core"; 2 | import * as Application from "koa"; 3 | import { CONTAINER_TAG, CONTROLLER } from "../const"; 4 | import * as _Types from "./type"; 5 | import { Types } from "./main"; 6 | const { ReqMethodParamType } = _Types; 7 | 8 | /** 9 | * Router pattern bindings 10 | * @type Map 11 | * Function:RouterClass 12 | */ 13 | export const routerPtnBindings: Map = new Map(); 14 | 15 | export const routerPathBindings: Map = new Map(); 16 | 17 | export function Filter(...args: any[]): ClassDecorator | any { 18 | return (function(target: Function) { 19 | // define class module 20 | if (target !== undefined) { 21 | const clazz = target as FunctionConstructor; // target is class constructor 22 | Container.provides([CONTAINER_TAG.FILTER_TAG, clazz, function() { 23 | const clazzName = clazz.name; 24 | const filter = new clazz(...args); 25 | Container.setObject(clazzName.substring(0, 1).toLowerCase() + clazzName.substring(1), filter); 26 | }]); 27 | 28 | args.unshift(CONTAINER_TAG.FILTER_TAG); 29 | Container.injectClazzManually(clazz, ...args); 30 | } 31 | }).apply(this, args); 32 | } 33 | 34 | export abstract class AbstractFilter { 35 | public abstract init(args: any, appConfig?: any): void; 36 | public abstract async doFilter(context: Application.Context, next): Promise; 37 | public abstract destroy(): void; 38 | } 39 | 40 | export function Plugin(...args: any[]): ClassDecorator | any { 41 | return (function(target: Function) { 42 | // define class module 43 | if (target !== undefined) { 44 | const clazz = target as FunctionConstructor; // target is class constructor 45 | Container.provides([CONTAINER_TAG.PLUGIN_TAG, clazz, function() { 46 | const clazzName = clazz.name; 47 | const plugin = new clazz(...args); 48 | return plugin; 49 | }]); 50 | 51 | args.unshift(CONTAINER_TAG.PLUGIN_TAG); 52 | Container.injectClazzManually(clazz, ...args); 53 | } 54 | }).apply(this, args); 55 | } 56 | 57 | export abstract class AbstractPlugin { 58 | public abstract do(config: any): (input: Types.Pluginput) => void; 59 | } 60 | 61 | export function Controller(...args: any[]): ClassDecorator | any { 62 | return function(target: Function) { 63 | // define class module 64 | if (target !== undefined) { 65 | const clazz = target as FunctionConstructor; // target is class constructor 66 | Container.provides([CONTAINER_TAG.CONTROLLER_TAG, clazz, function() { 67 | const clazzName = clazz.name; 68 | const composeMap = new Map(); 69 | composeMap.set(CONTROLLER.CLASS, clazz); 70 | composeMap.set(CONTROLLER.REQUEST_MAPPING, args && args.length && args[1] || `/${clazzName}`); 71 | Container.setObject(clazzName.substring(0, 1).toLowerCase() + clazzName.substring(1), composeMap); 72 | }]); 73 | 74 | args.unshift(CONTAINER_TAG.CONTROLLER_TAG); 75 | Container.injectClazzManually(clazz, ...args); 76 | } 77 | }; 78 | } 79 | 80 | /** 81 | * The param"s decorator for Request object of koa 82 | * @param {object} target 83 | * @param {string} methodName 84 | * @param {number} index 85 | * @constructor 86 | */ 87 | export function Request(target: object, methodName: string, index: number): void { 88 | const rfc: _Types.RouterForClz = getRouterForClz(target); 89 | rfc.regMethodParam(methodName, index, ReqMethodParamType.Request, {required: true}, (v) => { 90 | return v; 91 | }); 92 | } 93 | 94 | export function Response(target: object, methodName: string, index: number): void { 95 | const rfc: _Types.RouterForClz = getRouterForClz(target); 96 | rfc.regMethodParam(methodName, index, ReqMethodParamType.Response, { required: true }, (v) => { 97 | return v; 98 | }); 99 | } 100 | 101 | export function Param(_cfg: _Types.RouterParamType): Function { 102 | return function(target: Function, paramName: string, index: number) { // Use @Get(string|{url:string,render:string}) 103 | const rfc: _Types.RouterForClz = getRouterForClz(target); 104 | let dt = Reflect.getMetadata("design:paramtypes", target, paramName); 105 | if (!dt) { 106 | dt = Reflect.getMetadata("design:paramtypes", target.constructor, paramName); 107 | } 108 | if (!dt) { 109 | throw new Error("Reflect error occured."); 110 | } 111 | rfc.regMethodParam(paramName, index, ReqMethodParamType.Normal, _cfg, (v) => { 112 | if ( v === undefined || v === null) { 113 | return v; 114 | } 115 | const tfn = dt[index]; 116 | if (tfn.name.toUpperCase() === "OBJECT" || tfn.name.toUpperCase() === "BOOLEAN") { 117 | return typeof (v) === "string" ? (new Function("", `return ${v}`))() : v; // Support ill-formed json object 118 | } else { 119 | return tfn(v); 120 | } 121 | }); 122 | }; 123 | } 124 | 125 | export function Head(...args: (_Types.RPParam)[]): Function|any { 126 | return decoratorMethod("head", args); 127 | } 128 | 129 | export function Get(...args: (_Types.RPParam)[]): Function|any { 130 | return decoratorMethod("get", args); 131 | } 132 | 133 | export function Post(...args: (_Types.RPParam)[]): Function|any { 134 | return decoratorMethod("post", args); 135 | } 136 | 137 | // -------------------------------------------------------------------------------------------- 138 | 139 | function decoratorMethod(method: string, args): Function|any { 140 | const md = method.charAt(0).toUpperCase() + method.substring(1); 141 | if (args.length === 1) { // @Get(string|{url:string,render:string}) 142 | const cfg: any = args[0]; 143 | return function(target: Function, methodName: string, desc: object) { 144 | const rfc: _Types.RouterForClz = getRouterForClz(target); 145 | rfc[`set${md}`](methodName, cfg); 146 | }; 147 | } else 148 | /* istanbul ignore if */ 149 | if (args.length === 3) { // @Get 150 | const rfc: _Types.RouterForClz = getRouterForClz(args[0]); 151 | const meta = rfc.getMethodMeta(args[1] as string); 152 | if (meta) { 153 | rfc[`set${md}`](args[1] as string, meta.rpp); 154 | } else { 155 | throw new Error(`${md} decorator's param error.`); 156 | } 157 | } 158 | } 159 | 160 | function getRouterForClz(target) { 161 | const fn = target.constructor; 162 | return routerPtnBindings.get(fn) || (routerPtnBindings.set(fn, new _Types.RouterForClz(() => { 163 | return routerPathBindings.get(fn); 164 | })).get(fn)); 165 | } 166 | -------------------------------------------------------------------------------- /src/web/config.ts: -------------------------------------------------------------------------------- 1 | import { RouteCfgAssets } from "./type"; 2 | 3 | export let Start = new (class { 4 | private _importPath: string; 5 | set importPath(_importPath: string) { 6 | this._importPath = _importPath; 7 | } 8 | 9 | get importPath(): string { 10 | return this._importPath; 11 | } 12 | 13 | private _port: number = 8080; 14 | set port(_port: number) { 15 | this._port = _port; 16 | } 17 | 18 | get port(): number { 19 | return this._port; 20 | } 21 | }); 22 | 23 | export let Router = new (class { 24 | private _assets: RouteCfgAssets; 25 | 26 | set assets(_assets: RouteCfgAssets) { 27 | this._assets = _assets; 28 | } 29 | 30 | get assets(): RouteCfgAssets { 31 | return this._assets; 32 | } 33 | 34 | private _threshold = 2048; 35 | 36 | set gZipThreshold(_threshold: number) { 37 | this._threshold = _threshold; 38 | } 39 | 40 | get gZipThreshold(): number { 41 | return this._threshold; 42 | } 43 | 44 | private _errorProcessor: Function; 45 | 46 | set errorProcessor(errorProcessor: Function) { 47 | this._errorProcessor = errorProcessor; 48 | } 49 | 50 | get errorProcessor(): Function { 51 | return this._errorProcessor; 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /src/web/main.ts: -------------------------------------------------------------------------------- 1 | import * as util from "util"; 2 | import { Stream } from "stream"; 3 | import { Container } from "@rockerjs/core"; 4 | import { init, Logger, _Tracelocal } from "@rockerjs/common"; 5 | import * as co from "co"; 6 | import * as Application from "koa"; 7 | import * as compress from "koa-compress"; 8 | import "zone.js"; 9 | import { CONTAINER_TAG, CONTROLLER } from "../const"; 10 | import { FilterException } from "../errors/filter.exception"; 11 | import midRouter from "./router"; 12 | import * as Util from "../util"; 13 | import * as _Types from "./type"; 14 | import { ReqMethodParamType, RouterConfig, RouterMap, MVCError, RenderWrap } from "./type"; 15 | import { Start, Router } from "./config"; 16 | import { routerPtnBindings, routerPathBindings } from "./annotation"; 17 | 18 | interface IConfigParam { 19 | port?: number; 20 | gZipThreshold?: number; 21 | } 22 | 23 | const routerReg: RouterConfig & {all: RouterMap} = {all: {}}; 24 | 25 | export class RedirectResp extends _Types.ResponseWrap { 26 | 27 | private _url: string; 28 | /** 29 | * Response header's code,301\302(default) 30 | */ 31 | private _code: 302|301 = 302; 32 | 33 | constructor(url: string) { 34 | super(); 35 | this._url = url; 36 | } 37 | 38 | get url() { 39 | return this._url; 40 | } 41 | 42 | get code() { 43 | return this._code; 44 | } 45 | 46 | set code(code: 302|301) { 47 | this._code = code; 48 | } 49 | } 50 | 51 | export class DownloadResp extends _Types.ResponseWrap { 52 | private _stream: Stream; 53 | private _name: string; 54 | 55 | constructor(name: string, stream: Stream) { 56 | super(); 57 | this._name = name; 58 | this._stream = stream; 59 | } 60 | 61 | get name() { 62 | return this._name; 63 | } 64 | 65 | get stream() { 66 | return this._stream; 67 | } 68 | } 69 | 70 | /** 71 | * Render for return 72 | */ 73 | export class RenderResp extends RenderWrap { 74 | private _name: string; 75 | private _model: object; 76 | 77 | /** 78 | * Constructor 79 | * @param name The key in render description of 80 | * @param model 81 | */ 82 | constructor(name: string, model: object) { 83 | super(); 84 | this._name = name; 85 | this._model = model; 86 | } 87 | 88 | get name(): string { 89 | return this._name; 90 | } 91 | 92 | get model(): object { 93 | return this._model; 94 | } 95 | } 96 | 97 | // -------------------------------------------------------------------------------------------- 98 | const koaMidAry: Application.Middleware[] = []; 99 | 100 | export function pipe(midware: Application.Middleware) { 101 | if (!midware) { 102 | throw new FilterException("The midware for pipe should be a Promise or Generator function"); 103 | } 104 | koaMidAry.push(midware); 105 | return { 106 | route, 107 | pipe, 108 | start, 109 | }; 110 | } 111 | 112 | export function config(config: RouterConfig) { 113 | Router.assets = config.assets; 114 | Router.gZipThreshold = config.gZipThreshold || Router.gZipThreshold; // Default value is Router.gZipThreshold 115 | Router.errorProcessor = config.errorProcessor; 116 | 117 | return Object.assign(route, { pipe, start}); 118 | } 119 | 120 | export function route(modules: any[]) 121 | : { 122 | pipe: Function, 123 | start: Function, 124 | (routerMap: _Types.RouterMap | RouterConfig): { 125 | pipe: Function, 126 | start: Function, 127 | }, 128 | } { 129 | const routerMap = {}; 130 | // set controller 131 | Container.getTypedHashmap().get(CONTAINER_TAG.CONTROLLER_TAG).forEach((v, constructor) => { 132 | const clazzName = constructor.name.substring(0, 1).toLowerCase() + constructor.name.substring(1); 133 | const md = getModule(modules, constructor); 134 | if (!md) { 135 | throw new Error(`No file for Controller ${clazzName} defined.`); 136 | } 137 | 138 | const requestMapping = Container.getObject>(clazzName).get(CONTROLLER.REQUEST_MAPPING); 139 | routerPathBindings.set(constructor, md["filename"]); 140 | routerMap[requestMapping] = constructor; 141 | }); 142 | routerReg["all"] = routerMap as any; 143 | 144 | // Merge all 145 | routerPtnBindings.forEach(function(clzReg, fna) { 146 | routerPathBindings.forEach(function(fnPath, fnb) { 147 | // Notice,here may be an error,if more than one parent inherit here 148 | /* istanbul ignore if */ 149 | if (fna.isPrototypeOf(fnb)) { 150 | const rtc: _Types.RouterForClz = routerPtnBindings.get(fnb); 151 | if (rtc) { 152 | rtc.setParent(clzReg); // Process inherit 153 | } else { 154 | routerPtnBindings.set(fnb, clzReg); 155 | } 156 | routerPtnBindings.delete(fna); 157 | routerPathBindings.set(fna, routerPathBindings.get(fnb)); 158 | } 159 | }); 160 | }); 161 | 162 | const rn: any = { 163 | pipe, 164 | start, 165 | }; 166 | return rn; 167 | } 168 | 169 | const pluginAry: { (input: Types.Pluginput): void }[] = []; 170 | 171 | /** 172 | * Startup MVC container 173 | * @param {object} Configuration object 174 | */ 175 | function start(config: IConfigParam = { 176 | port: Start.port, 177 | }): { plugin: Function, server: Application} { 178 | if (typeof config["port"] !== "number") { 179 | throw new _Types.MVCError("Start server error, server port expect for start config."); 180 | } 181 | Start.port = config.port; 182 | 183 | // Router middleware 184 | const rfn: Function = midRouter(routerReg, routerPtnBindings); 185 | 186 | // Compress middleware 187 | const cfn: Function = compress({threshold: Router.gZipThreshold}); 188 | 189 | koaMidAry.push(async function(context: Application.Context, next) { 190 | await rfn(context, next); 191 | await cfn(context, next); // GZip 192 | }); 193 | 194 | let server = null; 195 | 196 | setImmediate(() => { 197 | Logger.info(`Server(${Util.getLocalIp()}) starting ...`); 198 | 199 | // Startup plugins 200 | if (pluginAry.length > 0) { 201 | const ref: Types.Pluginput = new Map(); 202 | routerPtnBindings.forEach((v, k) => { 203 | const tv = new Map(); 204 | ref.set(k, tv); 205 | v["methodReg"].forEach((mr) => { 206 | mr.forEach((rp) => { 207 | tv.set(rp.urlPattern, rp); 208 | }); 209 | }); 210 | }); 211 | 212 | pluginAry.forEach((pl) => { 213 | Logger.info(`Starting plugin ${pl} ...`); 214 | pl(ref); 215 | }); 216 | } 217 | 218 | try { 219 | // Startup koa 220 | const koa: Application = new Application(); 221 | server = koa; 222 | if (koaMidAry.length > 0) { 223 | koaMidAry.forEach((mid, index) => { 224 | koa.use(async function(context: Application.Context, next) { 225 | if (index === 0) { 226 | const zone = Zone.current.fork({ 227 | name: "koa-context", 228 | properties: { 229 | context, 230 | store: {}, // Cache {key,value} for an request trace 231 | }, 232 | }); 233 | context["_zone"] = zone; 234 | } 235 | await new Promise((resolve, reject) => { 236 | context["_zone"].run(async function() { 237 | try { 238 | if (Util.isGeneratorFunction(mid) || Util.isGenerator(mid)) { 239 | await co(mid.call(context, next)); 240 | } else { 241 | await mid(context, next); 242 | } 243 | resolve(); 244 | } catch (ex) { 245 | reject(ex); 246 | } 247 | }); 248 | }); 249 | }); 250 | }); 251 | } 252 | 253 | // Init global Tracelocal 254 | init({ 255 | Tracelocal() { 256 | return new class extends _Tracelocal { 257 | get id() { 258 | try { 259 | return Zone.current.get("context").request.header[_Types.TRACE_ID_KEY_IN_HEADER]; 260 | } catch (ex) { 261 | throw new _Types.MVCError(`Get trace id error\n${ex}`, 500); 262 | } 263 | } 264 | 265 | public get(key: string) { 266 | const r = Zone.current.get("store")[key]; 267 | return r !== undefined ? r : Zone.current.get("context")[key]; 268 | } 269 | 270 | public set(key: string, value: any): void { 271 | Zone.current.get("store")[key] = value; 272 | } 273 | }(); 274 | }, 275 | }); 276 | 277 | koa.context.onerror = onKoaErr; 278 | koa.listen(config.port, "0.0.0.0"); 279 | 280 | Logger.info(bootstrapMsg()); 281 | 282 | Logger.info(`Server(${Util.getLocalIp()}) start completed, listening on port ${config.port} ...`); 283 | 284 | process.on("uncaughtException", function(err) { 285 | Logger.error(err); 286 | }); 287 | 288 | process.on("unhandledRejection", function(reason, p) { 289 | Logger.error(`unhandled rejection: ${p} reason: ${reason}`); 290 | }); 291 | } catch (ex) { 292 | Logger.error("Start server error.\n"); 293 | Logger.error(ex); 294 | throw ex; 295 | } 296 | }); 297 | 298 | return { 299 | plugin, 300 | server, 301 | }; 302 | } 303 | 304 | export function plugin(pluginFn: {(input: Types.Pluginput): void}): {plugin: Function} { 305 | if (util.isFunction(pluginFn)) { 306 | pluginAry.push(pluginFn); 307 | } else { 308 | throw new _Types.MVCError(`The Plugin must be a function.`); 309 | } 310 | return { 311 | plugin, 312 | }; 313 | } 314 | 315 | export namespace Const { 316 | export const Assets: string = "Assets"; 317 | } 318 | 319 | export namespace Types { 320 | export type Pluginput = Map 329 | >; 330 | } 331 | 332 | // -------------------------------------------------------------------------------------------- 333 | 334 | function bootstrapMsg() { 335 | const startMsg = []; 336 | if (Router.errorProcessor) { 337 | startMsg.push(` Router errorProcessor:`); 338 | startMsg.push(` ${Router.errorProcessor}\n`); 339 | } 340 | 341 | if (Router.assets) { 342 | startMsg.push(` Router assets:`); 343 | startMsg.push(` ${JSON.stringify(Router.assets)}\n`); 344 | } 345 | 346 | if (routerReg.all) { 347 | startMsg.push(` Router mappings:`); 348 | const all = routerReg.all; 349 | for (const rootUrl in all) { 350 | const tv = routerPtnBindings.get(all[rootUrl]); 351 | if (tv) { 352 | startMsg.push(` ${rootUrl} => "${tv.fnPath()}"`); 353 | startMsg.push(`${tv.toString(8)}`); 354 | } else { 355 | startMsg.push(` ${rootUrl}:None`); 356 | } 357 | } 358 | } 359 | return startMsg.join("\n"); 360 | } 361 | 362 | function onKoaErr(err: any) { 363 | if (!err) { 364 | return; 365 | } 366 | const th = this; 367 | this["_zone"].run(function() { 368 | // wrap non-error object 369 | if (!(err instanceof Error)) { 370 | const newError: any = new Error(`non-error thrown: ${err}`); 371 | // err maybe an object, try to copy the name, message and stack to the new error instance 372 | if (err) { 373 | if (err.name) { 374 | newError.name = err.name; 375 | } 376 | if (err.message) { 377 | newError.message = err.message; 378 | } 379 | if (err.stack) { 380 | newError.stack = err.stack; 381 | } 382 | if (err.status) { 383 | newError.status = err.status; 384 | } 385 | if (err.headers) { 386 | newError.headers = err.headers; 387 | } 388 | } 389 | err = newError; 390 | } 391 | 392 | let errCode = typeof err["getCode"] === "function" ? err["getCode"]() : 500; 393 | let content: string; 394 | if (Router.errorProcessor) { 395 | content = Router.errorProcessor(err); 396 | if (typeof (content) === "boolean" && !content) { 397 | return; 398 | } 399 | errCode = 200; // status code is 200 when errorProcessor exist 400 | } 401 | 402 | th.response.status = errCode; 403 | if (content !== undefined && content !== null) { 404 | let data: string; 405 | if (typeof (content) === "object") { 406 | th.set("Content-Type", "application/json;charset=utf-8"); 407 | data = JSON.stringify(content); 408 | } else { 409 | th.set("Content-Type", "text/html"); 410 | data = content; 411 | } 412 | th.res.end(data); 413 | } else { 414 | th.set("Content-Type", "text/html"); 415 | th.res.end("

" + errCode + "

" + "
" + err + "
"); 416 | } 417 | setTimeout(function() { 418 | if (err.stack) { 419 | Logger.error(err.stack); 420 | } else if (err.message) { 421 | Logger.error(err.message); 422 | } else { 423 | Logger.error("Unknown error occurred."); 424 | } 425 | }); 426 | }); 427 | } 428 | 429 | /** 430 | * Get it"s defined module,Notice! here may be an error 431 | * @param md 432 | * @param {Function} clz 433 | * @returns [module,subClass] 434 | */ 435 | function getModule(modules, clz: Function): Promise { 436 | if (Array.isArray(modules)) { 437 | for (let i = 0, len = modules.length; i < len; i++) { 438 | const md = require("module")._cache[modules[i]]; 439 | const _exports = md.exports; 440 | for (const k in _exports) { 441 | if (_exports.hasOwnProperty(k)) { 442 | if (clz === _exports[k]) { 443 | return md; 444 | } 445 | } 446 | } 447 | if (clz === _exports) { 448 | return md; 449 | } 450 | } 451 | } 452 | return null; 453 | } 454 | -------------------------------------------------------------------------------- /src/web/router.ts: -------------------------------------------------------------------------------- 1 | import Application = require("koa"); 2 | import { Logger } from "@rockerjs/common"; 3 | import * as Types from "./type"; 4 | let routerReg; 5 | let routerPtnBindings: Map; 6 | import * as mime from "mime"; 7 | import * as FS from "fs"; 8 | import * as PATH from "path"; 9 | 10 | import * as Util from "../util"; 11 | import { Router } from "./config"; 12 | import { RouterMap, RouterConfig } from "./type"; 13 | import { Stream } from "stream"; 14 | import { DownloadResp, RedirectResp, RenderResp } from "./main"; 15 | 16 | const assetsPt = /\.(js|map|css|less|png|jpg|jpeg|gif|bmp|ico|webp|html|htm|eot|svg|ttf|woff|mp4|mp3)$/i; 17 | 18 | export default function(_routerMap, _routerPtnBindings: Map) { 19 | routerPtnBindings = _routerPtnBindings; 20 | routerReg = _routerMap; 21 | const urlSearcher = searchPtn(routerReg), assets = proAssets(), 22 | fnAssets = function(url, context: Application.Context) { 23 | if (assetsPt.test(url)) { // For assets 24 | assets(url, context); 25 | return true; 26 | } 27 | }, fnRouter = async function(url, context: Application.Context, bt: number) { 28 | const ptUrl = urlSearcher(url); // The url matched 29 | if (ptUrl) { 30 | const rw: FunctionConstructor = routerReg.all[ptUrl] as FunctionConstructor; 31 | if (rw) { 32 | const rfc: Types.RouterForClz = routerPtnBindings.get(rw); 33 | if (rfc) { // have decorators for router 34 | await invoke(context, ptUrl, url, rfc, rw); 35 | } 36 | } 37 | Logger.info(`[Rocker-mvc]Request ${context.request.url} costed ${new Date().getTime() - bt} ms.`); 38 | return true; 39 | } 40 | }; 41 | return async function(context: Application.Context, next) { 42 | let url: string = context.request.url; 43 | if (Util.isEmpty(url)) { 44 | throw new Types.MVCError("No url found", 404); 45 | } 46 | 47 | const bt = new Date().getTime(); 48 | url = url.replace(/\?[\s\S]*/ig, ""); 49 | 50 | if (Router.assets) { 51 | if (!fnAssets(url, context)) { 52 | if (!await fnRouter(url, context, bt)) { 53 | throw new Types.MVCError(`The request url(${url}) not found.`, 404); 54 | } 55 | } 56 | } else { 57 | if (!await fnRouter(url, context, bt)) { 58 | if (!fnAssets(url, context)) { 59 | throw new Types.MVCError(`The request url(${url}) not found.`, 404); 60 | } 61 | } 62 | } 63 | }; 64 | } 65 | 66 | // Assets 67 | function proAssets() { 68 | const EtagSet = new Set(); 69 | return function(url: string, context) { 70 | let etag = context.headers["if-none-match"]; 71 | if (etag && EtagSet.has(etag)) { 72 | context.response.status = 304; 73 | return; 74 | } 75 | 76 | if (!Router.assets) { 77 | if (!/^\/favicon.ico$/i.test(url)) { // Ignore /favicon.ico 78 | throw new Types.MVCError("No assets configuration in route.", 404); 79 | } else { 80 | return; 81 | } 82 | } 83 | 84 | let folderPath, cacheStrategy: { cache: "Etag" | "Cache-Control" , strategy?: string}; 85 | if (typeof (Router.assets) === "string") { 86 | folderPath = Router.assets; 87 | } else if (typeof (Router.assets) === "object") { 88 | for (const urlPre in Router.assets) { 89 | const to: any = Router.assets[urlPre]; 90 | if (url.startsWith(urlPre)) { 91 | url = url.substring(urlPre.length); 92 | if (url.trim() === "") { 93 | url = urlPre; 94 | } 95 | if (typeof (to) === "object") { 96 | cacheStrategy = to; 97 | folderPath = to.folder; 98 | } else { 99 | folderPath = to; 100 | } 101 | } 102 | } 103 | } 104 | 105 | cacheStrategy = Object.assign({cache: "Etag" }, cacheStrategy); 106 | 107 | if (folderPath) { 108 | const absPath = PATH.join(folderPath, url); 109 | if (!absPath.startsWith(folderPath)) { 110 | throw new Error("Access error."); 111 | } 112 | 113 | const stat = FS.statSync(absPath); 114 | if (stat.isFile()) { 115 | try { 116 | const mt = mime.getType(PATH.basename(absPath)); 117 | 118 | context.response.status = 200; 119 | context.response.set("Content-Type", mt); 120 | if (cacheStrategy.cache === "Etag") { 121 | etag = url + "-" + stat.mtime.getTime().toString(16); 122 | EtagSet.add(etag); 123 | context.response.set("ETag", etag); 124 | } else if (cacheStrategy.cache === "Cache-Control") { 125 | context.response.set("Cache-Control", cacheStrategy.strategy || "public, max-age=604800"); // Default value = a week 126 | } 127 | 128 | context.body = FS.createReadStream(absPath); 129 | } catch (ex) { 130 | if (!/^\/favicon.ico$/i.test(url)) { // Ignore /favicon.ico 131 | throw new Types.MVCError(`The request url(${url}) error.`, 500); 132 | } else { 133 | throw ex; 134 | } 135 | } 136 | return; 137 | } 138 | } 139 | throw new Types.MVCError(`${url} Not found.`, 404); 140 | }; 141 | } 142 | 143 | // Url pattern closure 144 | function searchPtn(_routerMap: RouterConfig & {all: RouterMap}) { 145 | let urlPattern: RegExp; 146 | let ts: string = ""; 147 | for (const key in _routerMap.all) { 148 | ts += "|^" + key + "$"; 149 | } 150 | 151 | urlPattern = new RegExp(ts.substring(1), "ig"); 152 | 153 | function recur(_url: string): string { 154 | if (urlPattern) { 155 | const url = _url === "" ? "/" : _url; 156 | let ptAry: RegExpExecArray; 157 | try { 158 | ptAry = urlPattern.exec(url); 159 | } finally { 160 | urlPattern.lastIndex = 0; 161 | } 162 | if (!ptAry) { 163 | const ary: string[] = url.split("/"); 164 | ary.pop(); 165 | if (!ary.length) { 166 | return; 167 | } 168 | const nts = ary.join("/"); 169 | return nts === _url ? undefined : recur(nts); 170 | } else { 171 | return ptAry[0]; 172 | } 173 | } 174 | } 175 | 176 | return recur; 177 | } 178 | 179 | /** 180 | * Invoke a function from router class 181 | * @param {Application.Context} _ctx 182 | * @param {string} _urlRoot 183 | * @param {string} _urlFull 184 | * @param {RouterForClz} routerForClz 185 | * @param {FunctionConstructor} fn 186 | * @returns {Promise} 187 | */ 188 | async function invoke(_ctx: Application.Context, 189 | _urlRoot: string, 190 | _urlFull: string, 191 | routerForClz: Types.RouterForClz, 192 | fn: FunctionConstructor) { 193 | let urlSub = _urlFull.substring(_urlRoot.length); 194 | urlSub = (urlSub.startsWith("/") ? "" : "/") + urlSub; 195 | let pattern: Types.RouterPattern; 196 | let args: any; 197 | 198 | if (_ctx.is("multipart")) { 199 | pattern = routerForClz.getPost(urlSub); 200 | args = ((_ctx.request) as any).body || {}; 201 | } else if (_ctx.request.method === "POST") { 202 | pattern = routerForClz.getPost(urlSub); 203 | args = await getPostArgs(_ctx); 204 | } else if (_ctx.request.method === "GET") { 205 | pattern = routerForClz.getGet(urlSub); 206 | args = _ctx.request.query; 207 | } else if (_ctx.request.method === "HEAD") { 208 | pattern = routerForClz.getHead(urlSub); 209 | args = _ctx.request.query; 210 | } 211 | 212 | if (pattern) { 213 | let instance; 214 | try { 215 | instance = new (fn as FunctionConstructor)(); // new instance 216 | } catch (ex) { 217 | Logger.error(`New class\n\n${fn}\nerror.`); 218 | throw ex; 219 | } 220 | const paramAry = []; 221 | const paramDescAry = routerForClz.getMethodParam(pattern.clzMethod); 222 | if (paramDescAry) { 223 | paramDescAry.forEach((_desc) => { 224 | if (_desc.type === Types.ReqMethodParamType.Normal) { 225 | if (_desc.required && !args[_desc.name]) { 226 | throw new Types.MVCError(`The request param[${_desc.name}] not found.`, 500); 227 | } 228 | paramAry.push(_desc.transformer(args[_desc.name])); 229 | } else if (_desc.type === Types.ReqMethodParamType.Request) { 230 | paramAry.push(_ctx.request); 231 | } else if (_desc.type === Types.ReqMethodParamType.Response) { 232 | paramAry.push(_ctx.response); 233 | } 234 | }); 235 | } 236 | 237 | const rtn = await instance[pattern.clzMethod].apply(instance, paramAry); 238 | if (rtn !== undefined) { 239 | if (rtn instanceof Stream) { // Return an Stream object 240 | _ctx.response.status = 200; 241 | _ctx.body = rtn; 242 | return; 243 | } else if (rtn instanceof RedirectResp) { // For redirect 244 | _ctx.response.status = rtn.code; 245 | _ctx.redirect(rtn.url); 246 | return; 247 | } else if (rtn instanceof DownloadResp) { // For download 248 | const dr: DownloadResp = rtn; 249 | _ctx.response.status = 200; 250 | _ctx.response.attachment(dr.name); // Download file name 251 | _ctx.body = dr.stream; 252 | return; 253 | } else if (typeof (rtn) === "function") { 254 | throw new Types.MVCError(`Object or raw value expected but got \n ${rtn}`); 255 | } 256 | } 257 | 258 | if (pattern.render) { // render by template 259 | if (rtn !== undefined && typeof (rtn) !== "object") { 260 | throw new Types.MVCError(`Object type expected but got \n ${rtn}`); 261 | } 262 | 263 | // Multi view 264 | /* istanbul ignore if */ 265 | if (rtn instanceof RenderResp) { 266 | if (typeof pattern.render !== "object") { 267 | throw new Types.MVCError(`The request url(${_urlFull}, full path: ${_ctx.url}, method: ${_ctx.method}), the render in decorator must be an object.`, 500); 268 | } 269 | const rd = pattern.render[rtn.name]; 270 | if (!rd) { 271 | throw new Types.MVCError(`The request url(${_urlFull}, full path: ${_ctx.url}, method: ${_ctx.method}), render(name:${rtn.name}) not found.`, 404); 272 | } 273 | _ctx.response.status = 200; 274 | _ctx.response.set("Content-Type", "text/html;charset=utf-8"); 275 | _ctx.response.set("Transfer-Encoding", "chunked"); 276 | 277 | rd.forEach(function(_rd) { 278 | const html = renderFn(routerForClz, _rd, rtn.model); 279 | _ctx.res.write(html); 280 | }); 281 | _ctx.res.end(); 282 | } else { 283 | _ctx.response.status = 200; 284 | _ctx.response.set("Content-Type", "text/html;charset=utf-8"); 285 | 286 | if (Array.isArray(pattern.render)) { // string[] for Bigpipe 287 | _ctx.response.set("Transfer-Encoding", "chunked"); 288 | pattern.render.forEach(function(_rd) { 289 | const html = renderFn(routerForClz, _rd, rtn); 290 | _ctx.res.write(html); 291 | }); 292 | _ctx.res.end(); 293 | } else { 294 | throw new Types.MVCError(`The request url(${_urlFull}, full path: ${_ctx.url}, method: ${_ctx.method}), render format error.`, 500); 295 | } 296 | } 297 | } else { 298 | _ctx.response.status = 200; 299 | _ctx.response.set("Content-Type", "application/json;charset=utf-8"); 300 | _ctx.body = typeof (rtn) === "object" ? JSON.stringify(rtn) : rtn; 301 | // _ctx.res.end(); 302 | } 303 | } else { 304 | throw new Types.MVCError(`The request url(${_urlFull}, full path: ${_ctx.url}, method: ${_ctx.method}) not found.`, 404); 305 | } 306 | } 307 | 308 | function renderFn(routerForClz: Types.RouterForClz, 309 | _render: { path: string, compile: Function }, 310 | _model: any) { 311 | try { 312 | const compiler: Function = _render.compile(); // Get template compiler 313 | return compiler(_model || {}); 314 | } catch (ex) { 315 | throw new Types.MVCError(ex); 316 | } 317 | } 318 | 319 | function getPostArgs(context: Application.Context) { 320 | return new Promise((resolve, reject) => { 321 | let pdata = ""; 322 | context.req.addListener("data", (postchunk) => { 323 | pdata += postchunk; 324 | }); 325 | context.req.addListener("end", () => { 326 | let reqArgs; 327 | if (pdata !== "") { 328 | try { 329 | // 针对urlencoded做解析 330 | if (pdata.trim().startsWith("{")) { 331 | reqArgs = (new Function("", `return ${pdata}`))(); 332 | } else { 333 | const pary = pdata.split("&"); 334 | if (pary && pary.length > 0) { 335 | reqArgs = {}; 336 | pary.forEach(function(_p) { 337 | let tary = _p.split("="); 338 | if (context.get("content-type").indexOf("application/x-www-form-urlencoded") !== -1) { 339 | tary = tary.map( (d) => { 340 | d = d.replace(/\+/g, " "); 341 | d = decodeURIComponent(d); 342 | return d; 343 | }); 344 | } 345 | if (tary && tary.length === 2) { 346 | reqArgs[tary[0].trim()] = tary[1]; 347 | } 348 | }); 349 | } 350 | } 351 | } catch (e) { 352 | reject(e); 353 | } 354 | } 355 | resolve(reqArgs); 356 | }); 357 | }); 358 | } 359 | -------------------------------------------------------------------------------- /src/web/type.ts: -------------------------------------------------------------------------------- 1 | import * as Util from "../util"; 2 | import * as FS from "fs"; 3 | import * as Path from "path"; 4 | import * as Ejs from "ejs"; 5 | 6 | export const TRACE_ID_KEY_IN_HEADER: string = "X-Trace-Id"; 7 | 8 | export enum ReqMethodType { 9 | Head, Get, Post, Delete, Update, 10 | } 11 | 12 | export enum ReqMethodParamType { 13 | Normal, Request, Response, Context, 14 | } 15 | 16 | export class RouterPattern { 17 | public fnPath: Function; 18 | public urlPattern: string; 19 | /** 20 | * Render code like: 21 | * {'a':'./t.ejs'} or {'a':['./t0.ejs','./t1.ejs']} or './t0.ejs' or ['./t0.ejs','./t1.ejs'] 22 | */ 23 | public render: { path: string, compile: Function}[] | {[index: string]: {path: string, compile: Function}[]}; 24 | public clzMethod: string; 25 | 26 | constructor(fnPath: Function, _clzMethod: string, _config?: RPParam) { 27 | this.fnPath = fnPath; 28 | this.clzMethod = _clzMethod; 29 | if (Util.isEmpty(_config)) { 30 | this.urlPattern = "/"; 31 | } else if (typeof _config === "string") { 32 | this.urlPattern = _config as string; 33 | } else if (typeof _config === "object") { 34 | this.urlPattern = _config["url"] as string; 35 | const renderCfg = _config["render"]; 36 | if (renderCfg) { 37 | const ary = []; 38 | const th = this; 39 | 40 | const proFn = function(_item) { 41 | return { 42 | get path() { 43 | if (typeof _item === "string") { 44 | const rp: string = Path.resolve(Path.dirname(th.fnPath()), _item); 45 | if (!FS.existsSync(rp)) { 46 | throw new MVCError(`The template file[${rp}] not Found.`, 404); 47 | } 48 | return rp; 49 | } else { 50 | throw new MVCError(`The config: render's item type(string|{[index:string]:string}) error.`, 500); 51 | } 52 | }, compile(_name: string) { 53 | const thPath = this.path; 54 | const rp = typeof (thPath) === "object" ? thPath[_name] : thPath; 55 | const content = FS.readFileSync(rp, "utf-8"); // Return template's content 56 | return Ejs.compile(content, { filename: rp }); // option {filename:...} 57 | }, 58 | }; 59 | }; 60 | 61 | // @Get({url:'/multiView',render:{'a':'./tpt.ejs'}}) 62 | if (typeof renderCfg === "object" && !Array.isArray(renderCfg)) { 63 | const rtn = {}; 64 | for (const name in renderCfg) { 65 | const tary = []; 66 | ((Array.isArray(renderCfg[name]) ? renderCfg[name] : [renderCfg[name]]) as string[]) 67 | .forEach((_tpt) => tary.push(proFn(_tpt))); 68 | rtn[name] = tary; 69 | } 70 | this.render = rtn; 71 | return; 72 | } 73 | 74 | [].concat(Array.isArray(renderCfg) ? renderCfg : [renderCfg]).forEach((_tpt) => ary.push(proFn(_tpt))); 75 | this.render = ary; 76 | } 77 | } 78 | } 79 | } 80 | 81 | interface RenderDesc { 82 | url: string; 83 | render?: string | string[] | {[index: string]: string | string[]}; 84 | } 85 | 86 | export type ServerFacade = { 87 | config: Function, 88 | }; 89 | 90 | export declare type RPParam = RenderDesc | string; 91 | 92 | export declare type RouterParamType = {name: string} | {required: boolean} | {name: string, required: boolean} | string; 93 | 94 | export type MethodParams = {index: number, name: string, type: ReqMethodParamType, transformer: Function, required: boolean}; 95 | 96 | /** 97 | * Router's register 98 | */ 99 | export class RouterForClz { 100 | public fnPath: Function; // clz's module 101 | 102 | constructor(fnPath: Function) { 103 | this.fnPath = fnPath; 104 | } 105 | 106 | public regMethodParam(_name: string, _index: number, _type: ReqMethodParamType, _cfg: RouterParamType, _transformer: Function) { 107 | let mp: MethodParams[] = this.paramReg.get(_name); 108 | if (!mp) { 109 | mp = new Array(); 110 | this.paramReg.set(_name, mp); 111 | } 112 | const name: string = typeof (_cfg) === "object" ? _cfg["name"] : _cfg; 113 | const required: boolean = typeof (_cfg ) === "object" ? _cfg["required"] : false; // default value is false 114 | mp.push({index: _index, type: _type, name, transformer: _transformer, required}); 115 | mp.sort((p, n) => { 116 | return p.index - n.index; 117 | }); 118 | } 119 | 120 | public getMethodMeta(_methodName: string) { 121 | return this.methodMeta.get(_methodName); 122 | } 123 | 124 | public getMethodParam(_clzMethod: string) { 125 | const rtn = this.paramReg.get(_clzMethod); 126 | return rtn ? rtn : this.parent ? this.parent.getMethodParam(_clzMethod) : undefined; 127 | } 128 | 129 | public setHead(_clzMethod: string, _config?: RPParam): void { 130 | this.setter(ReqMethodType.Head, _clzMethod, _config); 131 | } 132 | 133 | public getHead(_url: string): RouterPattern { 134 | return this.getter(ReqMethodType.Head, _url); 135 | } 136 | 137 | public setGet(_clzMethod: string, _config?: RPParam): void { 138 | this.setter(ReqMethodType.Get, _clzMethod, _config); 139 | } 140 | 141 | public getGet(_url: string): RouterPattern { 142 | return this.getter(ReqMethodType.Get, _url); 143 | } 144 | 145 | public setPost(_clzMethod: string, _config?: RPParam): void { 146 | this.setter(ReqMethodType.Post, _clzMethod, _config); 147 | } 148 | 149 | public getPost(_url: string): RouterPattern { 150 | return this.getter(ReqMethodType.Post, _url); 151 | } 152 | 153 | public setParent(parent: RouterForClz) { 154 | this.parent = parent; 155 | } 156 | 157 | public toString(blanks: number): string { 158 | const rtn = []; 159 | const ss = new Array(blanks).join(" "); 160 | this.methodReg.forEach((value, reqType) => { 161 | value.forEach((routerPattern, url) => { 162 | const renderStr = []; 163 | if (routerPattern.render) { 164 | if (Array.isArray(routerPattern.render)) { 165 | routerPattern.render.forEach((rp) => { 166 | renderStr.push(rp.path); 167 | }); 168 | } else { 169 | renderStr.push(JSON.stringify(routerPattern.render)); 170 | } 171 | } 172 | rtn.push(`${ss}${ReqMethodType[reqType].toUpperCase()} ${url} => {function:"${routerPattern.clzMethod}"` + 173 | (renderStr.length > 0 ? `,render:"${renderStr.join(",")}"}` : "}")); 174 | }); 175 | }); 176 | return rtn.join("\n"); 177 | } 178 | 179 | private parent: RouterForClz; 180 | 181 | private paramReg: Map = new Map(); 182 | 183 | private methodReg: Map> = new Map>(); 184 | 185 | private methodMeta: Map = new Map(); 186 | 187 | private getter(_reqType: ReqMethodType, _url: string) { 188 | const tg = this.methodReg.get(_reqType); 189 | if (tg) { 190 | const rtn = tg.get(_url); 191 | if (rtn) { 192 | return rtn; 193 | } 194 | } 195 | return this.parent ? this.parent.getter(_reqType, _url) : undefined; 196 | } 197 | 198 | private setter(_reqType: ReqMethodType, _clzMethod: string, _config: RPParam) { 199 | let tg = this.methodReg.get(_reqType); 200 | if (!tg) { 201 | tg = new Map(); 202 | this.methodReg.set(_reqType, tg); 203 | } 204 | const rp: RouterPattern = new RouterPattern(this.fnPath, _clzMethod, _config); 205 | tg.set(rp.urlPattern, rp); 206 | const treg = this.methodMeta.get(_clzMethod); 207 | if (treg) { 208 | treg.types.push(_reqType); 209 | } else { 210 | this.methodMeta.set(_clzMethod, {rpp: _config, types: [_reqType]}); 211 | } 212 | } 213 | } 214 | 215 | export declare type RouterMap = {[index: string]: Function}; 216 | export declare type RouteCfgAssets = string | {[index: string]: string | {folder: string, cache?: "Etag" | "Cache-Control" | "None", strategy?: string}}; 217 | export declare type RouterConfig = { 218 | renderStart?: string, 219 | renderEnd?: string, 220 | gZipThreshold?: number, // GZip threadhold number 221 | assets?: RouteCfgAssets, // Assets folder path 222 | errorProcessor?: Function, 223 | }; 224 | 225 | export class MVCError extends Error { 226 | private code: number; 227 | 228 | constructor(_msg: string, _code: number = 500) { 229 | super(_msg); 230 | this.code = _code; 231 | } 232 | 233 | public getCode(): number { 234 | return this.code; 235 | } 236 | } 237 | 238 | export class ResponseWrap { 239 | 240 | } 241 | 242 | export class RenderWrap { 243 | 244 | } 245 | -------------------------------------------------------------------------------- /test/app/assets/favicon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidian-inc/rockerjs-mvc/03d0843bb600e6f82c38efdcda71ba4c7732d389/test/app/assets/favicon.jpg -------------------------------------------------------------------------------- /test/app/component/db.ts: -------------------------------------------------------------------------------- 1 | // middleware starter 2 | import * as db from "@rockerjs/dao"; 3 | import { Component, AbstractComponent } from '../../../index' 4 | 5 | @Component 6 | export class Mysql extends AbstractComponent { 7 | static Module: typeof db; 8 | public name: string; 9 | constructor() { 10 | super(); 11 | this.name = 'mysql' 12 | } 13 | 14 | // 注入配置信息 15 | // 初始化 16 | async start(config) { 17 | db.start([{ 18 | host: config.host, 19 | user: config.user, 20 | password: config.password, 21 | port: config.port, 22 | database: config.database, 23 | sqlPath: config.resourcePath 24 | }]); 25 | 26 | Mysql.Module = db; 27 | return db; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/app/config/app.config: -------------------------------------------------------------------------------- 1 | ; this comment is being ignored 2 | 3 | [application] 4 | name=test 5 | other[]=123 6 | other[]=456 7 | 8 | [filter:auth] 9 | args[]=(uploadProfile)|(assets)|(user)|(\/home\/(dontNeedAuth|error|mysql|666666|head|redirect))|(\/build\/\d+\/)\S+ 10 | args[]=auth 11 | 12 | [filter:trace] 13 | 14 | [mysql] 15 | ;starter=@rockerjs/mysql-starter 16 | host=localhost 17 | user=NODE_PERF_APP_user 18 | port=32893 19 | database=NODE_PERF_APP 20 | password=root 21 | resourcePath=/test/app/repository/resource 22 | ;resourcePath=/dist/test/app/repository/resource 23 | 24 | [midLogger] 25 | starter=@rockerjs/midlog-starter 26 | env=dev 27 | 28 | [rpc] 29 | starter=@rockerjs/rpc-starter 30 | interfaces[]=/test/app/interfaces/httpInterface 31 | -------------------------------------------------------------------------------- /test/app/config/app.dev.config: -------------------------------------------------------------------------------- 1 | ; this comment is being ignored 2 | 3 | [application] 4 | env=dev 5 | 6 | [filter:wdr] 7 | args[]=(uploadProfile)|(\/build\/\d+\/)\S+ 8 | args[]=wdr 9 | 10 | [filter:trace] 11 | 12 | [mysql] 13 | starter=@rockerjs/mysql-starter 14 | host=xxx 15 | user=xxx 16 | port=3308 17 | database=NODE_PERF_APP 18 | password=root 19 | resourcePath=../../../../repository/resource 20 | 21 | [redis] 22 | starter=@rockerjs/redis-starter 23 | env=daily 24 | name=vstudio 25 | -------------------------------------------------------------------------------- /test/app/controller/HomeController.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"HomeController.js","sourceRoot":"","sources":["HomeController.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA,yCAAwC;AACxC,uCAAgE;AAChE,0CAA8C;AAG9C,IAAa,cAAc,GAA3B;IAKU,IAAI,CAAgB,IAAY,EAAmB,MAAc;;YACnE,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,gBAAgB,EAAE,CAAC;YAClD,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC;YAC5C,MAAM,CAAC;gBACH,GAAG,EAAE,aAAa;gBAClB,CAAC;gBACD,CAAC;gBACD,IAAI;gBACJ,MAAM;aACT,CAAA;QACL,CAAC;KAAA;IAGK,YAAY,CAAU,GAAG;;YAC3B,IAAI,GAAG,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,gBAAgB,EAAE,CAAC;YACpD,MAAM,CAAC,GAAG,CAAC;QACf,CAAC;KAAA;IAGK,KAAK,CAAgB,IAAY,EAAmB,MAAc;;YACpE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAA;QAC1C,CAAC;KAAA;CAEJ,CAAA;AA1BG;IADC,aAAM;8BACM,kBAAW;mDAAC;AAGzB;IADC,SAAG,CAAC,EAAC,GAAG,EAAE,IAAI,EAAC,CAAC;IACL,WAAA,WAAK,CAAC,MAAM,CAAC,CAAA,EAAgB,WAAA,WAAK,CAAC,QAAQ,CAAC,CAAA;;;;0CAUvD;AAGD;IADC,SAAG,CAAC,EAAC,GAAG,EAAE,eAAe,EAAC,CAAC;IACR,WAAA,aAAO,CAAA;;;;kDAG1B;AAGD;IADC,SAAG,CAAC,EAAC,GAAG,EAAE,QAAQ,EAAC,CAAC;IACR,WAAA,WAAK,CAAC,MAAM,CAAC,CAAA,EAAgB,WAAA,WAAK,CAAC,QAAQ,CAAC,CAAA;;;;2CAExD;AA1BQ,cAAc;IAD1B,gBAAU,CAAC,OAAO,CAAC;GACP,cAAc,CA4B1B;AA5BY,wCAAc"} -------------------------------------------------------------------------------- /test/app/controller/HomeController.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@rockerjs/core'; 2 | import { Controller, Head, Get, Post, Param, Request, Response, RedirectResp } from '../../../index'; 3 | import { MainService } from '../service/main'; 4 | 5 | @Controller("/home") 6 | export class HomeController { 7 | @Inject 8 | mainService: MainService; 9 | 10 | @Get({ url: '/user', render: '../templates/user.ejs' }) 11 | async render() { 12 | return { 13 | user: 'foo', 14 | ejs: { 15 | user: 'foo' 16 | } 17 | } 18 | } 19 | 20 | @Get({ url: '/mysql' }) 21 | @Post({ url: '/mysql' }) 22 | async home(@Param("name") name: string, @Param("person") person: object, @Request req, @Response res) { 23 | let a = await this.mainService.sendMsgThenquery(); 24 | let b = await this.mainService.queryCache(); 25 | console.log(req, res); 26 | return { 27 | tag: 'hello world', 28 | a, 29 | b, 30 | name, 31 | person 32 | } 33 | } 34 | 35 | @Get({ url: '/dontNeedAuth' }) 36 | async dontNeedAuth() { 37 | return { 38 | foo: 'bar' 39 | } 40 | } 41 | 42 | @Get({ url: '/needAuth' }) 43 | async needAuth(@Param("name") name: string, @Param("person") person: object) { 44 | return { 45 | name, 46 | person 47 | } 48 | } 49 | 50 | 51 | @Get({ url: '/error' }) 52 | async error(@Param("name") name: string, @Param("person") person: object) { 53 | throw new Error('test errorprocessor') 54 | } 55 | 56 | @Head({ url: '/head' }) 57 | async head() { 58 | return { 59 | result: "head echo" 60 | }; 61 | } 62 | 63 | @Get({ url: '/redirect' }) 64 | async redirect() { 65 | let rp: RedirectResp = new RedirectResp("https://www.weidian.com/"); 66 | rp.code = 301; 67 | return rp; 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /test/app/filter/Auth.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Auth.js","sourceRoot":"","sources":["Auth.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,kDAA0C;AAC1C,uCAAuD;AAEvD,IAAI,OAAO,GAAG,IAAI,CAAC;AAGnB,IAAa,GAAG,GAAhB,SAAiB,SAAQ,oBAAc;IAEnC,IAAI,CAAC,IAAc;QACf,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC;IACzC,CAAC;IAED,gCAAgC;IAC1B,QAAQ,CAAC,OAAO,EAAE,IAAI;;YACxB,MAAM,kBAAK,CAAC;gBACR,MAAM,EAAE,IAAI,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA,wCAAwC;aAC3E,CAAC,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;QACrB,CAAC;KAAA;IAED,OAAO;QACH,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;IACrC,CAAC;CACJ,CAAA;AAjBY,GAAG;IADf,YAAM;GACM,GAAG,CAiBf;AAjBY,kBAAG"} -------------------------------------------------------------------------------- /test/app/filter/Auth.ts: -------------------------------------------------------------------------------- 1 | import { Filter, AbstractFilter } from '../../../index'; 2 | 3 | let pattern = null; 4 | 5 | @Filter 6 | export class Auth extends AbstractFilter { 7 | 8 | 9 | init(args: string[]) { 10 | pattern = args[0]; 11 | console.log('auth filter init', args); 12 | } 13 | 14 | // koa context, this === context 15 | async doFilter(context, next) { 16 | await fakeLogin({ // 前置过滤 17 | ignore: new RegExp(pattern, 'i') // /(uploadProfile)|(\/build\/\d+\/)\S+/i 18 | })(context, next) 19 | } 20 | 21 | destroy() { 22 | console.log('auth filter destroy') 23 | } 24 | } 25 | 26 | 27 | 28 | 29 | type LoginParams = { 30 | ignore?: RegExp 31 | }; 32 | const ckName = 'login-l-u'; 33 | 34 | let fakeLogin = function login(_opt?: LoginParams) { 35 | return async function (ctx, _next) { 36 | let url = ctx.request.url; 37 | 38 | if (_opt && _opt.ignore && _opt.ignore.test(url)) {//ignore service 39 | return await _next(); 40 | } 41 | 42 | let ckv = ctx.cookies.get(ckName) 43 | let mkCtxState = function (_user) { 44 | ctx.state = { 45 | hostName: ctx.req.headers.host, 46 | userAgent: ctx.req.headers['user-agent'], 47 | loginUser: _user,//当前登录用户 48 | attr: function (_key) { 49 | return { 50 | 'login-user': _user 51 | }[_key]; 52 | } 53 | };//传递参数 54 | }; 55 | 56 | if (ckv) {//had login 57 | let json = new Buffer(ckv, 'base64').toString(); 58 | mkCtxState(JSON.parse(json)); 59 | return await _next(); 60 | } else { 61 | ctx.response.redirect("https://example.com"); 62 | return; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/app/filter/Trace.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Trace.js","sourceRoot":"","sources":["Trace.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0CAAmC;AACnC,uCAAuD;AAGvD,IAAa,KAAK,GAAlB,WAAmB,SAAQ,oBAAc;IACrC,IAAI,CAAC,IAAc;QACf,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;IAEK,QAAQ,CAAC,OAAO,EAAE,IAAI;;YACxB,MAAM,gBAAM,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAClC,CAAC;KAAA;IAED,OAAO;QACH,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAA;IACvC,CAAC;CACJ,CAAA;AAZY,KAAK;IADjB,YAAM;GACM,KAAK,CAYjB;AAZY,sBAAK"} -------------------------------------------------------------------------------- /test/app/filter/Trace.ts: -------------------------------------------------------------------------------- 1 | import tracer from '@rockerjs/tracer'; 2 | import { Filter, AbstractFilter } from '../../../index'; 3 | 4 | @Filter 5 | export class Trace extends AbstractFilter { 6 | init(args: string[]) { 7 | console.log('trace filter init', args); 8 | } 9 | 10 | async doFilter(context, next) { 11 | await tracer()(context, next); 12 | } 13 | 14 | destroy() { 15 | console.log('trace filter destroy') 16 | } 17 | } -------------------------------------------------------------------------------- /test/app/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,6BAA6B;AAC7B,yBAAuB;AACvB,uCAAiE;AACjE,yDAAyD;AAGzD,IAAM,GAAG,GAAT,SAAU,SAAQ,yBAAmB;IACpB,iBAAiB,CAAC,MAAM,EAAE,IAAI;;YACvC,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;YACpD,MAAM,CAAC,MAAM,CAAC;gBACV,MAAM,EAAE;oBACJ,SAAS,EAAE;wBACP,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC;wBAC5B,KAAK,EAAE,MAAM;qBAChB;iBACJ;gBACD,cAAc,EAAE,KAAK,CAAC,EAAE;oBACpB,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;oBACtC,MAAM,CAAC;wBACH,MAAM,EAAE;4BACJ,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI;4BAC9C,OAAO,EAAE,KAAK,CAAC,OAAO;yBACzB;qBACJ,CAAC;gBACN,CAAC;aACJ,CAAC,CAAC;QACP,CAAC;KAAA;IAEM,MAAM,CAAO,IAAI,CAAC,IAAc;;YACnC,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;YAEpC,0CAA0C;YAC1C,mBAAmB;YACnB,8BAA8B;YAC9B,kCAAkC;YAClC,6FAA6F;YAC7F,gFAAgF;YAChF,MAAM;YAEN,YAAY;YACZ,gCAAgC;YAChC,4CAA4C;YAC5C,OAAO;QACX,CAAC;KAAA;CACJ,CAAA;AAtCK,GAAG;IADR,iBAAW;GACN,GAAG,CAsCR"} -------------------------------------------------------------------------------- /test/app/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as fs from "fs" 3 | import '../../index' 4 | import { bootstrap } from '../../index' 5 | import { copyFolderRecursiveSync, copyFileSync } from './utils/fs' 6 | import { Application, AbstractApplication } from "../../index"; 7 | 8 | export const app = async () => { 9 | copyFolderRecursiveSync(path.resolve(__dirname, '../../../test/app/config'), path.resolve(__dirname)) 10 | copyFolderRecursiveSync(path.resolve(__dirname, '../../../test/app/assets'), path.resolve(__dirname)) 11 | copyFolderRecursiveSync(path.resolve(__dirname, '../../../test/app/templates'), path.resolve(__dirname)) 12 | copyFolderRecursiveSync(path.resolve(__dirname, '../../../test/app/repository/resource'), path.resolve(__dirname, 'repository')) 13 | copyFileSync(path.resolve(__dirname, '../../../package.json'), path.resolve(__dirname, '../../')) 14 | copyFileSync(path.resolve(__dirname, '../../../package.json'), path.resolve(__dirname)) 15 | await bootstrap(path.resolve(__dirname)); 16 | } 17 | 18 | 19 | @Application 20 | class App extends AbstractApplication { 21 | public async beforeServerStart(server, args) { 22 | console.log('beforeServerStart hook', server, args); 23 | server.config({ 24 | assets: { 25 | '/assets': { 26 | folder: path.join(__dirname, 'assets'), 27 | cache: 'Etag' 28 | }, 29 | }, 30 | errorProcessor: error => { 31 | return { 32 | status: { 33 | code: error.code == undefined ? 1 : error.code, 34 | message: error.message 35 | } 36 | }; 37 | } 38 | }); 39 | } 40 | 41 | public static async main(args: string[]) { 42 | console.log('main bussiness', args); 43 | } 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/app/interfaces/httpInterface.ts: -------------------------------------------------------------------------------- 1 | import { Rpc } from "@rockerjs/rpc-starter"; 2 | let { Resource } = Rpc.Module; 3 | 4 | export default abstract class HttpNpsRequest { 5 | @Resource({ baseUrl: "http://nps.vdian.net", url: '/api/npis/0.0.1/getUserAppInfo' }) 6 | queryYourAppsInfo(): any {} // need auth, in cookie 7 | } 8 | -------------------------------------------------------------------------------- /test/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rockerjs/mvc", 3 | "version": "1.0.1-4", 4 | "description": "A MVC framework based on Rockerjs/core used by node.js", 5 | "author": { 6 | "name": "yangli", 7 | "email": "yl.yuxiu@foxmail.com", 8 | "url": "https://github.com/royalrover" 9 | }, 10 | "scripts": { 11 | "build": "npm run clean && (tsc || true)", 12 | "clean": "rimraf ./dist", 13 | "cover": "npm run build && cross-env NODE_STARTER=rockerjs deco dist/test/test.spec.js && cd dist && cross-env NODE_STARTER=rockerjs istanbul cover _mocha -x src/errors/**/*.js -- test/test.spec.js --timeout 10000 --exit --reporter spec --recursive", 14 | "dev": "npm run clean && tsc -w", 15 | "lint": "tslint ./src/**/*.ts ./src/*.ts", 16 | "prepublish": "npm run build", 17 | "start": "tsc -w", 18 | "test": "npm run build && cross-env NODE_STARTER=rockerjs deco dist/test/test.spec.js", 19 | "test2": "cd test/app/ && cross-env NODE_STARTER=ssss node index.js" 20 | }, 21 | "dependencies": { 22 | "@rockerjs/core": "^1.0.2", 23 | "ejs": "^2.6.1", 24 | "ini": "^1.3.5", 25 | "koa": "^2.7.0", 26 | "koa-compress": "^3.0.0", 27 | "mime": "^2.4.2", 28 | "reflect-metadata": "^0.1.13", 29 | "sb-scandir": "^2.0.0" 30 | }, 31 | "devDependencies": { 32 | "@rockerjs/mysql-starter": "^1.0.0-1", 33 | "@rockerjs/redis-starter": "^1.0.0-1", 34 | "@rockerjs/rocketmq-starter": "^1.0.0-5", 35 | "@rockerjs/rpc-starter": "^1.0.0-2", 36 | "@rockerjs/tsunit": "^1.0.0", 37 | "@types/koa": "^2.0.48", 38 | "@types/node": "^7.10.5", 39 | "@vdian/commons": "^1.1.7", 40 | "@vdian/deco-mocha": "0.0.3", 41 | "@vdian/rocker-wdr": "0.0.4", 42 | "@vdian/tracer": "0.0.16", 43 | "coveralls": "^2.13.0", 44 | "cross-env": "^5.2.0", 45 | "istanbul": "^0.4.5", 46 | "moment": "^2.24.0", 47 | "request": "^2.88.0", 48 | "request-promise": "^4.2.4", 49 | "rimraf": "^2.6.3", 50 | "tslint": "^5.14.0", 51 | "typescript": "^2.7.2" 52 | }, 53 | "keywords": [ 54 | "ioc", 55 | "di", 56 | "javascript", 57 | "typescript", 58 | "node", 59 | "dependency injection", 60 | "dependency inversion", 61 | "inversion of control container", 62 | "AOP", 63 | "Aspect Oriented Program" 64 | ], 65 | "contributors": [ 66 | { 67 | "name": "chemingjun", 68 | "email": "chemingjun@weidian.com" 69 | }, 70 | { 71 | "name": "kangzhe", 72 | "email": "kangzhe@weidian.com" 73 | }, 74 | { 75 | "name": "dingjunjie", 76 | "email": "dingjunjie@weidian.com" 77 | } 78 | ], 79 | "license": "MIT", 80 | "directories": { 81 | "doc": "doc" 82 | }, 83 | "main": "./dist/index.js" 84 | } 85 | -------------------------------------------------------------------------------- /test/app/repository/dao/App_info.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"App_info.js","sourceRoot":"","sources":["App_info.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;;;;;;;;;;;;;;;;;;AAEb,2DAAgD;AAChD,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,qBAAK,CAAC,MAAM,CAAC,CAAC,iBAAiB;AAC3D,8CAA0C;AAE1C,gBAAwB,SAAQ,MAAM;IACrB,GAAG,CAAC,EAAC,KAAK,EAAC,OAAO,EAAC,QAAQ,EAAC,OAAO,EAAC;;YAC7C,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,aAAa,EAAC;gBACrC,KAAK,EAAC,OAAO,EAAC,QAAQ,EAAC,OAAO;aACjC,CAAC,CAAC;YACH,MAAM,CAAC,GAAG,CAAC;QACf,CAAC;KAAA;IAGY,QAAQ;;YACjB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;YACnD,MAAM,CAAC,GAAG,CAAC;QACf,CAAC;KAAA;IAGY,WAAW,CAAC,IAAI;;YACzB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,qBAAqB,EAAE,EAAC,IAAI,EAAC,CAAC,CAAC;YAC1D,MAAM,CAAC,GAAG,CAAC;QACf,CAAC;KAAA;IAGY,qBAAqB,CAAC,IAAI,EAAC,OAAO;;YAC3C,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,+BAA+B,EAAE;gBACxD,QAAQ,EAAE,IAAI;gBACd,OAAO;aACV,CAAC,CAAC;YACH,MAAM,CAAC,GAAG,CAAC;QACf,CAAC;KAAA;IAEY,GAAG,CAAC,KAAK;;YAClB,IAAI,GAAG,EAAC,KAAK,GAAG,KAAK,CAAC;YACtB,IAAG,CAAC;gBACA,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE;oBAChC,KAAK;iBACR,CAAC,CAAC;YACP,CAAC;YAAA,KAAK,CAAA,CAAC,CAAC,CAAC,CAAA,CAAC;gBACN,GAAG,GAAG,CAAC,CAAC;gBACR,KAAK,GAAG,IAAI,CAAC;YACjB,CAAC;YAED,MAAM,CAAC;gBACH,GAAG,EAAE,KAAK;gBACV,MAAM,EAAE,GAAG;aACd,CAAC;QACN,CAAC;KAAA;CAEJ;AArCG;IADC,OAAO,CAAC,kBAAO,CAAC;;;;0CAIhB;AAGD;IADC,OAAO,CAAC,kBAAO,CAAC;;;;6CAIhB;AAGD;IADC,OAAO,CAAC,kBAAO,CAAC;;;;uDAOhB;AA3BL,gCA8CC"} -------------------------------------------------------------------------------- /test/app/repository/dao/App_info.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Mysql } from "../../component/db"; 4 | const { DOBase, Mapping } = Mysql.Module; // 使用 typeof 强转类型 5 | import { AppInfo } from "../dto/App_info"; 6 | 7 | export class AppInfoDao extends DOBase { 8 | public async add({appid,secrete,username,appname}) { 9 | const fId = await this.exe('appInfo:add',{ 10 | appid,secrete,username,appname 11 | }); 12 | return fId; 13 | } 14 | 15 | @Mapping(AppInfo) 16 | public async queryAll() { 17 | const ary = await this.exe('appInfo:queryAll', {}); 18 | return ary; 19 | } 20 | 21 | @Mapping(AppInfo) 22 | public async queryByName(name) { 23 | const ary = await this.exe('appInfo:queryByName', {name}); 24 | return ary; 25 | } 26 | 27 | @Mapping(AppInfo) 28 | public async queryByNameAndAppname(name,appname) { 29 | const ary = await this.exe('appInfo:queryByNameAndAppname', { 30 | username: name, 31 | appname 32 | }); 33 | return ary; 34 | } 35 | 36 | public async del(appid) { 37 | let ret,isErr = false; 38 | try{ 39 | ret = await this.exe('appInfo:del', { 40 | appid 41 | }); 42 | }catch(e){ 43 | ret = e; 44 | isErr = true; 45 | } 46 | 47 | return { 48 | err: isErr, 49 | result: ret 50 | }; 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /test/app/repository/dto/App_info.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"App_info.js","sourceRoot":"","sources":["App_info.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;;;;;;;;;;AACb,2DAAgD;AAChD,IAAI,EAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAC,GAAG,qBAAK,CAAC,MAAM,CAAC;AAC1D,iCAAiC;AAEjC;IAiBW,UAAU,CAAC,UAAU;QACxB,IAAI,CAAC;YACD,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAA;QAC3D,CAAC;QAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACT,MAAM,CAAC,UAAU,CAAC;QACtB,CAAC;IACL,CAAC;CACJ;AAtBG;IADC,MAAM;;mCACG;AAGV;IADC,MAAM;;sCACM;AAGb;IADC,MAAM;;wCACQ;AAGf;IADC,MAAM;;yCACS;AAGhB;IADC,MAAM;;wCACQ;AAGf;IADC,MAAM,CAAC,YAAY,CAAC;;;;yCAOpB;AAvBL,0BAwBC"} -------------------------------------------------------------------------------- /test/app/repository/dto/App_info.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // import { Mysql } from "@rockerjs/mysql-starter"; 3 | import { Mysql } from "../../component/db" 4 | let { Column, DOBase, Mapping, Transaction } = Mysql.Module; 5 | import * as moment from 'moment'; 6 | 7 | export class AppInfo { 8 | @Column 9 | public id; 10 | 11 | @Column 12 | public appid; 13 | 14 | @Column 15 | public secrete; 16 | 17 | @Column 18 | public username; 19 | 20 | @Column 21 | public appname; 22 | 23 | @Column('gmt_create') 24 | public createTime(createTime) { 25 | try { 26 | return moment(createTime).format('YYYY-MM-DD HH:mm:ss') 27 | } catch (e) { 28 | return createTime; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /test/app/repository/resource/App_Info_Map.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 10 | 11 | 14 | 15 | 18 | 19 | 20 | insert into app_info ( appid,secrete,username,appname ) 21 | 22 | #{appid},#{secrete},#{username},#{appname} 23 | 24 | 25 | 26 | 27 | update app_info 28 | 29 | 30 | 31 | ${key} = #{data[key]}, 32 | 33 | 34 | ${key} = #{data[key]} 35 | 36 | 37 | 38 | 39 | 40 | 41 | ${key} = #{info[key]} and 42 | 43 | 44 | ${key} = #{info[key]} 45 | 46 | 47 | 48 | 49 | 50 | 51 | delete from app_info where appid=#{appid} 52 | 53 | 54 | -------------------------------------------------------------------------------- /test/app/service/main.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Inject, Container } from "@rockerjs/core"; 3 | import { AppInfoDao } from "../repository/dao/App_info"; 4 | 5 | export class MainService { 6 | @Inject 7 | db: AppInfoDao 8 | 9 | async sendMsgThenquery() { 10 | let result = await this.db.queryByName('yangli'); 11 | return { 12 | result 13 | } 14 | } 15 | 16 | async queryCache() { 17 | let ret = 8888 18 | return ret; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/app/templates/user.ejs: -------------------------------------------------------------------------------- 1 | <% if (user) { %> 2 |

<%= user %>

3 | <% } %> 4 | 5 | bar 6 | -------------------------------------------------------------------------------- /test/app/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | 4 | export function copyFileSync(source, target) { 5 | 6 | var targetFile = target; 7 | 8 | //if target is a directory a new file with the same name will be created 9 | if (fs.existsSync(target)) { 10 | if (fs.lstatSync(target).isDirectory()) { 11 | targetFile = path.join(target, path.basename(source)); 12 | } 13 | } 14 | 15 | fs.writeFileSync(targetFile, fs.readFileSync(source)); 16 | } 17 | 18 | export function copyFolderRecursiveSync(source, target) { 19 | var files = []; 20 | 21 | //check if folder needs to be created or integrated 22 | var targetFolder = path.join(target, path.basename(source)); 23 | if (!fs.existsSync(targetFolder)) { 24 | fs.mkdirSync(targetFolder); 25 | } 26 | 27 | //copy 28 | if (fs.lstatSync(source).isDirectory()) { 29 | files = fs.readdirSync(source); 30 | files.forEach(function (file) { 31 | var curSource = path.join(source, file); 32 | if (fs.lstatSync(curSource).isDirectory()) { 33 | copyFolderRecursiveSync(curSource, targetFolder); 34 | } else { 35 | copyFileSync(curSource, targetFolder); 36 | } 37 | }); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /test/fakeServer/FakeServer.ts: -------------------------------------------------------------------------------- 1 | // An experimental fake MySQL server for tricky integration tests. Expanded 2 | // as needed. 3 | 4 | var Buffer = require('safe-buffer').Buffer; 5 | var common = require('./common'); 6 | var Charsets = common.Charsets; 7 | var ClientConstants = common.ClientConstants; 8 | var Crypto = require('crypto'); 9 | var Net = require('net'); 10 | var tls = require('tls'); 11 | var Packets = common.Packets; 12 | var PacketWriter = common.PacketWriter; 13 | var Parser = common.Parser; 14 | var Types = common.Types; 15 | var Errors = common.Errors; 16 | var EventEmitter = require('events').EventEmitter; 17 | var Util = require('util'); 18 | 19 | module.exports = FakeServer; 20 | Util.inherits(FakeServer, EventEmitter); 21 | function FakeServer() { 22 | EventEmitter.call(this); 23 | 24 | this._server = null; 25 | this._connections = []; 26 | } 27 | 28 | FakeServer.prototype.listen = function(port, cb) { 29 | this._server = Net.createServer(this._handleConnection.bind(this)); 30 | this._server.listen(port, cb); 31 | }; 32 | 33 | FakeServer.prototype._handleConnection = function(socket) { 34 | var connection = new FakeConnection(socket); 35 | 36 | if (!this.emit('connection', connection)) { 37 | connection.handshake(); 38 | } 39 | 40 | this._connections.push(connection); 41 | }; 42 | 43 | FakeServer.prototype.destroy = function() { 44 | if (this._server._handle) { 45 | // close server if listening 46 | this._server.close(); 47 | } 48 | 49 | // destroy all connections 50 | this._connections.forEach(function(connection) { 51 | connection.destroy(); 52 | }); 53 | }; 54 | 55 | Util.inherits(FakeConnection, EventEmitter); 56 | function FakeConnection(socket) { 57 | EventEmitter.call(this); 58 | 59 | this.database = null; 60 | this.user = null; 61 | 62 | this._socket = socket; 63 | this._ssl = null; 64 | this._stream = socket; 65 | this._parser = new Parser({onPacket: this._parsePacket.bind(this)}); 66 | 67 | this._expectedNextPacket = null; 68 | this._handshakeInitializationPacket = null; 69 | this._handshakeOptions = {}; 70 | 71 | socket.on('data', this._handleData.bind(this)); 72 | } 73 | 74 | FakeConnection.prototype.authSwitchRequest = function authSwitchRequest(options) { 75 | this._sendPacket(new Packets.AuthSwitchRequestPacket(options)); 76 | }; 77 | 78 | FakeConnection.prototype.deny = function deny(message, errno) { 79 | message = message || 'Access Denied'; 80 | errno = errno || Errors.ER_ACCESS_DENIED_ERROR; 81 | this.error(message, errno); 82 | }; 83 | 84 | FakeConnection.prototype.error = function deny(message, errno) { 85 | this._sendPacket(new Packets.ErrorPacket({ 86 | message : (message || 'Error'), 87 | errno : (errno || Errors.ER_UNKNOWN_COM_ERROR) 88 | })); 89 | this._parser.resetPacketNumber(); 90 | }; 91 | 92 | FakeConnection.prototype.handshake = function(options) { 93 | this._handshakeOptions = options || {}; 94 | 95 | var packetOptions = common.extend({ 96 | scrambleBuff1 : Buffer.from('1020304050607080', 'hex'), 97 | scrambleBuff2 : Buffer.from('0102030405060708090A0B0C', 'hex'), 98 | serverCapabilities1 : 512, // only 1 flag, PROTOCOL_41 99 | protocol41 : true 100 | }, this._handshakeOptions); 101 | 102 | this._handshakeInitializationPacket = new Packets.HandshakeInitializationPacket(packetOptions); 103 | 104 | this._sendPacket(this._handshakeInitializationPacket); 105 | }; 106 | 107 | FakeConnection.prototype.ok = function ok() { 108 | this._sendPacket(new Packets.OkPacket()); 109 | this._parser.resetPacketNumber(); 110 | }; 111 | 112 | FakeConnection.prototype._sendAuthResponse = function _sendAuthResponse(got, expected) { 113 | if (expected.toString('hex') === got.toString('hex')) { 114 | this.ok(); 115 | } else { 116 | this.deny('expected ' + expected.toString('hex') + ' got ' + got.toString('hex')); 117 | } 118 | 119 | this._parser.resetPacketNumber(); 120 | }; 121 | 122 | FakeConnection.prototype._sendPacket = function(packet) { 123 | switch (packet.constructor) { 124 | case Packets.AuthSwitchRequestPacket: 125 | this._expectedNextPacket = Packets.AuthSwitchResponsePacket; 126 | break; 127 | case Packets.HandshakeInitializationPacket: 128 | this._expectedNextPacket = Packets.ClientAuthenticationPacket; 129 | break; 130 | case Packets.UseOldPasswordPacket: 131 | this._expectedNextPacket = Packets.OldPasswordPacket; 132 | break; 133 | default: 134 | this._expectedNextPacket = null; 135 | break; 136 | } 137 | 138 | var writer = new PacketWriter(); 139 | packet.write(writer); 140 | this._stream.write(writer.toBuffer(this._parser)); 141 | }; 142 | 143 | FakeConnection.prototype._handleData = function(buffer) { 144 | this._parser.write(buffer); 145 | }; 146 | 147 | FakeConnection.prototype._handleQueryPacket = function _handleQueryPacket(packet) { 148 | var conn = this; 149 | var match; 150 | var sql = packet.sql; 151 | 152 | if ((match = /^SELECT ([0-9]+);?$/i.exec(sql))) { 153 | var num = match[1]; 154 | 155 | this._sendPacket(new Packets.ResultSetHeaderPacket({ 156 | fieldCount: 1 157 | })); 158 | 159 | this._sendPacket(new Packets.FieldPacket({ 160 | catalog : 'def', 161 | charsetNr : Charsets.UTF8_GENERAL_CI, 162 | default : '0', 163 | name : num, 164 | protocol41 : true, 165 | type : Types.LONG 166 | })); 167 | 168 | this._sendPacket(new Packets.EofPacket()); 169 | 170 | var writer = new PacketWriter(); 171 | writer.writeLengthCodedString(num); 172 | this._socket.write(writer.toBuffer(this._parser)); 173 | 174 | this._sendPacket(new Packets.EofPacket()); 175 | this._parser.resetPacketNumber(); 176 | return; 177 | } 178 | 179 | if ((match = /^SELECT CURRENT_USER\(\);?$/i.exec(sql))) { 180 | this._sendPacket(new Packets.ResultSetHeaderPacket({ 181 | fieldCount: 1 182 | })); 183 | 184 | this._sendPacket(new Packets.FieldPacket({ 185 | catalog : 'def', 186 | charsetNr : Charsets.UTF8_GENERAL_CI, 187 | name : 'CURRENT_USER()', 188 | protocol41 : true, 189 | type : Types.VARCHAR 190 | })); 191 | 192 | this._sendPacket(new Packets.EofPacket()); 193 | 194 | var writer = new PacketWriter(); 195 | writer.writeLengthCodedString((this.user || '') + '@localhost'); 196 | this._socket.write(writer.toBuffer(this._parser)); 197 | 198 | this._sendPacket(new Packets.EofPacket()); 199 | this._parser.resetPacketNumber(); 200 | return; 201 | } 202 | 203 | if ((match = /^SELECT SLEEP\(([0-9]+)\);?$/i.exec(sql))) { 204 | var sec = match[1]; 205 | var time = sec * 1000; 206 | 207 | setTimeout(function () { 208 | conn._sendPacket(new Packets.ResultSetHeaderPacket({ 209 | fieldCount: 1 210 | })); 211 | 212 | conn._sendPacket(new Packets.FieldPacket({ 213 | catalog : 'def', 214 | charsetNr : Charsets.UTF8_GENERAL_CI, 215 | name : 'SLEEP(' + sec + ')', 216 | protocol41 : true, 217 | type : Types.LONG 218 | })); 219 | 220 | conn._sendPacket(new Packets.EofPacket()); 221 | 222 | var writer = new PacketWriter(); 223 | writer.writeLengthCodedString(0); 224 | conn._socket.write(writer.toBuffer(conn._parser)); 225 | 226 | conn._sendPacket(new Packets.EofPacket()); 227 | conn._parser.resetPacketNumber(); 228 | }, time); 229 | return; 230 | } 231 | 232 | if ((match = /^SELECT \* FROM stream LIMIT ([0-9]+);?$/i.exec(sql))) { 233 | var num = match[1]; 234 | 235 | this._writePacketStream(num); 236 | return; 237 | } 238 | 239 | if ((match = /^SHOW STATUS LIKE 'Ssl_cipher';?$/i.exec(sql))) { 240 | this._sendPacket(new Packets.ResultSetHeaderPacket({ 241 | fieldCount: 2 242 | })); 243 | 244 | this._sendPacket(new Packets.FieldPacket({ 245 | catalog : 'def', 246 | charsetNr : Charsets.UTF8_GENERAL_CI, 247 | name : 'Variable_name', 248 | protocol41 : true, 249 | type : Types.VARCHAR 250 | })); 251 | 252 | this._sendPacket(new Packets.FieldPacket({ 253 | catalog : 'def', 254 | charsetNr : Charsets.UTF8_GENERAL_CI, 255 | name : 'Value', 256 | protocol41 : true, 257 | type : Types.VARCHAR 258 | })); 259 | 260 | this._sendPacket(new Packets.EofPacket()); 261 | 262 | var writer = new PacketWriter(); 263 | writer.writeLengthCodedString('Ssl_cipher'); 264 | writer.writeLengthCodedString(this._ssl ? this._ssl.getCurrentCipher().name : ''); 265 | this._stream.write(writer.toBuffer(this._parser)); 266 | 267 | this._sendPacket(new Packets.EofPacket()); 268 | this._parser.resetPacketNumber(); 269 | return; 270 | } 271 | 272 | if (/INVALID/i.test(sql)) { 273 | this.error('Invalid SQL', Errors.ER_PARSE_ERROR); 274 | return; 275 | } 276 | 277 | this.error('Interrupted unknown query', Errors.ER_QUERY_INTERRUPTED); 278 | }; 279 | 280 | FakeConnection.prototype._parsePacket = function() { 281 | var Packet = this._determinePacket(); 282 | var packet = new Packet({protocol41: true}); 283 | 284 | packet.parse(this._parser); 285 | 286 | switch (Packet) { 287 | case Packets.AuthSwitchResponsePacket: 288 | if (!this.emit('authSwitchResponse', packet)) { 289 | this.deny('No auth response handler'); 290 | } 291 | break; 292 | case Packets.ClientAuthenticationPacket: 293 | this.database = (packet.database || null); 294 | this.user = (packet.user || null); 295 | 296 | if (!this.emit('clientAuthentication', packet)) { 297 | this.ok(); 298 | } 299 | break; 300 | case Packets.SSLRequestPacket: 301 | this._startTLS(); 302 | break; 303 | case Packets.ComQueryPacket: 304 | if (!this.emit('query', packet)) { 305 | this._handleQueryPacket(packet); 306 | } 307 | break; 308 | case Packets.ComPingPacket: 309 | if (!this.emit('ping', packet)) { 310 | this.ok(); 311 | } 312 | break; 313 | case Packets.ComChangeUserPacket: 314 | this.database = (packet.database || null); 315 | this.user = (packet.user || null); 316 | 317 | if (!this.emit('changeUser', packet)) { 318 | if (packet.user === 'does-not-exist') { 319 | this.deny('User does not exist'); 320 | break; 321 | } else if (packet.database === 'does-not-exist') { 322 | this.error('Database does not exist', Errors.ER_BAD_DB_ERROR); 323 | break; 324 | } 325 | 326 | this.ok(); 327 | } 328 | break; 329 | case Packets.ComQuitPacket: 330 | if (!this.emit('quit', packet)) { 331 | this._socket.end(); 332 | } 333 | break; 334 | default: 335 | if (!this.emit(packet.constructor.name, packet)) { 336 | throw new Error('Unexpected packet: ' + Packet.name); 337 | } 338 | } 339 | }; 340 | 341 | FakeConnection.prototype._determinePacket = function _determinePacket() { 342 | if (this._expectedNextPacket) { 343 | var Packet = this._expectedNextPacket; 344 | 345 | if (Packet === Packets.ClientAuthenticationPacket) { 346 | return !this._ssl && (this._parser.peak(1) << 8) & ClientConstants.CLIENT_SSL 347 | ? Packets.SSLRequestPacket 348 | : Packets.ClientAuthenticationPacket; 349 | } 350 | 351 | this._expectedNextPacket = null; 352 | 353 | return Packet; 354 | } 355 | 356 | var firstByte = this._parser.peak(); 357 | switch (firstByte) { 358 | case 0x01: return Packets.ComQuitPacket; 359 | case 0x03: return Packets.ComQueryPacket; 360 | case 0x0e: return Packets.ComPingPacket; 361 | case 0x11: return Packets.ComChangeUserPacket; 362 | default: 363 | throw new Error('Unknown packet, first byte: ' + firstByte); 364 | } 365 | }; 366 | 367 | FakeConnection.prototype.destroy = function() { 368 | this._socket.destroy(); 369 | }; 370 | 371 | FakeConnection.prototype._writePacketStream = function _writePacketStream(count) { 372 | var remaining = count; 373 | var timer = setInterval(writeRow.bind(this), 20); 374 | 375 | this._socket.on('close', cleanup); 376 | this._socket.on('error', cleanup); 377 | 378 | this._sendPacket(new Packets.ResultSetHeaderPacket({ 379 | fieldCount: 2 380 | })); 381 | 382 | this._sendPacket(new Packets.FieldPacket({ 383 | catalog : 'def', 384 | charsetNr : Charsets.UTF8_GENERAL_CI, 385 | name : 'id', 386 | protocol41 : true, 387 | type : Types.LONG 388 | })); 389 | 390 | this._sendPacket(new Packets.FieldPacket({ 391 | catalog : 'def', 392 | charsetNr : Charsets.UTF8_GENERAL_CI, 393 | name : 'title', 394 | protocol41 : true, 395 | type : Types.VARCHAR 396 | })); 397 | 398 | this._sendPacket(new Packets.EofPacket()); 399 | 400 | function cleanup() { 401 | clearInterval(timer); 402 | } 403 | 404 | function writeRow() { 405 | if (remaining === 0) { 406 | cleanup(); 407 | 408 | this._socket.removeListener('close', cleanup); 409 | this._socket.removeListener('error', cleanup); 410 | 411 | this._sendPacket(new Packets.EofPacket()); 412 | this._parser.resetPacketNumber(); 413 | return; 414 | } 415 | 416 | remaining -= 1; 417 | 418 | var num = count - remaining; 419 | var writer = new PacketWriter(); 420 | writer.writeLengthCodedString(num); 421 | writer.writeLengthCodedString('Row #' + num); 422 | this._socket.write(writer.toBuffer(this._parser)); 423 | } 424 | }; 425 | 426 | if (tls.TLSSocket) { 427 | // 0.11+ environment 428 | FakeConnection.prototype._startTLS = function _startTLS() { 429 | // halt parser 430 | this._parser.pause(); 431 | this._socket.removeAllListeners('data'); 432 | 433 | // socket <-> encrypted 434 | var secureContext = tls.createSecureContext(common.getSSLConfig()); 435 | var secureSocket = new tls.TLSSocket(this._socket, { 436 | secureContext : secureContext, 437 | isServer : true 438 | }); 439 | 440 | // cleartext <-> protocol 441 | secureSocket.on('data', this._handleData.bind(this)); 442 | this._stream = secureSocket; 443 | 444 | var conn = this; 445 | secureSocket.on('secure', function () { 446 | conn._ssl = this.ssl; 447 | }); 448 | 449 | // resume 450 | var parser = this._parser; 451 | process.nextTick(function() { 452 | var buffer = parser._buffer.slice(parser._offset); 453 | parser._offset = parser._buffer.length; 454 | parser.resume(); 455 | secureSocket.ssl.receive(buffer); 456 | }); 457 | }; 458 | } else { 459 | // pre-0.11 environment 460 | FakeConnection.prototype._startTLS = function _startTLS() { 461 | // halt parser 462 | this._parser.pause(); 463 | this._socket.removeAllListeners('data'); 464 | 465 | // inject secure pair 466 | var credentials = Crypto.createCredentials(common.getSSLConfig()); 467 | var securePair = tls.createSecurePair(credentials, true); 468 | this._socket.pipe(securePair.encrypted); 469 | this._stream = securePair.cleartext; 470 | securePair.cleartext.on('data', this._handleData.bind(this)); 471 | securePair.encrypted.pipe(this._socket); 472 | 473 | var conn = this; 474 | securePair.on('secure', function () { 475 | conn._ssl = this.ssl; 476 | }); 477 | 478 | // resume 479 | var parser = this._parser; 480 | process.nextTick(function() { 481 | var buffer = parser._buffer.slice(parser._offset); 482 | parser._offset = parser._buffer.length; 483 | parser.resume(); 484 | securePair.encrypted.write(buffer); 485 | }); 486 | }; 487 | } 488 | -------------------------------------------------------------------------------- /test/fakeServer/common.ts: -------------------------------------------------------------------------------- 1 | var common = exports; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | // common.lib = path.resolve(__dirname, '..', 'lib'); 6 | common.lib = 'mysql/lib'; 7 | common.fixtures = path.resolve(__dirname, 'fixtures'); 8 | 9 | // Useful for triggering ECONNREFUSED errors on connect() 10 | common.bogusPort = 47378; 11 | // Useful for triggering ER_ACCESS_DENIED_ERROR errors on connect() 12 | common.bogusPassword = 'INVALID PASSWORD'; 13 | 14 | // Used for simulating a fake mysql server 15 | common.fakeServerPort = 32893; 16 | // Used for simulating a fake mysql server 17 | common.fakeServerSocket = __dirname + '/fake_server.sock'; 18 | 19 | common.testDatabase = process.env.MYSQL_DATABASE || 'test'; 20 | 21 | // Export common modules 22 | common.Auth = require(common.lib + '/protocol/Auth'); 23 | common.Charsets = require(common.lib + '/protocol/constants/charsets'); 24 | common.ClientConstants = require(common.lib + '/protocol/constants/client'); 25 | common.Connection = require(common.lib + '/Connection'); 26 | common.ConnectionConfig = require(common.lib + '/ConnectionConfig'); 27 | common.Errors = require(common.lib + '/protocol/constants/errors'); 28 | common.Packets = require(common.lib + '/protocol/packets'); 29 | common.PacketWriter = require(common.lib + '/protocol/PacketWriter'); 30 | common.Parser = require(common.lib + '/protocol/Parser'); 31 | common.PoolConfig = require(common.lib + '/PoolConfig'); 32 | common.PoolConnection = require(common.lib + '/PoolConnection'); 33 | common.SqlString = require(common.lib + '/protocol/SqlString'); 34 | common.Types = require(common.lib + '/protocol/constants/types'); 35 | 36 | var Mysql = require('mysql'); 37 | var FakeServer = require('./FakeServer'); 38 | 39 | common.createConnection = function(config) { 40 | config = mergeTestConfig(config); 41 | return Mysql.createConnection(config); 42 | }; 43 | 44 | common.createQuery = Mysql.createQuery; 45 | 46 | common.createTestDatabase = function createTestDatabase(connection, callback) { 47 | var database = common.testDatabase; 48 | 49 | connection.query('CREATE DATABASE ??', [database], function (err) { 50 | if (err && err.code !== 'ER_DB_CREATE_EXISTS') { 51 | callback(err); 52 | return; 53 | } 54 | 55 | callback(null, database); 56 | }); 57 | }; 58 | 59 | common.createPool = function(config) { 60 | config = mergeTestConfig(config); 61 | config.connectionConfig = mergeTestConfig(config.connectionConfig); 62 | return Mysql.createPool(config); 63 | }; 64 | 65 | common.createPoolCluster = function(config) { 66 | config = mergeTestConfig(config); 67 | config.createConnection = common.createConnection; 68 | return Mysql.createPoolCluster(config); 69 | }; 70 | 71 | common.createFakeServer = function(options) { 72 | console.log(options) 73 | return new FakeServer(common.extend({}, options)); 74 | }; 75 | 76 | 77 | 78 | common.detectNewline = function detectNewline(path) { 79 | var newlines = fs.readFileSync(path, 'utf8').match(/(?:\r?\n)/g) || []; 80 | var crlf = newlines.filter(function (nl) { return nl === '\r\n'; }).length; 81 | var lf = newlines.length - crlf; 82 | 83 | return crlf > lf ? '\r\n' : '\n'; 84 | }; 85 | 86 | common.extend = function extend(dest, src) { 87 | for (var key in src) { 88 | dest[key] = src[key]; 89 | } 90 | 91 | return dest; 92 | }; 93 | 94 | common.getTestConnection = function getTestConnection(config, callback) { 95 | if (!callback && typeof config === 'function') { 96 | callback = config; 97 | config = {}; 98 | } 99 | 100 | var connection = common.createConnection(config); 101 | 102 | connection.connect(function (err) { 103 | console.log(err) 104 | if (err && err.code === 'ECONNREFUSED') { 105 | if (process.env.CI) { 106 | throw err; 107 | } 108 | 109 | common.skipTest('cannot connect to MySQL server'); 110 | } 111 | 112 | if (err) { 113 | callback(err); 114 | return; 115 | } 116 | 117 | callback(null, connection); 118 | }); 119 | }; 120 | 121 | common.skipTest = function skipTest(message) { 122 | var msg = 'skipping - ' + message + '\n'; 123 | 124 | try { 125 | fs.writeSync(process.stdout.fd, msg); 126 | fs.fsyncSync(process.stdout.fd); 127 | } catch (e) { 128 | // Ignore error 129 | } 130 | 131 | process.exit(0); 132 | }; 133 | 134 | common.useTestDb = function(connection) { 135 | common.createTestDatabase(connection, function (err) { 136 | if (err) throw err; 137 | }); 138 | 139 | connection.query('USE ' + common.testDatabase); 140 | }; 141 | 142 | common.getTestConfig = function(config) { 143 | return mergeTestConfig(config); 144 | }; 145 | 146 | common.getSSLConfig = function() { 147 | return { 148 | ca : fs.readFileSync(path.join(common.fixtures, 'server.crt'), 'ascii'), 149 | cert : fs.readFileSync(path.join(common.fixtures, 'server.crt'), 'ascii'), 150 | ciphers : 'ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:AES128-SHA:HIGH:!MD5:!aNULL:!EDH', 151 | key : fs.readFileSync(path.join(common.fixtures, 'server.key'), 'ascii') 152 | }; 153 | }; 154 | 155 | function mergeTestConfig(config) { 156 | config = common.extend({ 157 | host : process.env.MYSQL_HOST, 158 | port : process.env.MYSQL_PORT, 159 | user : process.env.MYSQL_USER, 160 | password : process.env.MYSQL_PASSWORD, 161 | socketPath : process.env.MYSQL_SOCKET 162 | }, config); 163 | 164 | return config; 165 | } 166 | -------------------------------------------------------------------------------- /test/mvc.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import { contextConfiguration, Test, run, OnlyRun, Describe, before, after } from '@rockerjs/tsunit'; 3 | import { app } from './app' 4 | import * as request from 'request-promise' 5 | import * as md5 from 'md5' 6 | import * as fs from 'fs' 7 | import * as path from 'path' 8 | import { resolve } from 'url'; 9 | import * as mysql from './fakeServer/common' 10 | 11 | const expect = chai.expect; 12 | 13 | const sleep = (ms) => new Promise((resolve) => { 14 | setTimeout(() => { 15 | resolve() 16 | }, ms); 17 | }) 18 | 19 | class MVCSpec { 20 | 21 | 22 | @before 23 | async before() { 24 | 25 | let createFakeServer = () => new Promise((resolve, reject) => { 26 | let server = mysql.createFakeServer() 27 | 28 | server.listen(mysql.fakeServerPort, () => resolve()) 29 | server.on('connection', function(conn) { 30 | conn.handshake(); 31 | conn.on('query', function(packet) { 32 | switch (packet.sql.trim()) { 33 | case `select * from app_info where username='yangli'`: 34 | this._sendPacket(new mysql.Packets.ResultSetHeaderPacket({ 35 | fieldCount: 2 36 | })); 37 | 38 | this._sendPacket(new mysql.Packets.FieldPacket({ 39 | catalog : 'def', 40 | charsetNr : mysql.Charsets.UTF8_GENERAL_CI, 41 | name : 'appname', 42 | protocol41 : true, 43 | type : mysql.Types.VARCHAR 44 | })); 45 | 46 | this._sendPacket(new mysql.Packets.FieldPacket({ 47 | catalog : 'def', 48 | charsetNr : mysql.Charsets.UTF8_GENERAL_CI, 49 | name : 'username', 50 | protocol41 : true, 51 | type : mysql.Types.VARCHAR 52 | })); 53 | 54 | 55 | this._sendPacket(new mysql.Packets.EofPacket()) 56 | 57 | var writer = new mysql.PacketWriter(); 58 | writer.writeLengthCodedString('dingjunjie'); 59 | writer.writeLengthCodedString('yangli'); 60 | this._socket.write(writer.toBuffer(this._parser)); 61 | 62 | var writer = new mysql.PacketWriter(); 63 | writer.writeLengthCodedString('IOriens'); 64 | writer.writeLengthCodedString('yangli'); 65 | this._socket.write(writer.toBuffer(this._parser)); 66 | 67 | this._sendPacket(new mysql.Packets.EofPacket()); 68 | this._parser.resetPacketNumber(); 69 | break; 70 | default: 71 | this._handleQueryPacket(packet); 72 | break; 73 | } 74 | }); 75 | }); 76 | }) 77 | 78 | 79 | await createFakeServer() 80 | 81 | await app() 82 | } 83 | 84 | 85 | @Test('test render ejs') 86 | async testRenderEJSSuccess() { 87 | let res = await request('http://localhost:8080/home/user') 88 | expect(res).to.include('

foo

') 89 | } 90 | 91 | @Test('test auth') 92 | async testAuthSuccess() { 93 | let res = await request('http://localhost:8080/home/needAuth') 94 | expect(res).to.be.a('string');; 95 | expect(res).to.include('Example Domain') 96 | } 97 | 98 | @Test('test no need auth') 99 | async testNoNeedAuthSuccess() { 100 | let res = await request('http://localhost:8080/home/dontNeedAuth') 101 | expect(res).to.equal('{"foo":"bar"}');; 102 | } 103 | 104 | @Test('test error handling') 105 | async testErrorHandleSuccess() { 106 | let res = await request('http://localhost:8080/home/error') 107 | expect(res).to.equal('{"status":{"code":1,"message":"test errorprocessor"}}');; 108 | } 109 | 110 | @Test('test no such url error handling') 111 | async testNoSuchUrlSuccess() { 112 | let res = await request('http://localhost:8080/home/666666') 113 | expect(res).to.equal('{"status":{"code":404,"message":"The request url(/home/666666, full path: /home/666666, method: GET) not found."}}');; 114 | } 115 | 116 | 117 | @Test('test sql request') 118 | async testSQLRequestSuccess() { 119 | let data = { 120 | name: 'foo', 121 | person: JSON.stringify({ 122 | a: 1, 123 | b: 2 124 | }) 125 | } 126 | let res = await request.post('http://localhost:8080/home/mysql', { form: data }) 127 | console.log(res) 128 | 129 | expect(res).to.include('"person":{"a":1,"b":2}'); 130 | expect(res).to.include('"name":"foo"'); 131 | } 132 | 133 | @Test('test head type request') 134 | async testHeadRequest() { 135 | let res = await request.head('http://localhost:8080/home/head') 136 | console.log(res) 137 | expect(res).to.have.property(`content-type`); 138 | } 139 | 140 | @Test('test redirect type request') 141 | async testRedirectRequest() { 142 | let res = await request.get('http://localhost:8080/home/redirect') 143 | expect(res).to.include(`微店`); 144 | } 145 | 146 | @Test('test static assets') 147 | async testStaticAssetsSuccess() { 148 | let res = await request.get({ 149 | url: 'http://localhost:8080/assets/favicon.jpg', 150 | encoding: null 151 | }) 152 | 153 | let buffer = Buffer.from(res, 'utf8'); 154 | let fsContent = fs.readFileSync(path.resolve(__dirname, 'app/assets/favicon.jpg')) 155 | expect(md5(buffer)).to.equal(md5(fsContent)); 156 | 157 | await sleep(1000) 158 | 159 | res = await request.get({ 160 | url: 'http://localhost:8080/assets/favicon.jpg', 161 | encoding: null 162 | }) 163 | buffer = Buffer.from(res, 'utf8'); 164 | fsContent = fs.readFileSync(path.resolve(__dirname, 'app/assets/favicon.jpg')) 165 | expect(md5(buffer)).to.equal(md5(fsContent)); 166 | 167 | } 168 | 169 | } 170 | 171 | 172 | 173 | export { MVCSpec }; 174 | -------------------------------------------------------------------------------- /test/test.spec.ts: -------------------------------------------------------------------------------- 1 | import { run } from '@rockerjs/tsunit'; 2 | import { MVCSpec } from "./mvc.spec"; 3 | run(new MVCSpec()); 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "experimentalDecorators": true, 5 | "emitDecoratorMetadata": true, 6 | "mapRoot": "./dist", 7 | "module": "commonjs", 8 | "outDir": "./dist", 9 | "sourceMap": true, 10 | "target": "es6" 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | "dist", 15 | "demo" 16 | ] 17 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "test/**" 9 | ] 10 | }, 11 | "jsRules": { 12 | }, 13 | "rules": { 14 | "array-type": false, 15 | "ban-types": false, 16 | "callable-types": false, 17 | "forin": false, 18 | "interface-name": false, 19 | "interface-over-type-literal": false, 20 | "one-variable-per-declaration": false, 21 | "member-ordering": false, 22 | "no-trailing-whitespace": false, 23 | "no-string-literal": false, 24 | "no-namespace": false, 25 | "object-literal-sort-keys": false, 26 | "only-arrow-functions": [ 27 | false 28 | ], 29 | "ordered-imports": false, 30 | "max-classes-per-file": true, 31 | "max-line-length": [true, 220], 32 | "new-parens": false, 33 | "no-unused-expression": false, 34 | "no-shadowed-variable": false, 35 | "prefer-for-of": false, 36 | "variable-name": false 37 | }, 38 | "rulesDirectory": [] 39 | } --------------------------------------------------------------------------------