├── .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 |
--------------------------------------------------------------------------------