├── .eslintignore ├── .travis.yml ├── .gitignore ├── test ├── routes │ └── index.js ├── controllers │ └── indexCtrl.js └── index.js ├── .eslintrc.json ├── .editorconfig ├── LICENSE ├── package.json ├── History.md ├── router.js ├── paloma.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | node_modules/* 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test.js 3 | coverage 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /test/routes/index.js: -------------------------------------------------------------------------------- 1 | app.route({ 2 | method: 'GET', 3 | path: '/', 4 | controller: 'indexCtrl' 5 | }) 6 | -------------------------------------------------------------------------------- /test/controllers/indexCtrl.js: -------------------------------------------------------------------------------- 1 | app.controller('indexCtrl', function (ctx, next) { 2 | ctx.body = 'This is index page' 3 | }) 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "globals": { 4 | "describe": true, 5 | "it": true, 6 | "app": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 Paloma contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paloma", 3 | "description": "An angular-like MVC framework, based on `koa@2` & `bottlejs`.", 4 | "version": "1.2.5", 5 | "main": "paloma.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "pretest": "npm run lint", 9 | "test": "istanbul cover _mocha" 10 | }, 11 | "keywords": [ 12 | "web", 13 | "app", 14 | "http", 15 | "application", 16 | "framework", 17 | "middleware", 18 | "mvc", 19 | "koa", 20 | "bottlejs", 21 | "di", 22 | "angular" 23 | ], 24 | "dependencies": { 25 | "another-json-schema": "^3.8.2", 26 | "bottlejs": "1.7.1", 27 | "fn-args": "3.0.0", 28 | "koa": "2.6.2", 29 | "koa-compose": "4.1.0", 30 | "path-to-regexp": "3.0.0", 31 | "require-directory": "2.1.1" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/palomajs/paloma" 36 | }, 37 | "license": "MIT", 38 | "engines": { 39 | "node": ">= 7.6" 40 | }, 41 | "devDependencies": { 42 | "eslint": "5.12.1", 43 | "eslint-config-standard": "12.0.0", 44 | "eslint-plugin-import": "2.15.0", 45 | "eslint-plugin-node": "8.0.1", 46 | "eslint-plugin-promise": "4.0.1", 47 | "eslint-plugin-standard": "4.0.0", 48 | "istanbul": "0.4.5", 49 | "koa-bodyparser": "^4.2.1", 50 | "mocha": "^5.2.0", 51 | "supertest": "^3.4.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | ## 1.2.5/2019-01-24 2 | 3 | - update deps 4 | - fix: ctx.state.routerName 5 | 6 | ## 1.2.4/2018-12-20 7 | 8 | - upgrade deps 9 | - fix tests 10 | 11 | ## 1.2.3/2018-06-21 12 | 13 | - update AJS@3.7.0 14 | 15 | ## 1.2.2/2018-06-04 16 | 17 | - add `ctx.state.routerName` 18 | 19 | ## 1.2.1/2018-05-14 20 | 21 | - export `app.Types` 22 | 23 | ## 1.2.0/2018-05-14 24 | 25 | - update deps 26 | - add `Types` 27 | 28 | ## 1.1.8/2018-04-11 29 | 30 | - update `another-json-schema` 31 | 32 | ## 1.1.7/2018-03-15 33 | 34 | - add `ctx.request.params` 35 | 36 | ## 1.1.6/2018-03-07 37 | 38 | - fix controller function cannot inject services 39 | - upgrade deps 40 | - update README.md 41 | - update test 42 | 43 | ## 1.1.5/2018-01-23 44 | 45 | - add `ctx._matchedRoute` when a route is matched 46 | 47 | ## 1.1.4/2018-01-10 48 | 49 | - update deps 50 | - add .eslintignore & lint script 51 | 52 | ## 1.1.3/2017-11-14 53 | 54 | - return `originError` message if exist 55 | 56 | ## 1.1.2/2017-11-14 57 | 58 | - upgrade `another-json-schema@^3.2.1` 59 | 60 | ## 1.1.1/2017-11-14 61 | 62 | - upgrade `another-json-schema@3.2.0` 63 | 64 | ## 1.1.0/2017-10-13 65 | 66 | - build-in paloma-router, use `another-json-schema` instead of `validator-it` 67 | - use standard eslint 68 | - use istanbul & update tests coverage to 100% 69 | 70 | ## 1.0.2/2017-09-11 71 | 72 | - update dependencies 73 | - update README 74 | - fix typo 75 | 76 | ## 1.0.1/2017-03-17 77 | 78 | - update deps, especially for koa@2 79 | 80 | ## 1.0.0/2016-11-15 81 | 82 | - remove generatorFunction support!!! 83 | - fix async function controller bug(fn-args@2.0.0) 84 | - tweak codes & update deps 85 | 86 | ## 0.5.0/2016-11-09 87 | 88 | - update deps 89 | 90 | ## 0.4.0/2016-03-16 91 | 92 | - support generatorFunction in `.controller` 93 | 94 | ## 0.3.0/2016-03-10 95 | 96 | - remove `.view` 97 | - fix `.controller` no return 98 | 99 | ## 0.2.0/2015-11-13 100 | 101 | - use paloma-router@0.2.0 102 | - update tests 103 | 104 | ## 0.1.2/2015-11-11 105 | 106 | - use bottlejs@^1.1.0 107 | - update `.service` test 108 | 109 | ## 0.1.1/2015-11-11 110 | 111 | - Add travis-ci 112 | - Improve `controller` performance 113 | 114 | ## 0.1.0/2015-11-10 115 | 116 | It just works. 117 | -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | const fnArgs = require('fn-args') 2 | const debug = require('debug')('paloma-router') 3 | const pathToRegexp = require('path-to-regexp') 4 | const AJS = require('another-json-schema') 5 | const compose = require('koa-compose') 6 | 7 | module.exports = function (route) { 8 | const method = route.method.toUpperCase() 9 | const path = route.path 10 | const routerName = route.routerName 11 | const validate = route.validate 12 | 13 | let controller = Array.isArray(route.controller) ? route.controller : [route.controller] 14 | 15 | controller = controller.map(controllerName => { 16 | if (typeof controllerName === 'string') { 17 | return this.controller(controllerName) 18 | } 19 | if (typeof controllerName === 'function') { 20 | const _args = fnArgs(controllerName).slice(2) 21 | return (ctx, next) => { 22 | return controllerName.apply(null, [ctx, next].concat(_args.map(arg => this._bottle.container[arg]))) 23 | } 24 | } 25 | throw new TypeError('`controller` only support function or name of controller.') 26 | }) 27 | 28 | if (validate) { 29 | controller.unshift(validatorMiddleware(validate, `${method} ${path}`)) 30 | } 31 | controller = compose(controller) 32 | 33 | return (ctx, next) => { 34 | ctx.request.params = ctx.params = {} 35 | if (!matches(ctx, method)) return next() 36 | 37 | const keys = [] 38 | const re = pathToRegexp(path, keys) 39 | const m = re.exec(ctx.path) 40 | 41 | /* istanbul ignore else */ 42 | if (m) { 43 | ctx.state.routerName = routerName 44 | ctx._matchedRoute = path 45 | const args = m.slice(1).map(decode) 46 | 47 | keys.forEach((pathRe, index) => { 48 | ctx.params[pathRe.name] = args[index] 49 | }) 50 | debug('%s %s matches %s %j', ctx.method, path, ctx.path, args) 51 | return controller(ctx, next) 52 | } 53 | 54 | // miss 55 | return next() 56 | } 57 | } 58 | 59 | /** 60 | * Decode value. 61 | */ 62 | 63 | function decode (val) { 64 | /* istanbul ignore else */ 65 | if (val) return decodeURIComponent(val) 66 | } 67 | 68 | /** 69 | * Check request method. 70 | */ 71 | 72 | function matches (ctx, method) { 73 | if (ctx.method === method) return true 74 | if (method === 'GET' && ctx.method === 'HEAD') return true 75 | return false 76 | } 77 | 78 | function validatorMiddleware (schema, path) { 79 | const compiledSchema = AJS(path, schema) 80 | return (ctx, next) => { 81 | const result = compiledSchema.validate(ctx.request, { additionalProperties: true }) 82 | if (result.valid) { 83 | return next() 84 | } 85 | ctx.throw(result.error.status || result.error.statusCode || 400, result.error.originError || result.error) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /paloma.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const path = require('path') 3 | 4 | const Bottle = require('bottlejs') 5 | const fnArgs = require('fn-args') 6 | const Koa = require('koa') 7 | const requireDirectory = require('require-directory') 8 | const AJS = require('another-json-schema') 9 | 10 | const router = require('./router') 11 | 12 | module.exports = class Paloma extends Koa { 13 | constructor () { 14 | super() 15 | 16 | this._bottle = new Bottle() 17 | this._controllers = Object.create(null) 18 | } 19 | 20 | load (dir) { 21 | if (path.isAbsolute(dir)) { 22 | requireDirectory(module, dir) 23 | } else { 24 | requireDirectory(module, path.join(path.dirname(module.parent.filename), dir)) 25 | } 26 | return this 27 | } 28 | 29 | route (route) { 30 | assert(typeof route === 'object', '`route` must be a object') 31 | assert(typeof route.method === 'string', '`method` must be a string, like: \'GET\'') 32 | assert(typeof route.path === 'string', '`path` must be a string, like: \'/users/:name\'') 33 | assert(typeof route.controller === 'string' || typeof route.controller === 'function' || Array.isArray(route.controller), '`controller` must be a string or function or array') 34 | 35 | this.use(router.call(this, route)) 36 | return this 37 | } 38 | 39 | controller (name, fn) { 40 | assert(typeof name === 'string', 'controller name must be string.') 41 | 42 | if (!fn) { 43 | if (!this._controllers[name]) { 44 | throw new TypeError('controller ' + name + ' is not defined') 45 | } 46 | return this._controllers[name] 47 | } 48 | const _args = fnArgs(fn).slice(2) 49 | 50 | this._controllers[name] = (ctx, next) => { 51 | return fn.apply(null, [ctx, next].concat(_args.map(arg => this._bottle.container[arg]))) 52 | } 53 | return this 54 | } 55 | 56 | service (name, fn) { 57 | assert(typeof name === 'string', 'service name must be string.') 58 | 59 | if (!fn) { 60 | const _service = this._bottle.container[name] 61 | if (!_service) { 62 | throw new TypeError('service ' + name + ' is not defined') 63 | } 64 | return _service 65 | } 66 | const _args = fnArgs(fn) 67 | this._bottle.service.apply(this._bottle, [name, fn].concat(_args)) 68 | return this 69 | } 70 | 71 | factory () { 72 | this._bottle.factory.apply(this._bottle, arguments) 73 | return this 74 | } 75 | 76 | provider () { 77 | this._bottle.provider.apply(this._bottle, arguments) 78 | return this 79 | } 80 | 81 | constant () { 82 | this._bottle.constant.apply(this._bottle, arguments) 83 | return this 84 | } 85 | 86 | value () { 87 | this._bottle.value.apply(this._bottle, arguments) 88 | return this 89 | } 90 | 91 | decorator () { 92 | this._bottle.decorator.apply(this._bottle, arguments) 93 | return this 94 | } 95 | 96 | middlewares () { 97 | this._bottle.middleware.apply(this._bottle, arguments) 98 | return this 99 | } 100 | 101 | get Types () { 102 | return AJS.Types 103 | } 104 | } 105 | 106 | module.exports.Types = AJS.Types 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Paloma 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Build status][travis-image]][travis-url] 5 | [![Dependency Status][david-image]][david-url] 6 | [![License][license-image]][license-url] 7 | [![Downloads][downloads-image]][downloads-url] 8 | 9 | An angular-like MVC framework, based on: 10 | 11 | - [koa@2](https://github.com/koajs/koa/tree/v2.x): Next generation web framework for node.js. 12 | - [bottlejs](https://github.com/young-steveo/bottlejs): A powerful dependency injection micro container. 13 | 14 | ### Installation 15 | 16 | ```sh 17 | $ npm i paloma --save 18 | ``` 19 | 20 | If you use `async` function as controller, you may need node v7.6.0+ or babel. 21 | 22 | ### Scaffold 23 | 24 | see [create-paloma-app](https://github.com/palomajs/create-paloma-app). 25 | 26 | ### Example 27 | 28 | **Common function** 29 | 30 | ```js 31 | const Paloma = require('paloma') 32 | const app = new Paloma() 33 | 34 | app.controller('indexCtrl', (ctx, next, indexService) => { 35 | ctx.body = `Hello, ${indexService.getName()}` 36 | }) 37 | 38 | app.service('indexService', function () { 39 | this.getName = function () { 40 | return 'Paloma' 41 | } 42 | }) 43 | 44 | app.route({ 45 | method: 'GET', 46 | path: '/', 47 | controller: 'indexCtrl' 48 | }) 49 | 50 | app.listen(3000) 51 | 52 | /* 53 | $ curl localhost:3000 54 | Hello, Paloma 55 | */ 56 | ``` 57 | 58 | When a route is matched, its path is available at `ctx._matchedRoute`. 59 | 60 | **Async function** 61 | 62 | ```js 63 | const Paloma = require('paloma') 64 | const app = new Paloma() 65 | 66 | app.controller('indexCtrl', async (ctx, next, indexService) => { 67 | ctx.body = await Promise.resolve(`Hello, ${indexService.getName()}`) 68 | }) 69 | 70 | app.service('indexService', function () { 71 | this.getName = function () { 72 | return 'Paloma' 73 | } 74 | }) 75 | 76 | app.route({ 77 | method: 'GET', 78 | path: '/', 79 | controller: 'indexCtrl' 80 | }) 81 | 82 | app.listen(3000) 83 | 84 | /* 85 | $ curl localhost:3000 86 | Hello, Paloma 87 | */ 88 | ``` 89 | 90 | or 91 | 92 | ```js 93 | const Paloma = require('paloma') 94 | const app = new Paloma() 95 | 96 | app.route({ 97 | method: 'GET', 98 | path: '/', 99 | controller: async (ctx, next, indexService) => { 100 | ctx.body = await Promise.resolve(`Hello, ${indexService.getName()}`) 101 | } 102 | }) 103 | 104 | app.service('indexService', function () { 105 | this.getName = function () { 106 | return 'Paloma' 107 | } 108 | }) 109 | 110 | app.listen(3000) 111 | ``` 112 | 113 | **routerName** 114 | 115 | ```js 116 | const Paloma = require('paloma') 117 | const app = new Paloma() 118 | 119 | app.route({ 120 | method: 'GET', 121 | path: '/', 122 | routerName: 'getHome', 123 | controller: (ctx, next) => { 124 | ctx.body = `routerName: ${ctx.state.routerName}` 125 | } 126 | }) 127 | 128 | app.listen(3000) 129 | 130 | /* 131 | $ curl localhost:3000 132 | routerName: getHome 133 | */ 134 | ``` 135 | 136 | **Validator** 137 | 138 | ```js 139 | const Paloma = require('paloma') 140 | const app = new Paloma() 141 | 142 | app.controller('indexCtrl', (ctx, next) => { 143 | ctx.body = `Hello, ${ctx.query.name}` 144 | }) 145 | 146 | app.route({ 147 | method: 'GET', 148 | path: '/', 149 | controller: 'indexCtrl', 150 | validate: { 151 | query: { 152 | name: { type: 'string', enum: ['tom', 'xp'], required: true } 153 | } 154 | } 155 | }) 156 | 157 | app.listen(3000) 158 | /* 159 | $ curl localhost:3000 160 | ($.query.name: undefined) ✖ (required: true) 161 | $ curl localhost:3000?name=tom 162 | Hello, tom 163 | $ curl localhost:3000?name=nswbmw 164 | ($.query.name: "nswbmw") ✖ (enum: tom,xp) 165 | */ 166 | ``` 167 | 168 | More validators usage see [another-json-schema](https://github.com/nswbmw/another-json-schema). 169 | 170 | **Array controllers** 171 | 172 | ```js 173 | const bodyParser = require('koa-bodyparser') 174 | const Paloma = require('paloma') 175 | const app = new Paloma() 176 | 177 | app.controller('indexCtrl', (ctx, next) => { 178 | ctx.body = ctx.request.body 179 | }) 180 | 181 | app.route({ 182 | method: 'POST', 183 | path: '/', 184 | controller: [bodyParser(), 'indexCtrl'] 185 | }) 186 | 187 | app.listen(3000) 188 | ``` 189 | 190 | More examples see [test](./test) and [paloma-examples](https://github.com/palomajs/paloma-examples). 191 | 192 | ### API 193 | 194 | #### load(dir) 195 | 196 | Load all files by [require-directory](https://github.com/troygoode/node-require-directory). 197 | 198 | Param | Type | Description 199 | :---------|:-----------|:----------- 200 | **dir** | *String* | An absolute path or relative path. 201 | 202 | #### route(route) 203 | 204 | Register a route. `route` use `app.use` internally, so pay attention to the middleware load order. 205 | 206 | Param | Type | Description 207 | :-------------------------------------|:-------------------------------------|:----------- 208 | **route** | *Object* | 209 | **route.method** | *String* | HTTP request method, eg: `GET`, `post`. 210 | **route.path** | *String* | Request path, see [path-to-regexp](https://github.com/pillarjs/path-to-regexp), eg: `/:name`. 211 | **route.controller** | *String\|Function\|[String\|Function]* | Controller functions or names. 212 | **route.validate**
*(optional)* | *Object* | Validate Object schemas. 213 | 214 | #### controller(name[, fn]) 215 | 216 | Register or get a controller. If `fn` missing, return a controller by `name`. 217 | 218 | Param | Type | Description 219 | :---------------------------|:------------|:----------- 220 | **name** | *String* | Controller name. 221 | **fn**
*(optional)* | *Function* | Controller handler. 222 | **fn->arguments[0]->ctx** | *Object* | Koa's `ctx`. 223 | **fn->arguments[1]->next** | *Function* | Koa's `next`. 224 | **fn->arguments[2...]** | *Object* | Instances of services. 225 | 226 | #### service(name[, fn]) 227 | 228 | Register a service constructor or get a service instance. If `fn` missing, return a service instance by `name`. 229 | 230 | Param | Type | Description 231 | :----------|:-----------|:----------- 232 | **name** | *String* | The name of the service. Must be unique to each service instance. 233 | **fn** | *Function* | A constructor function that will be instantiated as a singleton. 234 | 235 | #### factory(name, fn) 236 | 237 | Register a service factory. 238 | 239 | Param | Type | Description 240 | :-----------|:-----------|:-------- 241 | **name** | *String* | The name of the service. Must be unique to each service instance. 242 | **fn** | *Function* | A function that should return the service object. Will only be called once; the Service will be a singleton. Gets passed an instance of the container to allow dependency injection when creating the service. 243 | 244 | #### provider(name, fn) 245 | 246 | Register a service provider. 247 | 248 | Param | Type | Details 249 | :------------|:-----------|:-------- 250 | **name** | *String* | The name of the service. Must be unique to each service instance. 251 | **fn** | *Function* | A constructor function that will be instantiated as a singleton. Should expose a function called `$get` that will be used as a factory to instantiate the service. 252 | 253 | #### constant(name, value) 254 | 255 | Register a read only value as a service. 256 | 257 | Param | Type | Details 258 | :---------|:-----------|:-------- 259 | **name** | *String* | The name of the constant. Must be unique to each service instance. 260 | **value** | *Mixed* | A value that will be defined as enumerable, but not writable. 261 | 262 | #### value(name, value) 263 | 264 | Register an arbitrary value as a service. 265 | 266 | Param | Type | Details 267 | :----------|:---------|:-------- 268 | **name** | *String* | The name of the value. Must be unique to each service instance. 269 | **value** | *Mixed* | A value that will be defined as enumerable, readable and writable. 270 | 271 | #### decorator([name, ]fn) 272 | 273 | Register a decorator function that the provider will use to modify your services at creation time. 274 | 275 | Param | Type | Details 276 | :--------------------------|:-----------|:-------- 277 | **name**
*(optional)* | *String* | The name of the service this decorator will affect. Will run for all services if not passed. 278 | **fn** | *Function* | A function that will accept the service as the first parameter. Should return the service, or a new object to be used as the service. 279 | 280 | #### middlewares([name, ]fn) 281 | 282 | Register a middleware function. This function will be executed every time the service is accessed. Distinguish with koa's `middleware`. 283 | 284 | Param | Type | Details 285 | :--------------------------|:-----------|:-------- 286 | **name**
*(optional)* | *String* | The name of the service for which this middleware will be called. Will run for all services if not passed. 287 | **fn** | *Function* | A function that will accept the service as the first parameter, and a `next` function as the second parameter. Should execute `next()` to allow other middleware in the stack to execute. Bottle will throw anything passed to the `next` function, i.e. `next(new Error('error msg'))`. 288 | 289 | #### use(fn) & 290 | 291 | see [koa](https://github.com/koajs/koa/tree/v2.x). 292 | 293 | ### License 294 | 295 | MIT 296 | 297 | [npm-image]: https://img.shields.io/npm/v/paloma.svg?style=flat-square 298 | [npm-url]: https://npmjs.org/package/paloma 299 | [travis-image]: https://img.shields.io/travis/palomajs/paloma.svg?style=flat-square 300 | [travis-url]: https://travis-ci.org/palomajs/paloma 301 | [david-image]: http://img.shields.io/david/palomajs/paloma.svg?style=flat-square 302 | [david-url]: https://david-dm.org/palomajs/paloma 303 | [license-image]: http://img.shields.io/npm/l/paloma.svg?style=flat-square 304 | [license-url]: LICENSE 305 | [downloads-image]: http://img.shields.io/npm/dm/paloma.svg?style=flat-square 306 | [downloads-url]: https://npmjs.org/package/paloma 307 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const Paloma = require('../paloma') 2 | const request = require('supertest') 3 | const assert = require('assert') 4 | const path = require('path') 5 | 6 | describe('Paloma', function () { 7 | it('.load()', function () { 8 | const app = global.app = new Paloma() 9 | try { 10 | app.load('abc') 11 | } catch (e) { 12 | assert.strict.equal(e.code, 'ENOENT') 13 | } 14 | 15 | app.load(path.join(__dirname, 'controllers')) 16 | assert(app.controller('indexCtrl'), 'indexCtrl should exist!') 17 | 18 | delete global.app 19 | }) 20 | 21 | it('.controller()', function () { 22 | const app = new Paloma() 23 | app.controller('indexCtrl', function (ctx, next) { 24 | ctx.body = 'This is index page' 25 | }) 26 | assert(app.controller('indexCtrl'), 'indexCtrl should exist!') 27 | }) 28 | 29 | describe('.route()', function () { 30 | it('wrong controller type', function () { 31 | const app = new Paloma() 32 | let error 33 | 34 | try { 35 | app.route({ 36 | method: 'GET', 37 | path: '/', 38 | controller: [1] 39 | }) 40 | } catch (e) { 41 | error = e 42 | } 43 | assert(error.message === '`controller` only support function or name of controller.') 44 | }) 45 | 46 | it('controller string', function () { 47 | const app = new Paloma() 48 | 49 | app.controller('indexCtrl', function (ctx, next) { 50 | ctx.body = 'This is index page' 51 | }) 52 | 53 | app.route({ 54 | method: 'GET', 55 | path: '/', 56 | controller: 'indexCtrl' 57 | }) 58 | 59 | return request(app.callback()) 60 | .get('/') 61 | .expect(200) 62 | .then((res) => { 63 | assert.strict.equal(res.text, 'This is index page') 64 | }) 65 | }) 66 | 67 | it('controller function', function () { 68 | const app = new Paloma() 69 | 70 | app.service('User', function () { 71 | this.getUsers = () => ['tom', 'xp'] 72 | }) 73 | 74 | app.route({ 75 | method: 'GET', 76 | path: '/users', 77 | controller: function (ctx, next, User) { 78 | ctx.body = User.getUsers() 79 | } 80 | }) 81 | 82 | return request(app.callback()) 83 | .get('/users') 84 | .expect(200) 85 | .then((res) => { 86 | assert.strict.deepEqual(res.body, ['tom', 'xp']) 87 | }) 88 | }) 89 | 90 | it('controller array', function () { 91 | const app = new Paloma() 92 | 93 | app.controller('indexCtrl', function (ctx, next) { 94 | ctx.body = 'This is index page' 95 | return next() 96 | }) 97 | 98 | app.route({ 99 | method: 'GET', 100 | path: '/', 101 | controller: [ 102 | 'indexCtrl', 103 | function (ctx, next) { 104 | ctx.body += '!!!' 105 | } 106 | ] 107 | }) 108 | 109 | return request(app.callback()) 110 | .get('/') 111 | .expect(200) 112 | .then((res) => { 113 | assert.strict.equal(res.text, 'This is index page!!!') 114 | }) 115 | }) 116 | 117 | it('controller not exist', function () { 118 | const app = new Paloma() 119 | let error 120 | 121 | try { 122 | app.route({ 123 | method: 'GET', 124 | path: '/', 125 | controller: 'indexCtrl' 126 | }) 127 | } catch (e) { 128 | error = e 129 | } 130 | assert(error.message === 'controller indexCtrl is not defined') 131 | }) 132 | 133 | it('params', function () { 134 | const app = new Paloma() 135 | 136 | app.controller('userCtrl', function (ctx, next) { 137 | ctx.body = `This is ${ctx.params.user} page` 138 | }) 139 | 140 | app.route({ 141 | method: 'GET', 142 | path: '/users/:user', 143 | controller: 'userCtrl' 144 | }) 145 | 146 | return request(app.callback()) 147 | .get('/users/nswbmw') 148 | .expect(200) 149 | .then((res) => { 150 | assert.strict.equal(res.text, 'This is nswbmw page') 151 | }) 152 | }) 153 | 154 | it('validate', function () { 155 | const app = new Paloma() 156 | const bodyparser = require('koa-bodyparser') 157 | 158 | app.use(bodyparser()) 159 | app.controller('indexCtrl', function (ctx, next) { 160 | ctx.body = ctx.request.body 161 | }) 162 | 163 | app.route({ 164 | method: 'post', 165 | path: '/', 166 | controller: 'indexCtrl', 167 | validate: { 168 | body: { 169 | user: { type: 'string', required: true }, 170 | age: { type: 'number' } 171 | } 172 | } 173 | }) 174 | 175 | return request(app.callback()) 176 | .post('/') 177 | .expect(400) 178 | .then((res) => { 179 | assert.strict.equal(res.text, '($.body.user: undefined) ✖ (required: true)') 180 | }) 181 | .then(() => { 182 | return request(app.callback()) 183 | .post('/') 184 | .send({ user: 'nswbmw', age: 18 }) 185 | .expect(200) 186 | .then((res) => { 187 | assert.strict.deepEqual(res.body, { user: 'nswbmw', age: 18 }) 188 | }) 189 | }) 190 | .then(() => { 191 | return request(app.callback()) 192 | .post('/') 193 | .send({ user: 'nswbmw', age: '18' }) 194 | .expect(400) 195 | .then((res) => { 196 | assert.strict.equal(res.text, '($.body.age: "18") ✖ (type: number)') 197 | }) 198 | }) 199 | }) 200 | 201 | it('validate with customize error', function () { 202 | const app = new Paloma() 203 | const bodyparser = require('koa-bodyparser') 204 | 205 | app.use(bodyparser()) 206 | app.controller('indexCtrl', function (ctx, next) { 207 | ctx.body = ctx.request.body 208 | }) 209 | 210 | const email = (actual, key, parent) => { 211 | if (/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(actual)) { 212 | return true 213 | } 214 | throw new Error('E-mail format is incorrect!') 215 | } 216 | 217 | app.route({ 218 | method: 'post', 219 | path: '/', 220 | controller: 'indexCtrl', 221 | validate: { 222 | body: { 223 | email: { type: email } 224 | } 225 | } 226 | }) 227 | 228 | return request(app.callback()) 229 | .post('/') 230 | .send({ email: '123' }) 231 | .expect(400) 232 | .then((res) => { 233 | assert.strict.equal(res.text, 'E-mail format is incorrect!') 234 | }) 235 | .then(() => { 236 | return request(app.callback()) 237 | .post('/') 238 | .send({ email: '123@aa.bb' }) 239 | .expect(200) 240 | .then((res) => { 241 | assert.strict.deepEqual(res.body, { email: '123@aa.bb' }) 242 | }) 243 | }) 244 | }) 245 | 246 | it('404', function () { 247 | const app = new Paloma() 248 | 249 | app.route({ 250 | method: 'GET', 251 | path: '/', 252 | controller: function (ctx, next) { 253 | ctx.body = 'This is index page' 254 | } 255 | }) 256 | 257 | return request(app.callback()) 258 | .post('/') 259 | .expect(404) 260 | .then((res) => { 261 | assert.strict.equal(res.text, 'Not Found') 262 | }) 263 | .then(() => { 264 | return request(app.callback()) 265 | .get('/users') 266 | .expect(404) 267 | .then((res) => { 268 | assert.strict.equal(res.text, 'Not Found') 269 | }) 270 | }) 271 | }) 272 | 273 | it('HEAD', function () { 274 | const app = new Paloma() 275 | 276 | app.route({ 277 | method: 'get', 278 | path: '/', 279 | controller: function (ctx, next) { 280 | ctx.body = 'This is index page' 281 | } 282 | }) 283 | 284 | return request(app.callback()) 285 | .head('/') 286 | .expect(200) 287 | .then((res) => { 288 | assert.strict.equal(res.text, undefined) 289 | }) 290 | }) 291 | }) 292 | 293 | describe('.service()', function () { 294 | it('normal', function () { 295 | const app = new Paloma() 296 | const authors = ['nswbmw', 'john', 'jack'] 297 | const posts = { 298 | nswbmw: 'one', 299 | john: 'two', 300 | jack: 'three' 301 | } 302 | 303 | app.service('Post', function () { 304 | this.getPostByUser = function (user) { 305 | return posts[user] 306 | } 307 | }) 308 | 309 | app.service('User', function (Post) { 310 | this.getUserById = function (id) { 311 | return `${authors[id]} - ${Post.getPostByUser(authors[id])}` 312 | } 313 | }) 314 | 315 | assert.strict.equal(app._bottle.container.User, app.service('User')) 316 | assert.strict.equal(app.service('User').getUserById(0), 'nswbmw - one') 317 | assert.strict.equal(app.service('User').getUserById(1), 'john - two') 318 | assert.strict.equal(app.service('User').getUserById(2), 'jack - three') 319 | assert.strict.equal(app.service('User').getUserById(3), 'undefined - undefined') 320 | }) 321 | 322 | it('http', function () { 323 | const app = new Paloma() 324 | const authors = ['nswbmw', 'john', 'jack'] 325 | 326 | app.service('User', function () { 327 | this.getUserById = function (id) { 328 | return authors[id] 329 | } 330 | }) 331 | 332 | app.controller('indexCtrl', function (ctx, next, User) { 333 | const id = +ctx.query.id 334 | ctx.body = User.getUserById(id) 335 | }) 336 | 337 | app.route({ 338 | method: 'get', 339 | path: '/', 340 | controller: 'indexCtrl' 341 | }) 342 | 343 | return request(app.callback()) 344 | .get('/') 345 | .query({ id: 0 }) 346 | .expect(200) 347 | .then((res) => { 348 | assert(res.text === 'nswbmw') 349 | }) 350 | }) 351 | 352 | it('service not exist', function () { 353 | const app = new Paloma() 354 | let error 355 | 356 | try { 357 | app.service('User') 358 | } catch (e) { 359 | error = e 360 | } 361 | 362 | assert(error.message === 'service User is not defined') 363 | }) 364 | }) 365 | 366 | it('.factory()', function () { 367 | const app = new Paloma() 368 | 369 | function UserService (authors) { 370 | this.getUserById = function (id) { 371 | return authors[id] 372 | } 373 | } 374 | app.constant('authors', ['nswbmw', 'john', 'jack']) 375 | app.factory('User', function (container) { 376 | return new UserService(container.authors) 377 | }) 378 | 379 | assert.strict.equal(app._bottle.container.User, app.service('User')) 380 | assert.strict.equal(app.service('User').getUserById(0), 'nswbmw') 381 | assert.strict.equal(app.service('User').getUserById(1), 'john') 382 | assert.strict.equal(app.service('User').getUserById(2), 'jack') 383 | assert.strict.equal(app.service('User').getUserById(3), undefined) 384 | }) 385 | 386 | it('.provider()', function () { 387 | const app = new Paloma() 388 | 389 | function UserService (authors) { 390 | this.getUserById = function (id) { 391 | return authors[id] 392 | } 393 | } 394 | app.constant('authors', ['nswbmw', 'john', 'jack']) 395 | app.provider('User', function () { 396 | this.$get = function (container) { 397 | return new UserService(container.authors) 398 | } 399 | }) 400 | 401 | assert.strict.equal(app._bottle.container.User, app.service('User')) 402 | assert.strict.equal(app.service('User').getUserById(0), 'nswbmw') 403 | assert.strict.equal(app.service('User').getUserById(1), 'john') 404 | assert.strict.equal(app.service('User').getUserById(2), 'jack') 405 | assert.strict.equal(app.service('User').getUserById(3), undefined) 406 | }) 407 | 408 | it('.constant()', function () { 409 | const app = new Paloma() 410 | app.constant('authors', ['nswbmw', 'john', 'jack']) 411 | 412 | assert.strict.deepEqual(app._bottle.container.authors, ['nswbmw', 'john', 'jack']) 413 | try { 414 | app._bottle.container.authors = [] 415 | } catch (e) { 416 | assert(e.message.match("Cannot assign to read only property 'authors' of")) 417 | } 418 | }) 419 | 420 | it('.value()', function () { 421 | const app = new Paloma() 422 | app.value('authors', ['nswbmw', 'john', 'jack']) 423 | app._bottle.container.authors = ['a', 'b', 'c'] 424 | 425 | assert.strict.deepEqual(app._bottle.container.authors, ['a', 'b', 'c']) 426 | }) 427 | 428 | it('.decorator()', function () { 429 | const app = new Paloma() 430 | const authors = ['nswbmw', 'john', 'jack'] 431 | var i = 0 432 | 433 | app.service('User', function () { 434 | this.getUserById = function (id) { 435 | return authors[id] 436 | } 437 | }) 438 | 439 | app.decorator(function (service) { 440 | ++i 441 | service.getIdByUser = function (user) { 442 | return authors.indexOf(user) 443 | } 444 | return service 445 | }) 446 | 447 | assert.strict.equal(app._bottle.container.User, app.service('User')) 448 | 449 | assert.strict.equal(app.service('User').getUserById(0), 'nswbmw') 450 | assert.strict.equal(app.service('User').getUserById(1), 'john') 451 | assert.strict.equal(app.service('User').getUserById(2), 'jack') 452 | assert.strict.equal(app.service('User').getUserById(3), undefined) 453 | 454 | assert.strict.equal(app.service('User').getIdByUser('nswbmw'), 0) 455 | assert.strict.equal(app.service('User').getIdByUser('john'), 1) 456 | assert.strict.equal(app.service('User').getIdByUser('jack'), 2) 457 | assert.strict.equal(app.service('User').getIdByUser('tom'), -1) 458 | 459 | assert.strict.equal(i, 1) 460 | }) 461 | 462 | it('.middleware()', function () { 463 | const app = new Paloma() 464 | const authors = ['nswbmw', 'john', 'jack'] 465 | var i = 0 466 | 467 | app.service('User', function () { 468 | this.getUserById = function (id) { 469 | return authors[id] 470 | } 471 | }) 472 | 473 | app.middlewares(function (service) { 474 | ++i 475 | return service 476 | }) 477 | 478 | assert.strict.equal(app._bottle.container.User, app.service('User')) 479 | 480 | assert.strict.equal(app.service('User').getUserById(0), 'nswbmw') 481 | assert.strict.equal(app.service('User').getUserById(1), 'john') 482 | assert.strict.equal(app.service('User').getUserById(2), 'jack') 483 | assert.strict.equal(app.service('User').getUserById(3), undefined) 484 | 485 | assert.strict.equal(i, 6) 486 | }) 487 | 488 | it('.Types', function () { 489 | const app = new Paloma() 490 | assert.ok(typeof app.Types === 'object') 491 | }) 492 | }) 493 | --------------------------------------------------------------------------------