├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── README.md ├── docs ├── auth.md ├── create-a-release.md ├── custom-middleware.md ├── examples │ ├── client-proxy.js │ ├── common │ │ ├── plugin.js │ │ ├── repo.js │ │ └── routes.js │ ├── express-secure.js │ ├── hapi-secure.js │ ├── using-connect.js │ ├── using-express.js │ ├── using-hapi.js │ └── using-log.js └── providing-routes.md ├── lib ├── adapters │ └── log.js └── mapper.js ├── package-lock.json ├── package.json ├── test ├── .eslintrc.json ├── log.test.js ├── mapper.test.js ├── mocha.opts └── web.test.js └── web.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'eslint:recommended', 3 | env: { 4 | node: true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 6 8 | }, 9 | rules: { 10 | 'no-console': 0 11 | } 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | 4 | *~ 5 | *.log 6 | coverage.html 7 | 8 | node_modules 9 | docs/annotated 10 | docs/coverage 11 | .vscode 12 | .nyc_output 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | 4 | env: 5 | - SENECA_VER=seneca@2.x.x 6 | - SENECA_VER=seneca@3.x.x 7 | - SENECA_VER=seneca@plugin 8 | - SENECA_VER=senecajs/seneca 9 | 10 | node_js: 11 | - "12" 12 | - "10" 13 | - "8" 14 | 15 | before_script: 16 | - npm uninstall seneca 17 | - npm install $SENECA_VER 18 | 19 | after_script: 20 | - npm run coveralls 21 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 2.2.2 04/12/2022 2 | 3 | - Bump dependencies. 4 | 5 | ## 2.2.1 09/09/2018 6 | 7 | - Bump dependencies. 8 | 9 | ## 2.2.0 03/12/2017 10 | 11 | - Adds support for providing middleware. 12 | 13 | ## 2.1.0 13/06/2017 14 | 15 | - Minor fixes 16 | - Tagging as per https://senecajs-dev.tumblr.com/post/161775321262/plugin-github-tags-20170613 17 | 18 | ## 2.0.0 01/10/2016 19 | 20 | - Fix to path generation under windows (#85) 21 | - Provide option to disable body parser when using express/connect (#93) 22 | - Suffix handling in route map (#95) 23 | - Adds support for overwriting route name (#97) 24 | - Passing string as adapter has been removed (#100). Resolution: Require the module instead, 25 | - seneca-web-adapter-connect 26 | - seneca-web-adapter-express 27 | - seneca-web-adapter-hapi 28 | - seneca-web-adapter-koa1 29 | - seneca-web-adapter-koa2 30 | - Log adapter is now the only default included adapter and runs when no adapter is specified. 31 | 32 | ## 1.0.0 11/09/2016 33 | 34 | * module rebuilt from the ground up 35 | * Routing supported for hapi, express, and connect 36 | * Auth supported for hapi and express 37 | * Passport and Bell now directly supported 38 | * New adapter engine for adding custom adapters 39 | * New route mapper generating a well defined route map 40 | * Startware and Endware no longer supported 41 | * Middleware now longer supported 42 | * Message handling over transport now supported 43 | * seneca-auth no longer supported 44 | * mapRoutes, context, and setServer exported for external use 45 | * Server can now be changed at runtime 46 | * Redirects and Autohandlers now supported for all adapter types 47 | * Logging adapter added for logging routes 48 | * Support added for latest versions of seneca, express, hapi and connect 49 | * Lots of examples added 50 | * Readme updated to new spec and examples 51 | 52 | 53 | ## 0.8.0 54 | 55 | * buildcontext type functions for Hapi implementation 56 | * Logging for adding Hapi route 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 - 2016 Richard Rodger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Seneca][Logo] 2 | 3 | # seneca-web 4 | [![npm version][npm-badge]][npm-url] 5 | [![Build Status][travis-badge]][travis-url] 6 | [![Coverage Status][coveralls-badge]][coveralls-url] 7 | [![Dependency Status][david-badge]][david-url] 8 | [![Gitter chat][gitter-badge]][gitter-url] 9 | 10 | - __Sponsor:__ [nearForm][Sponsor] 11 | - __Node:__ 4.x, 6.x 12 | - __Seneca:__ 1.x - 3.x 13 | 14 | 15 | This plugin allows http requests to be mapped to seneca actions. Http actions handled 16 | locally can access the raw `request` and `response` objects. Actions handled over 17 | transport can access a reduced set of request data including payloads and headers. 18 | 19 | If you're using this module, and need help, you can: 20 | 21 | - Post a [github issue][], 22 | - Tweet to [@senecajs][], 23 | - Ask on the [Gitter][gitter-url]. 24 | 25 | If you are new to Seneca in general, please take a look at [senecajs.org][]. We have 26 | everything from tutorials to sample apps to help get you up and running quickly. 27 | 28 | 29 | ## Install 30 | ``` 31 | npm install seneca-web 32 | ``` 33 | 34 | ## Test 35 | To run tests locally, 36 | 37 | ``` 38 | npm run test 39 | ``` 40 | 41 | To obtain a coverage report, 42 | 43 | ``` 44 | npm run coverage; open docs/coverage.html 45 | ``` 46 | 47 | ## Quick example 48 | 49 | __Route map__ 50 | ```js 51 | var Routes = [{ 52 | pin: 'role:admin,cmd:*', 53 | prefix: '/v1', 54 | postfix: '/?param=true', 55 | map: { 56 | home: { 57 | GET: true, 58 | POST: true, 59 | alias: '/home' 60 | }, 61 | logout: { 62 | GET: true, 63 | redirect: '/' 64 | }, 65 | profile: { 66 | GET: true, 67 | autoreply: false 68 | }, 69 | login: { 70 | POST: true, 71 | auth: { 72 | strategy: 'local', 73 | pass: '/profile', 74 | fail: '/' 75 | } 76 | } 77 | } 78 | }] 79 | ``` 80 | 81 | 82 | ## Adapters 83 | 84 | An adapter that maps the routes to routes in a web framework must be provided via the `adapter` parameter 85 | 86 | The following adapters are provided: 87 | 88 | * [seneca-web-adapter-connect](https://github.com/senecajs/seneca-web-adapter-connect) 89 | * [seneca-web-adapter-express](https://github.com/senecajs/seneca-web-adapter-express) 90 | * [seneca-web-adapter-hapi](https://github.com/senecajs/seneca-web-adapter-hapi) 91 | * [seneca-web-adapter-koa1](https://github.com/senecajs/seneca-web-adapter-koa1) 92 | * [seneca-web-adapter-koa2](https://github.com/senecajs/seneca-web-adapter-koa2) 93 | 94 | __Hapi__ 95 | ```js 96 | 'use strict' 97 | 98 | var Hapi = require('hapi') 99 | var Seneca = require('seneca') 100 | var Web = require('../../') 101 | var Routes = require('./common/routes') 102 | var Plugin = require('./common/plugin') 103 | 104 | var config = { 105 | routes: Routes, 106 | adapter: require('seneca-web-adapter-hapi'), 107 | context: (() => { 108 | var server = new Hapi.Server() 109 | server.connection({port: 4000}) 110 | return server 111 | })() 112 | } 113 | 114 | var seneca = Seneca() 115 | .use(Plugin) 116 | .use(Web, config) 117 | .ready(() => { 118 | var server = seneca.export('web/context')() 119 | 120 | server.start(() => { 121 | console.log('server started on: ' + server.info.uri) 122 | }) 123 | }) 124 | ``` 125 | 126 | __Express__ 127 | ```js 128 | 'use strict' 129 | 130 | var Seneca = require('seneca') 131 | var Express = require('Express') 132 | var Web = require('../../') 133 | var Routes = require('./common/routes') 134 | var Plugin = require('./common/plugin') 135 | 136 | var config = { 137 | routes: Routes, 138 | adapter: require('seneca-web-adapter-express'), 139 | context: Express() 140 | } 141 | 142 | var seneca = Seneca() 143 | .use(Plugin) 144 | .use(Web, config) 145 | .ready(() => { 146 | var server = seneca.export('web/context')() 147 | 148 | server.listen('4000', () => { 149 | console.log('server started on: 4000') 150 | }) 151 | }) 152 | 153 | ``` 154 | 155 | __Connect__ 156 | ```js 157 | 'use strict' 158 | 159 | var Seneca = require('seneca') 160 | var Connect = require('connect') 161 | var Http = require('http') 162 | var Web = require('../../') 163 | var Routes = require('./common/routes') 164 | var Plugin = require('./common/plugin') 165 | 166 | var config = { 167 | routes: Routes, 168 | adapter: require('seneca-web-adapter-connect'), 169 | context: Connect() 170 | } 171 | 172 | var seneca = Seneca() 173 | .use(Plugin) 174 | .use(Web, config) 175 | .ready(() => { 176 | var connect = seneca.export('web/context')() 177 | var http = Http.createServer(connect) 178 | 179 | http.listen(4060, () => { 180 | console.log('server started on: 4060') 181 | }) 182 | }) 183 | 184 | ``` 185 | 186 | ## Plugin Configuration 187 | 188 | * `context` - optional. Routes are mapped to the context. You can provide this later by acting upon [role:web,context:*](#rolewebcontext) or calling either the [context](#context), or [setServer](#setserver) exported method. 189 | 190 | * `routes` - optional. An object identifying the routes to map. See [Providing Routes](./docs/providing-routes.md) for more details. You can add to this later acting upon [role:web,route:*](#rolewebroute) or calling the [mapRoutes](#maproutes) or [setServer](#setserver) exported method. 191 | 192 | * `adapter` - optional. the adapter to use. See [Adapters](#adapters) above for a list of supported web frameworks. You can add this later by calling the [setServer](#setserver) exported method. 193 | 194 | * `auth` - optional. Authentication provider (express only). See [Authentication](./docs/auth.md) for more details 195 | 196 | * `options` - optional. Additional options 197 | 198 | * `middleware` - object. default: null. Provide middleware functions that can be called prior the request handler. 199 | 200 | * `parseBody` - boolean. default: true. If a body parser has not been provided using `express` or `connect`, `seneca-web` will attempt to parse the body. This will not work if `body-parser` has already been used on the app. To disable this behavior, pass `{options: {parseBody: false}}` to the plugin options. 201 | 202 | ```js 203 | .use(SenecaWeb, { 204 | routes: Routes, 205 | context: express, 206 | adapter: require('seneca-web-adapter-express'), 207 | auth: Passport, 208 | options: { 209 | parseBody: false, 210 | middleware: { 211 | 'some-middleware': (req, res, next) => { 212 | next() 213 | } 214 | } 215 | } 216 | }) 217 | ``` 218 | 219 | 220 | ## Action Patterns 221 | 222 | ### role:web,route:* 223 | Define a web service as a mapping from URL routes to action patterns. 224 | 225 | ```js 226 | seneca.act('role:web', {routes: Routes}, (err, reply) => { 227 | console.log(err || reply.routes) 228 | }) 229 | ``` 230 | 231 | ### role:web,set:server 232 | 233 | Change any of the [plugin configuration options](#plugin-onfiguration). Note that only plain objects are transported across microservices. In practice, this can only really be used on the same microservice node, or to set `options` and `routes`. 234 | 235 | ```js 236 | seneca.act('role:web,set:server', { 237 | routes: Routes, 238 | context: context, 239 | adapter: require('seneca-web-adapter-express'), 240 | auth: Passport, 241 | options: {parseBody: false} 242 | }, (err, reply) => { 243 | console.log(err || reply.ok) 244 | }) 245 | ``` 246 | 247 | **For the definition expected for Routes, see [Providing Routes][providing-routes]** 248 | 249 | ## Exported Methods 250 | 251 | ### context 252 | Provides the current context so it can be used to start the server or 253 | add custom logic, strategies, or middleware. 254 | 255 | ```js 256 | var seneca = Seneca() 257 | .use(Plugin) 258 | .use(Web, config) 259 | .ready(() => { 260 | 261 | // This will be whatever server is being used. 262 | // seneca-web doesn't autostart the server, it 263 | // must first be exported and then started. 264 | var context = seneca.export('web/context')() 265 | }) 266 | ``` 267 | 268 | ### mapRoutes 269 | Allows routes to be mapped outside of using seneca directly. Provides the 270 | same functionality as `role:web,route:*`. 271 | 272 | ```js 273 | var seneca = Seneca() 274 | .use(Plugin) 275 | .use(Web, config) 276 | .ready(() => { 277 | 278 | // Provides the same functionality as seneca.act('role:web', {routes ...}) 279 | // can be used to add more routes at runtime without needing seneca. 280 | seneca.export('web/mapRoutes')(Routes, (err, reply) => { 281 | ... 282 | }) 283 | }) 284 | ``` 285 | 286 | **For the definition expected for Routes, see [Providing Routes][providing-routes]** 287 | 288 | ### setServer 289 | Allows the server and adapter to be swapped out after runtime. 290 | 291 | ```js 292 | var seneca = Seneca() 293 | .use(Plugin) 294 | .use(Web, config) 295 | .ready(() => { 296 | 297 | var config = { 298 | context: Express(), 299 | adapter: require('seneca-web-adapter-express'), 300 | routes: Routes 301 | } 302 | 303 | // Provides the same functionality as seneca.act('role:web', {routes ...}) 304 | // can be used to add more routes at runtime without needing seneca. 305 | seneca.export('web/setServer')(config, (err, reply) => { 306 | ... 307 | }) 308 | }) 309 | ``` 310 | 311 | ## Auth 312 | Both Hapi and Express support secure routing. Hapi support is via it's built in 313 | auth mechanism and allows Bell and custom strategies. Express auth is provided 314 | via passport, which supports 100s of strategies. 315 | 316 | __Secure Express routes__ 317 | ```js 318 | map: { 319 | home: { 320 | GET: true, 321 | POST: true, 322 | alias: '/' 323 | }, 324 | logout: { 325 | GET: true, 326 | redirect: '/' 327 | }, 328 | profile: { 329 | GET: true, 330 | secure: { 331 | fail: '/' 332 | } 333 | }, 334 | login: { 335 | POST: true, 336 | auth: { 337 | strategy: 'local', 338 | pass: '/profile', 339 | fail: '/' 340 | } 341 | } 342 | ``` 343 | 344 | Express routes use `auth` for `passport.authorize` and `secure` for checking the 345 | existence of `request.user`. Both secure and auth guards support fail redirects. Auth 346 | also supports pass routing. 347 | 348 | __Secure Hapi routes__ 349 | ```js 350 | map: { 351 | home: { 352 | GET: true, 353 | POST: true, 354 | alias: '/' 355 | }, 356 | profile: { 357 | GET: true, 358 | auth: { 359 | strategy: 'simple', 360 | fail: '/' 361 | } 362 | }, 363 | admin: { 364 | GET: true, 365 | auth: { 366 | strategy: 'simple', 367 | pass: '/profile', 368 | fail: '/' 369 | } 370 | } 371 | } 372 | ``` 373 | Hapi routes do not use the `secure` option. All routes are secured using `auth`. Both 374 | pass and fail redirects are supported. 375 | 376 | ## Examples 377 | A number of examples showing basic and secure usage for hapi and express as well as 378 | showing connect and log usage are provided in [./docs/examples](/senecajs/seneca-web/tree/master/docs/examples). 379 | 380 | Examples include: 381 | 382 | - Logging routes when building maps (via log adapter). 383 | - Basic expres, hapi, and connect usage. 384 | - Securing Express and Hapi routes. 385 | - Proxying some routes over to transport 386 | 387 | ## Contributing 388 | The [Senecajs org][] encourage open participation. If you feel you can help in any way, 389 | be it with documentation, examples, extra testing, or new features please get in touch. 390 | 391 | 392 | ## License 393 | Copyright (c) 2013 - 2016, Richard Rodger and other contributors. 394 | Licensed under [MIT][]. 395 | 396 | [Sponsor]: http://nearform.com 397 | [Logo]: http://senecajs.org/files/assets/seneca-logo.png 398 | [npm-badge]: https://badge.fury.io/js/seneca-web.svg 399 | [npm-url]: https://badge.fury.io/js/seneca-web 400 | [travis-badge]: https://api.travis-ci.org/senecajs/seneca-web.svg 401 | [travis-url]: https://travis-ci.org/senecajs/seneca-web 402 | [coveralls-badge]:https://coveralls.io/repos/senecajs/seneca-web/badge.svg?branch=master&service=github 403 | [coveralls-url]: https://coveralls.io/github/senecajs/seneca-web?branch=master 404 | [david-badge]: https://david-dm.org/senecajs/seneca-web.svg 405 | [david-url]: https://david-dm.org/senecajs/seneca-web 406 | [gitter-badge]: https://badges.gitter.im/senecajs/seneca.png 407 | [gitter-url]: https://gitter.im/senecajs/seneca 408 | [MIT]: ./LICENSE 409 | [Senecajs org]: https://github.com/senecajs/ 410 | [Seneca.js]: https://www.npmjs.com/package/seneca 411 | [senecajs.org]: http://senecajs.org/ 412 | [github issue]: https://github.com/senecajs/seneca-web/issues 413 | [@senecajs]: http://twitter.com/senecajs 414 | [providing-routes]: https://github.com/senecajs/seneca-web/blob/master/docs/providing-routes.md 415 | -------------------------------------------------------------------------------- /docs/auth.md: -------------------------------------------------------------------------------- 1 | # Authorization 2 | 3 | You can provide `auth` and `secure` keys to routes to indicate they should be authenticated/secured. 4 | 5 | For express, you can provide an `auth` to the root seneca-web configuration (typically this is a configured Passport instance) 6 | 7 | Please refer to the [hapi-secure](./examples/hapi-secure.js) and [express-secure](./examples/express-secure.js) for functional examples. 8 | 9 | Currently authentication is not supported with the following adapters: 10 | 11 | * seneca-web-adapter-connect 12 | * seneca-web-adapter-koa-1 13 | * seneca-web-adapter-koa-2 14 | 15 | ## Configuration 16 | 17 | The following options are used to configure authentication on routes attached with `seneca-web`. 18 | 19 | On the root `seneca-web` configuration object: 20 | 21 | * `auth` - authentication provider - used only with `express` 22 | 23 | On each route: 24 | 25 | * `auth` - an object that is passed to the underlying web framework. 26 | 27 | * `strategy` - a string identifying the strategy to use 28 | 29 | * `pass` - redirect the user somewhere upon success 30 | 31 | * `fail` - redirect the user somewhere upon failure 32 | 33 | * `secure` - a object indicating a route requires a req.user to be present (express only) 34 | 35 | * `fail` - user will be redirected here if not logged in 36 | -------------------------------------------------------------------------------- /docs/create-a-release.md: -------------------------------------------------------------------------------- 1 | # Creating a release 2 | 3 | 1. Review github issues, triage, close and merge issues related to the release. 4 | 2. Update CHANGES.md, with date release, notes, and version. 5 | 3. Pull down the repository locally on the master branch. 6 | 4. Ensure there are no outstanding commits and the branch is clean. 7 | 5. Run `npm install` and ensure all dependencies correctly install. 8 | 6. Run `npm run test` and ensure testing and linting passes. 9 | 7. Run `npm version vx.x.x -m "version x.x.x"` where `x.x.x` is the version. 10 | 8. Run `git push upstream master --tags` 11 | 9. Run `npm publish` 12 | 10. Go to the [Github release page][Releases] and hit 'Draft a new release'. 13 | 11. Paste the Changelog content for this release and add additional release notes. 14 | 12. Choose the tag version and a title matching the release and publish. 15 | 13. Notify core maintainers of the release via email. 16 | 17 | [Releases]: https://github.com/senecajs/seneca-web/releases -------------------------------------------------------------------------------- /docs/custom-middleware.md: -------------------------------------------------------------------------------- 1 | # Applying Custom Middleware 2 | 3 | Middleware allows you run functions that read from request and mutate the response prior to hitting the request handler for a given web framework. 4 | 5 | Examples of middleware functions for the supported frameworks: 6 | 7 | ```js 8 | // express / connect 9 | route.middleware = (req, res, next) => { /* ...etc... */ } 10 | 11 | // koa1 12 | route.middleware = function * (next) => { /* ...etc... */ } 13 | 14 | // koa2 15 | route.middleware = async (ctx, next) => { /* ...etc... */ } 16 | 17 | // hapi 18 | route.middleware = async (request, h) => { /* ...etc... */ } 19 | ``` 20 | 21 | In `express` and `connect` - these are functions that are called prior to the request handler. `next` is called to move to the next middleware or request handler. 22 | 23 | In `koa1` and `koa2` - middleware requires a router with a `use` function. Our tests run against `koa-router` but any similar routing libraries that expose `use` should work as well. Similar to `express` and `connect`, but you `yield next()` and `await next()` respectively. 24 | 25 | In `hapi` - the middleware is attached as a `pre` option on the router. This allows for the same options and nested arrays that hapi supports. 26 | 27 | There are two ways to provide custom middleware to a route: 28 | 29 | ## Applying through context 30 | 31 | This is the easiest, but the middleware will be applied to all requestes mounted to seneca.web. You can provide context to `seneca-web`, and apply middleware by changing the context. For hapi, this can be an `onRequest` handler that runs with every request. 32 | 33 | ```js 34 | // express family 35 | 36 | const context = new Router() 37 | 38 | // apply custom middleware to context 39 | context.use((req, res, next) => { /* ...etc... */ }) 40 | 41 | seneca.use(SenecaWeb, { 42 | context, 43 | adapter: require('seneca-web-adapter-express') 44 | }) 45 | 46 | seneca.ready(function () { 47 | const app = express() 48 | const router = this.export('web/context')() 49 | app.use('/api', router) 50 | app.listen(4000) 51 | }) 52 | 53 | // hapi 54 | 55 | const context = new Hapi.Server() 56 | context.connection({port: 4000}) 57 | 58 | // apply custom middleware to all requests 59 | context.ext({ 60 | type: 'onRequest', 61 | method: function (request, h) { /*...etc...*/ } 62 | }) 63 | 64 | seneca.use(SenecaWeb, { 65 | context, 66 | adapter: require('seneca-web-adapter-hapi') 67 | }) 68 | 69 | seneca.ready(function () { 70 | const server = seneca.export('web/context')() 71 | server.start() 72 | }) 73 | 74 | ``` 75 | 76 | ## Using defined middleware 77 | 78 | You can define an object under `middleware` key in the SenecaWeb options. The middleware will be an object with key as middleware name and value as middleware function, as follows: 79 | 80 | ### Using string keys 81 | 82 | ```js 83 | seneca.use(SenecaWeb, { 84 | middleware = { 85 | 'middleware1': (req, res, next) => { /*...etc... */ }, 86 | 'middleware2': (req, res, next) => { /*...etc...*/ } 87 | } 88 | }) 89 | ``` 90 | 91 | Then you can specify certain route to use specific middleware as follows; 92 | 93 | ```js 94 | { 95 | "routes": [ 96 | { 97 | pin: 'role:api,cmd:*', 98 | map: { 99 | ping: { 100 | GET: true, 101 | middleware: ['middleware1', 'middleware2'] 102 | }, 103 | ping: { 104 | GET: true, 105 | middleware: 'middleware1' 106 | } 107 | } 108 | } 109 | ] 110 | } 111 | ``` 112 | 113 | * When `/ping` is requested, `middleware1` and `middleware2` will be run prior to the seneca action and response handling. 114 | * When `/pong` is requested, only `middleware1` will be run. 115 | 116 | ### Using functions 117 | 118 | You can provide the `middleware` key as a function in a prior action. Prior actions allow you to proxy requests to other actions, modify the arguments and call the action normally. In the following, you can add middleware and call the regular `role:web,routes:*` action. This can allow you to specify a more specific pin and attach middleware before the routes make their way into `seneca-web`: 119 | 120 | ```js 121 | seneca.use(SenecaWeb, {...etc}) 122 | seneca.ready(function () { 123 | seneca.add('role:web,special:true,routes:*', function (msg, cb) { 124 | msg.routes.middleware = () => { /* ...etc... */ } 125 | this.prior(msg, cb) 126 | }) 127 | }) 128 | ``` 129 | -------------------------------------------------------------------------------- /docs/examples/client-proxy.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | var Express = require('express') 5 | var Seneca = require('seneca') 6 | var Web = require('../../') 7 | 8 | var Routes = require('./common/routes') 9 | var Plugin = require('./common/plugin') 10 | 11 | Seneca() 12 | .use(Plugin) 13 | .listen({port: '4041'}) 14 | .ready((err) => { 15 | if (err) return console.log(err) 16 | 17 | var seneca = Seneca() 18 | .use(Plugin) 19 | .use(Web, {adapter: require('seneca-web-adapter-express'), context: Express()}) 20 | .client({port: '4041', pin: 'role:todo,cmd:new'}) 21 | .ready(() => { 22 | seneca.act('role:web', {routes: Routes}, (err, reply) => { 23 | if (err) return console.log(err) 24 | 25 | var express = seneca.export('web/context')() 26 | express.listen(4050, (err) => { 27 | if (err) return console.log(err) 28 | 29 | console.log('express listening on 4050') 30 | console.log(reply.routes) 31 | process.exit() 32 | }) 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /docs/examples/common/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function plugin () { 4 | var seneca = this 5 | 6 | seneca.add('role:todo,cmd:list', (msg, done) => { 7 | done(null, {ok: true}) 8 | }) 9 | 10 | seneca.add('role:todo,cmd:edit', (msg, done) => { 11 | var rep = msg.response$ 12 | 13 | // Custom handlers send back request and response 14 | // Objects who may not have the same shape. Bear 15 | // in mind accessing these objects limits the 16 | // ability to swap frameworks easily. 17 | if (rep.send) { 18 | rep.send(msg.args) 19 | } 20 | else { 21 | rep(null, msg.args) 22 | } 23 | 24 | done() 25 | }) 26 | 27 | seneca.add('role:admin,cmd:validate', (msg, done) => { 28 | done(null, {ok: true, args: msg.args}) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /docs/examples/common/repo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var storage = [ 4 | {id: 1, username: 'jack', password: 'admin', displayName: 'Jack', email: 'jack@example.com'}, 5 | {id: 2, username: 'jill', password: 'admin', displayName: 'Jill', email: 'jill@example.com'} 6 | ] 7 | 8 | function findById (id, cb) { 9 | process.nextTick(() => { 10 | var idx = id - 1 11 | 12 | if (storage[idx]) { 13 | cb(null, storage[idx]) 14 | } 15 | else { 16 | cb(new Error('User ' + id + ' does not exist')) 17 | } 18 | }) 19 | } 20 | 21 | function findByUsername (username, cb) { 22 | process.nextTick(() => { 23 | for (var i = 0, len = storage.length; i < len; i++) { 24 | var record = storage[i] 25 | 26 | if (record.username === username) { 27 | return cb(null, record) 28 | } 29 | } 30 | 31 | return cb(null, null) 32 | }) 33 | } 34 | 35 | exports.users = { 36 | findById: findById, 37 | findByUsername: findByUsername 38 | } 39 | -------------------------------------------------------------------------------- /docs/examples/common/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = [ 4 | { 5 | prefix: '/todo', 6 | pin: 'role:todo,cmd:*', 7 | map: { 8 | list: true, 9 | edit: { 10 | GET: true 11 | } 12 | } 13 | }, 14 | { 15 | prefix: '/admin', 16 | pin: 'role:admin,cmd:*', 17 | map: { 18 | validate: { 19 | POST: true, 20 | alias: '/manage' 21 | } 22 | } 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /docs/examples/express-secure.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Seneca = require('seneca') 4 | var Web = require('../../') 5 | var Express = require('express') 6 | var Passport = require('passport') 7 | var Strategy = require('passport-local').Strategy 8 | var CookieParser = require('cookie-parser') 9 | var BodyParser = require('body-parser') 10 | var Session = require('express-session') 11 | var Repo = require('./common/repo') 12 | 13 | // The config for our routes 14 | var Routes = [{ 15 | pin: 'role:admin,cmd:*', 16 | map: { 17 | home: { 18 | GET: true, 19 | POST: true, 20 | alias: '/' 21 | }, 22 | logout: { 23 | GET: true, 24 | redirect: '/' 25 | }, 26 | profile: { 27 | GET: true, 28 | secure: { 29 | fail: '/' 30 | } 31 | }, 32 | login: { 33 | POST: true, 34 | auth: { 35 | strategy: 'local', 36 | pass: '/profile', 37 | fail: '/' 38 | } 39 | } 40 | } 41 | }] 42 | 43 | // Plugin to handle our routes 44 | function Plugin () { 45 | var seneca = this 46 | 47 | seneca.add('role:admin,cmd:home', (msg, done) => { 48 | done(null, {ok: true, message: 'please log in...'}) 49 | }) 50 | 51 | seneca.add('role:admin,cmd:logout', (msg, done) => { 52 | msg.request$.logout() 53 | 54 | done(null, {ok: true}) 55 | }) 56 | 57 | seneca.add('role:admin,cmd:profile', (msg, done) => { 58 | done(null, {ok: true, user: msg.args.user}) 59 | }) 60 | } 61 | 62 | // Set our custom strategy in passport, plus user serialization. 63 | Passport.use(new Strategy((username, password, cb) => { 64 | Repo.users.findByUsername(username, (err, user) => { 65 | if (err) { 66 | cb(err) 67 | } 68 | else if (!user) { 69 | cb(null, false) 70 | } 71 | else if (user.password !== password) { 72 | cb(null, false) 73 | } 74 | else { 75 | cb(null, user) 76 | } 77 | }) 78 | })) 79 | 80 | Passport.serializeUser((user, cb) => { 81 | cb(null, user.id) 82 | }) 83 | 84 | Passport.deserializeUser((id, cb) => { 85 | Repo.users.findById(id, (err, user) => { 86 | if (err) { 87 | return cb(err) 88 | } 89 | else { 90 | cb(null, user) 91 | } 92 | }) 93 | }) 94 | 95 | // Prep express 96 | var app = Express() 97 | app.use(CookieParser()) 98 | app.use(BodyParser.urlencoded({extended: true})) 99 | app.use(Session({secret: 'magically', resave: false, saveUninitialized: false})) 100 | app.use(Passport.initialize()) 101 | app.use(Passport.session()) 102 | 103 | // The config we will pass to seneca-web 104 | var config = { 105 | adapter: require('seneca-web-adapter-express'), 106 | context: app, 107 | routes: Routes, 108 | auth: Passport 109 | } 110 | 111 | // Server and start as usual. 112 | 113 | var seneca = Seneca() 114 | .use(Plugin) 115 | .use(Web, config) 116 | .ready(() => { 117 | var server = seneca.export('web/context')() 118 | 119 | server.listen('4050', (err) => { 120 | console.log(err || 'server started on: 4050') 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /docs/examples/hapi-secure.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Seneca = require('seneca') 4 | var Web = require('../../') 5 | var Hapi = require('hapi') 6 | var Basic = require('hapi-auth-basic') 7 | var Repo = require('./common/repo') 8 | 9 | // The config for our routes 10 | var Routes = [{ 11 | pin: 'role:admin,cmd:*', 12 | map: { 13 | home: { 14 | GET: true, 15 | POST: true, 16 | alias: '/' 17 | }, 18 | profile: { 19 | GET: true, 20 | auth: { 21 | strategy: 'simple', 22 | fail: '/' 23 | } 24 | }, 25 | admin: { 26 | GET: true, 27 | auth: { 28 | strategy: 'simple', 29 | pass: '/profile', 30 | fail: '/' 31 | } 32 | } 33 | } 34 | }] 35 | 36 | // Plugin to handle our routes 37 | function Plugin () { 38 | var seneca = this 39 | 40 | seneca.add('role:admin,cmd:home', (msg, done) => { 41 | done(null, {ok: true, message: 'please log in...'}) 42 | }) 43 | 44 | seneca.add('role:admin,cmd:profile', (msg, done) => { 45 | done(null, {ok: true, user: msg.args.user}) 46 | }) 47 | } 48 | 49 | var app = new Hapi.Server() 50 | app.connection({port: 4050}) 51 | 52 | function validate (request, username, password, done) { 53 | Repo.users.findByUsername(username, (err, user) => { 54 | if (err) { 55 | done(err) 56 | } 57 | else if (!user) { 58 | done(null, false) 59 | } 60 | else if (user.password !== password) { 61 | done(null, false) 62 | } 63 | else { 64 | done(null, true, user) 65 | } 66 | }) 67 | } 68 | 69 | app.register(Basic, (err) => { 70 | if (err) throw err 71 | 72 | app.auth.strategy('simple', 'basic', {validateFunc: validate}) 73 | 74 | // The config we will pass to seneca-web 75 | var config = { 76 | adapter: require('seneca-web-adapter-hapi'), 77 | context: app, 78 | routes: Routes 79 | } 80 | 81 | // Server and start as usual. 82 | 83 | var seneca = Seneca() 84 | .use(Plugin) 85 | .use(Web, config) 86 | .ready(() => { 87 | var server = seneca.export('web/context')() 88 | 89 | server.start((err) => { 90 | console.log(err || 'server started on: ' + server.info.uri) 91 | }) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /docs/examples/using-connect.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Seneca = require('seneca') 4 | var Connect = require('connect') 5 | var Http = require('http') 6 | var Web = require('../../') 7 | var Routes = require('./common/routes') 8 | var Plugin = require('./common/plugin') 9 | 10 | var config = { 11 | routes: Routes, 12 | adapter: require('seneca-web-adapter-connect'), 13 | context: Connect() 14 | } 15 | 16 | var seneca = Seneca() 17 | .use(Plugin) 18 | .use(Web, config) 19 | .ready(() => { 20 | var connect = seneca.export('web/context')() 21 | var http = Http.createServer(connect) 22 | 23 | http.listen(4060, () => { 24 | console.log('server started on: 4060') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /docs/examples/using-express.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Seneca = require('seneca') 4 | var Express = require('express') 5 | var Web = require('../../') 6 | var Routes = require('./common/routes') 7 | var Plugin = require('./common/plugin') 8 | 9 | var config = { 10 | routes: Routes, 11 | adapter: require('seneca-web-adapter-express'), 12 | context: Express() 13 | } 14 | 15 | var seneca = Seneca() 16 | .use(Plugin) 17 | .use(Web, config) 18 | .ready(() => { 19 | var server = seneca.export('web/context')() 20 | 21 | server.listen('4000', () => { 22 | console.log('server started on: 4000') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /docs/examples/using-hapi.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Hapi = require('hapi') 4 | var Seneca = require('seneca') 5 | var Web = require('../../') 6 | var Routes = require('./common/routes') 7 | var Plugin = require('./common/plugin') 8 | 9 | var config = { 10 | routes: Routes, 11 | adapter: require('seneca-web-adapter-hapi'), 12 | context: (() => { 13 | var server = new Hapi.Server() 14 | server.connection({port: 4000}) 15 | return server 16 | })() 17 | } 18 | 19 | var seneca = Seneca() 20 | .use(Plugin) 21 | .use(Web, config) 22 | .ready(() => { 23 | var server = seneca.export('web/context')() 24 | 25 | server.start(() => { 26 | console.log('server started on: ' + server.info.uri) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /docs/examples/using-log.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Seneca = require('seneca')() 4 | var Web = require('../../') 5 | 6 | // The 'log' adapter switch logs mapped input to the console window in 7 | // pretty format. The log switch can be turned used in debug scenario's 8 | // to check the routes that have been generated. 9 | 10 | Seneca.use(Web, { 11 | adapter: 'log', 12 | routes: { 13 | pin: 'role:admin,cmd:*', 14 | map: { 15 | home: { 16 | GET: true, 17 | POST: true, 18 | alias: '/' 19 | }, 20 | logout: { 21 | GET: true, 22 | redirect: '/' 23 | }, 24 | profile: { 25 | GET: true, 26 | secure: { 27 | fail: '/' 28 | } 29 | }, 30 | login: { 31 | POST: true, 32 | auth: { 33 | strategy: 'local', 34 | pass: '/profile', 35 | fail: '/' 36 | } 37 | } 38 | } 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /docs/providing-routes.md: -------------------------------------------------------------------------------- 1 | # Providing Routes 2 | 3 | At it's core, seneca-web maps seneca actions to a web framework's routes. 4 | 5 | ## Route Definition 6 | 7 | Options for what is mapped and how it behaves is performed by passing the route config object to seneca-web by either acting upon `role:web` or by calling the exported `web/mapRoutes` method. 8 | 9 | ### routes[] 10 | 11 | routes is an array of objects that define how a seneca pin is to be turned into a web route. 12 | 13 | * `pin` - the seneca actions to pin on. If an asterik is provided, it will be expanded into the commands in `map` below. 14 | 15 | * `prefix` - a prefix applied to all mapped actions 16 | 17 | * `postfix` - a suffix applied to all mapped actions. 18 | 19 | * `map` - an object or boolean defining all commands to map (see below) 20 | 21 | * `middleware` - middleware to be applied to these routes. This can be either a string or a function. If string, the middleware must be defined in the `seneca-web` options, `options.middleware`. See [Custom middleware](./providing-routes.md) 22 | 23 | ### map 24 | 25 | `map` is an object with each key assumed to be an action available under the `pin` specified at the root of `routes` 26 | 27 | Each value can be either: 28 | 29 | * `true` if true is passed, a simple `GET` is created using ${prefix}/${key}/${postfix} 30 | 31 | * `object` an object can be passed to customize the path and behavior of the route. 32 | 33 | If an object is passed, the following properties are recognized: 34 | 35 | * `middleware` - if provided, this middleware will be applied to the request prior to the requst handler. This can be either a string or a function. If string, the middleware must be defined in the `seneca-web` options, `options.middleware`. See [Custom middleware](./providing-routes.md) 36 | 37 | * `alias` - if provided, `prefix`, `postfix` and the `name` are ignored this string will be used for the path; no frills, no gimmicks. (default: false) 38 | 39 | * `auth` - if provided, this will be passed to the framework's auth setup. This is highly dependent on framework, for more details see [auth](./auth.md) 40 | 41 | * `autoreply` - if false, seneca-web will not respond to the request. The seneca action must use the framework's response action to generate the response. (default: true) 42 | 43 | * `name` - if provided, this will allow you to override the name of the action (default: the key of the object) 44 | 45 | * `redirect` - if provided, instead of returning the response of the seneca action, the user will be redirected here instead. (default: false) 46 | 47 | * `secure` - this is used in conjunction with `auth`. For more details see [auth](./auth.md) 48 | 49 | * `suffix` - if provided, this will be appended to the path. (default: false) 50 | 51 | In addition to the above, the following HTTP verbs can be provided as `true` 52 | 53 | * GET 54 | * POST 55 | * PUT 56 | * HEAD 57 | * DELETE 58 | * OPTIONS 59 | * PATCH 60 | 61 | If any of these are passed, this type of HTTP verb will be added. Any combination of verbs can be used. 62 | 63 | ### Path Generation 64 | 65 | If `alias` is used: 66 | 67 | /${alias} 68 | 69 | Otherwise: 70 | 71 | /${prefix}/${key}/${postfix}/${suffix} 72 | 73 | ### Quick Examples 74 | 75 | ```js 76 | const Routes = [{ 77 | pin: 'role:admin,cmd:*', 78 | prefix: '/v1', 79 | postfix: '/:id' 80 | map: { 81 | home: { 82 | GET: true, 83 | POST: true, 84 | alias: '/home' 85 | }, 86 | logout: { 87 | GET: true, 88 | redirect: '/' 89 | }, 90 | profile: { 91 | GET: true, 92 | autoreply: false 93 | }, 94 | login: { 95 | POST: true, 96 | auth: { 97 | strategy: 'local', 98 | pass: '/v1/profile', 99 | fail: '/' 100 | } 101 | } 102 | } 103 | }] 104 | ``` 105 | 106 | Results in: 107 | 108 | | method | route | action | notes | 109 | |-----------|-----------------|------------------------|----------------------------------------------------------------------| 110 | | GET, POST | / | role:admin,cmd:home | alias has overwritten everything else; GET/POST both added | 111 | | GET | /v1/logout/:id | role:admin,cmd:logout | user is redirected to '/' | 112 | | GET | /v1/profile/:id | role:admin,cmd:profile | no response is generated, action must respond via response$ | 113 | | POST | /v1/login/:id | role:admin,cmd:login | local auth strategy is used, user is redirected upon success/failure | 114 | 115 | ### REST example 116 | 117 | ```js 118 | const Routes = [{ 119 | prefix: '/user', 120 | pin: 'role:user,cmd:*', 121 | map: { 122 | list: {GET: true, name: ''} 123 | load: {GET: true, name: '', suffix: '/:id'}, 124 | edit: {PUT: true, name: '', suffix: '/:id'}, 125 | create: {POST: true, name: ''}, 126 | delete: {DELETE: true, name: '', suffix: '/:id'}, 127 | } 128 | }] 129 | 130 | ``` 131 | Results in: 132 | 133 | | method | route | action | 134 | |--------|-----------|----------------------| 135 | | GET | /user | role:user,cmd:list | 136 | | GET | /user/:id | role:user,cmd:load | 137 | | PUT | /user/:id | role:user,cmd:edit | 138 | | POST | /user | role:user,cmd:create | 139 | | DELETE | /user/:id | role:user,cmd:delete | 140 | -------------------------------------------------------------------------------- /lib/adapters/log.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var _ = require('lodash') 4 | 5 | module.exports = function log (options, context, auth, routes, done) { 6 | if (_.isFunction(options.sink)) { 7 | options.sink(routes) 8 | } 9 | else { 10 | console.log(JSON.stringify({routes: routes}, null, 2)) 11 | } 12 | 13 | done(null, {ok: true, routes: routes}) 14 | } 15 | -------------------------------------------------------------------------------- /lib/mapper.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Path = require('path') 4 | var Url = require('url') 5 | var _ = require('lodash') 6 | 7 | module.exports = function mapper(routePlan) { 8 | var routes = [] 9 | 10 | // routePlan can be an array or single object. 11 | routePlan = _.flatten([routePlan]) 12 | 13 | _.each(routePlan, routeSet => { 14 | _.mapKeys(routeSet.map, (value, key) => { 15 | // We need both a part and pin to have a valid route. 16 | if (!routeSet.pin || !key) { 17 | return 18 | } 19 | 20 | var defaultRoute = { 21 | prefix: false, 22 | postfix: false, 23 | suffix: false, 24 | part: false, 25 | pin: false, 26 | alias: false, 27 | methods: [], 28 | autoreply: true, 29 | redirect: false, 30 | auth: false, 31 | middleware: false, 32 | secure: false 33 | } 34 | 35 | if (routeSet.middleware) { 36 | defaultRoute.middleware = _.isArray(routeSet.middleware) 37 | ? routeSet.middleware 38 | : [routeSet.middleware] 39 | } 40 | 41 | var route = { 42 | prefix: routeSet.prefix, 43 | postfix: routeSet.postfix, 44 | pin: routeSet.pin, 45 | part: key 46 | } 47 | 48 | // Minimally viable route. 49 | route = _.merge({}, defaultRoute, route) 50 | 51 | // allows custom patterns, looks for the * in "role:todo,cmd:*" 52 | route.pattern = route.pin.replace(':*', `:${key}`) 53 | 54 | // If the value a bool the route is a simple get. 55 | if (!_.isObject(value)) { 56 | route.methods.push('GET') 57 | route.path = buildPath(route) 58 | routes.push(route) 59 | return 60 | } 61 | 62 | // Set alias', suffix, middleware and redirects. 63 | route.alias = value.alias || route.alias 64 | route.redirect = value.redirect || route.redirect 65 | route.suffix = value.suffix || route.suffix 66 | 67 | if (value.middleware) { 68 | route.middleware = _.concat( 69 | route.middleware || [], 70 | _.isArray(value.middleware) ? value.middleware : [value.middleware] 71 | ) 72 | } 73 | 74 | // allow user to overwrite the name of the route 75 | // this can be blank string (which is falsy) 76 | if (typeof value.name !== 'undefined') { 77 | route.part = value.name 78 | } 79 | 80 | // Checking specifically for false as true or missing means on. 81 | if (value.autoreply === false) { 82 | route.autoreply = false 83 | } 84 | 85 | // If there is an auth and strategy, use it. 86 | if (value.auth && value.auth.strategy) { 87 | route.auth = value.auth 88 | } 89 | 90 | // If there is a secure, set it 91 | if (value.secure) { 92 | route.secure = value.secure 93 | } 94 | 95 | _.mapKeys(value, (active, method) => { 96 | if (isValidMethod(method)) { 97 | route.methods.push(method) 98 | } 99 | }) 100 | 101 | // build the route and push to the list 102 | route.path = buildPath(route) 103 | routes.push(route) 104 | }) 105 | }) 106 | 107 | return routes 108 | } 109 | 110 | function buildPath(route) { 111 | let path = null 112 | if (route.alias) { 113 | path = Path.join('/', route.alias) 114 | } else { 115 | const prefix = route.prefix || '' 116 | const part = route.part || '' 117 | const postfix = route.postfix || '' 118 | const suffix = route.suffix || '' 119 | path = Path.join('/', prefix, part, postfix, suffix) 120 | } 121 | return Url.parse(path).path 122 | } 123 | 124 | function isValidMethod(method) { 125 | method = method || '' 126 | method = method.toString() 127 | method = method.toUpperCase() 128 | 129 | const methods = ['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'PATCH'] 130 | 131 | return _.includes(methods, method) 132 | } 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seneca-web", 3 | "description": "Http route mapping for Seneca microservices.", 4 | "version": "2.2.2", 5 | "keywords": [ 6 | "seneca", 7 | "web", 8 | "plugin" 9 | ], 10 | "author": "Richard Rodger (https://github.com/rjrodger)", 11 | "contributors": [ 12 | "Mircea Alexandru (http://alexandrumircea.ro)", 13 | "Dean McDonnell (http://github.com/mcdonnelldean)", 14 | "Mihai Dima (https://github.com/mihaidma)", 15 | "Michael Robinson (https://github.com/faceleg)", 16 | "Wyatt Preul (https://github.com/geek)", 17 | "Cristian Kiss (https://github.com/ckiss)", 18 | "Shane Lacey (https://github.com/shanel262)", 19 | "William P. Riley-Land (https://github.com/wprl)", 20 | "Adrien Becchis (https://github.com/AdrieanKhisbe)", 21 | "Marius Ursache (https://github.com/bamse16)", 22 | "Vito Tardia (https://github.com/vtardia)", 23 | "Michele Capra (https://github.com/piccoloaiutante)", 24 | "David Mark Clements (https://github.com/davidmarkclements)", 25 | "Reto Inderbitzin (https://github.com/indr)", 26 | "David Gonzalez (http://github.com/dgonzalez)", 27 | "Tyler Waters (https://github.com/tswaters)", 28 | "Isaac Kasongoyo (https://github.com/kasongoyo)" 29 | ], 30 | "license": "MIT", 31 | "main": "web.js", 32 | "scripts": { 33 | "pretest": "eslint .", 34 | "test": "mocha", 35 | "test-debug": "node --inspect-brk node_modules/mocha/bin/mocha --no-timeouts", 36 | "coveralls": "nyc --reporter=text-lcov npm test | coveralls", 37 | "coverage": "nyc --reporter=html --report-dir=docs/coverage npm test", 38 | "prettier": "prettier --write --no-semi --single-quote web.js test/*.js" 39 | }, 40 | "files": [ 41 | "LICENSE", 42 | "README.md", 43 | "web.js", 44 | "lib" 45 | ], 46 | "dependencies": { 47 | "lodash": "^4.17.15" 48 | }, 49 | "devDependencies": { 50 | "coveralls": "^3.0.7", 51 | "eslint": "^8.29.0", 52 | "mocha": "^10.1.0", 53 | "nyc": "^15.1.0", 54 | "prettier": "^2.8.0", 55 | "request": "^2.88.0", 56 | "seneca": "^3.17.0" 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "https://github.com/senecajs/seneca-web.git" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/log.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const Seneca = require('seneca') 5 | const Web = require('../') 6 | 7 | describe('log adapter', () => { 8 | it('logs routes to a sink', (done) => { 9 | var config = { 10 | options: { 11 | sink: (routes) => { 12 | assert.equal(routes.length, 1) 13 | done() 14 | }, 15 | }, 16 | routes: { 17 | pin: 'role:test,cmd:*', 18 | map: { 19 | ping: true, 20 | }, 21 | }, 22 | } 23 | 24 | Seneca({ log: 'test' }).use(Web, config) 25 | }) 26 | 27 | it('logs routes to console by default', (done) => { 28 | var config = { 29 | options: { 30 | sink: null, 31 | }, 32 | routes: { 33 | pin: 'role:test,cmd:*', 34 | map: { 35 | ping: true, 36 | }, 37 | }, 38 | } 39 | 40 | var called = false 41 | var payload = null 42 | 43 | var log = console.log 44 | console.log = (raw) => { 45 | called = true 46 | payload = JSON.parse(raw).routes 47 | } 48 | 49 | Seneca({ log: 'test' }) 50 | .use(Web, config) 51 | .ready(() => { 52 | assert.equal(called, true) 53 | assert(payload) 54 | assert.equal(payload.length, 1) 55 | 56 | console.log = log 57 | done() 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/mapper.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const Mapper = require('../lib/mapper') 5 | 6 | describe('map-routes', () => { 7 | it('handles empty input', (done) => { 8 | var result = Mapper([]) 9 | assert.equal(result.length, 0) 10 | 11 | done() 12 | }) 13 | 14 | it('Missing values are normalized', (done) => { 15 | const route = { 16 | pin: 'role:test,cmd:*', 17 | map: { 18 | ping: true, 19 | }, 20 | } 21 | 22 | var result = Mapper(route)[0] 23 | assert.deepEqual(result.methods, ['GET']) 24 | assert.equal(result.prefix, false) 25 | assert.equal(result.postfix, false) 26 | assert.equal(result.suffix, false) 27 | assert.equal(result.alias, false) 28 | assert.equal(result.autoreply, true) 29 | assert.equal(result.redirect, false) 30 | assert.equal(result.auth, false) 31 | assert.equal(result.secure, false) 32 | 33 | done() 34 | }) 35 | 36 | it('The value of quick maps are not considered', (done) => { 37 | const route = { 38 | pin: 'role:test,cmd:*', 39 | map: { 40 | ping: 0, 41 | pong: 'string', 42 | }, 43 | } 44 | 45 | var result = Mapper(route) 46 | assert.equal(result.length, 2) 47 | assert.deepEqual(result[0].methods, ['GET']) 48 | assert.deepEqual(result[1].methods, ['GET']) 49 | 50 | done() 51 | }) 52 | 53 | it('fails if no pin is provided', (done) => { 54 | const route = { 55 | map: { 56 | ping: true, 57 | }, 58 | } 59 | 60 | var result = Mapper(route) 61 | assert.equal(result.length, 0) 62 | 63 | done() 64 | }) 65 | 66 | it('can handle custom pins', (done) => { 67 | const route = { 68 | pin: 'ns:api,handle:*', 69 | map: { 70 | ping: true, 71 | }, 72 | } 73 | 74 | var result = Mapper(route)[0] 75 | assert.equal(result.pin, 'ns:api,handle:*') 76 | assert.equal(result.pattern, 'ns:api,handle:ping') 77 | assert.equal(result.path, '/ping') 78 | 79 | done() 80 | }) 81 | 82 | it('can specify custom route alias', (done) => { 83 | const route = { 84 | pin: 'role:api,cmd:*', 85 | map: { 86 | ping: { 87 | alias: 'foo/bar', 88 | GET: 'true', 89 | }, 90 | }, 91 | } 92 | 93 | var result = Mapper(route)[0] 94 | assert.equal(result.path, '/foo/bar') 95 | done() 96 | }) 97 | 98 | it('can specify custom auto reply', (done) => { 99 | const route = { 100 | pin: 'role:api,cmd:*', 101 | map: { 102 | ping: { 103 | autoreply: false, 104 | GET: 'true', 105 | }, 106 | }, 107 | } 108 | 109 | var result = Mapper(route)[0] 110 | assert.equal(result.autoreply, false) 111 | 112 | done() 113 | }) 114 | 115 | it('prefixes prefix, postfixes postfix, suffixes suffix', (done) => { 116 | const route = { 117 | pin: 'role:test,cmd:*', 118 | prefix: 'api', 119 | postfix: 'v1', 120 | map: { 121 | ping: { 122 | GET: true, 123 | suffix: '/:param', 124 | }, 125 | }, 126 | } 127 | 128 | var result = Mapper(route)[0] 129 | assert.equal(result.prefix, 'api') 130 | assert.equal(result.postfix, 'v1') 131 | assert.equal(result.suffix, '/:param') 132 | assert.equal(result.path, '/api/ping/v1/:param') 133 | 134 | done() 135 | }) 136 | 137 | it('does not need a prefix or postfix', (done) => { 138 | const route = { 139 | pin: 'role:test,cmd:*', 140 | map: { 141 | ping: true, 142 | }, 143 | } 144 | 145 | var result = Mapper(route)[0] 146 | assert.deepEqual(result.methods, ['GET']) 147 | assert.equal(result.path, '/ping') 148 | 149 | done() 150 | }) 151 | 152 | it('allows overwriting of the key', (done) => { 153 | const route = { 154 | pin: 'role:user,cmd:*', 155 | map: { 156 | a: { GET: true, name: 'w' }, 157 | b: { GET: true, name: 'x' }, 158 | c: { GET: true, name: 'y' }, 159 | d: { GET: true, name: 'z' }, 160 | }, 161 | } 162 | 163 | var results = Mapper(route) 164 | 165 | assert.deepEqual( 166 | results.map((result) => result.path), 167 | ['/w', '/x', '/y', '/z'] 168 | ) 169 | done() 170 | }) 171 | 172 | describe('specifying middleware', () => { 173 | let route = null 174 | 175 | beforeEach((done) => { 176 | route = { 177 | pin: 'role:api,cmd:*', 178 | map: { 179 | ping: { 180 | GET: 'true', 181 | }, 182 | }, 183 | } 184 | done() 185 | }) 186 | 187 | it('at root, string', (done) => { 188 | route.middleware = 'middleware' 189 | const result = Mapper(route) 190 | assert.deepEqual(result[0].middleware, ['middleware']) 191 | done() 192 | }) 193 | 194 | it('at root, array', (done) => { 195 | route.middleware = ['middleware'] 196 | const result = Mapper(route) 197 | assert.deepEqual(result[0].middleware, ['middleware']) 198 | done() 199 | }) 200 | 201 | it('per route, string', (done) => { 202 | route.map.ping.middleware = 'middleware' 203 | const result = Mapper(route) 204 | assert.deepEqual(result[0].middleware, ['middleware']) 205 | done() 206 | }) 207 | 208 | it('per route, array', (done) => { 209 | route.map.ping.middleware = ['middleware'] 210 | const result = Mapper(route) 211 | assert.deepEqual(result[0].middleware, ['middleware']) 212 | done() 213 | }) 214 | 215 | it('value overwrites root', (done) => { 216 | route.middleware = ['by our powers'] 217 | route.map.ping.middleware = ['combined!'] 218 | const result = Mapper(route) 219 | assert.deepEqual(result[0].middleware, ['by our powers', 'combined!']) 220 | done() 221 | }) 222 | }) 223 | }) 224 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 5000 2 | test/*.test.js 3 | -------------------------------------------------------------------------------- /test/web.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | describe('web', () => { 4 | it('n/a', (done) => { 5 | done() 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /web.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const LogAdapter = require('./lib/adapters/log') 4 | var Mapper = require('./lib/mapper') 5 | var _ = require('lodash') 6 | 7 | var opts = { 8 | routes: null, 9 | context: null, 10 | adapter: LogAdapter, 11 | auth: null, 12 | options: { 13 | parseBody: true, 14 | }, 15 | } 16 | 17 | var locals = { 18 | context: null, 19 | adapter: null, 20 | options: null, 21 | } 22 | 23 | module.exports = function web(options) { 24 | var seneca = this 25 | var extend = seneca.util.deepextend 26 | 27 | // Avoid deepextending context and auth, 28 | // they aren't stringify friendly 29 | opts = extend(opts, _.omit(options, ['context', 'auth'])) 30 | opts.context = options.context || null 31 | opts.auth = options.auth || null 32 | if (options.middleware) { 33 | opts.options.middleware = options.middleware 34 | } 35 | 36 | seneca.add('role:web,routes:*', mapRoutes) 37 | seneca.add('role:web,set:server', setServer) 38 | seneca.add('init:web', init) 39 | 40 | // exported functions, they can be called 41 | // via seneca.export('web/key'). 42 | var exported = { 43 | setServer: setServer.bind(seneca), 44 | mapRoutes: mapRoutes.bind(seneca), 45 | context: () => { 46 | return locals.context 47 | }, 48 | } 49 | 50 | return { 51 | name: 'web', 52 | exportmap: exported, 53 | } 54 | } 55 | 56 | // Creates a route-map and passes it to a given adapter. The msg can 57 | // optionally contain a custom adapter or context for once off routing. 58 | function mapRoutes(msg, done) { 59 | var seneca = this 60 | var adapter = msg.adapter || locals.adapter 61 | var context = msg.context || locals.context 62 | var options = msg.options || locals.options 63 | var routes = Mapper(msg.routes) 64 | var auth = msg.auth || locals.auth 65 | 66 | // Call the adaptor with the mapped routes, context to apply them to 67 | // and instance of seneca and the provided consumer callback. 68 | adapter.call(seneca, options, context, auth, routes, done) 69 | } 70 | 71 | // Sets the 'default' server context. Any call to mapRoutes will use this server 72 | // as it's context if none is provided. This is the server returned by getServer. 73 | function setServer(msg, done) { 74 | var seneca = this 75 | var context = msg.context || locals.context 76 | var adapter = msg.adapter || locals.adapter 77 | var options = msg.options || locals.options 78 | var auth = msg.auth || locals.auth 79 | var routes = msg.routes 80 | 81 | // If the adapter is a string, we look up the 82 | // adapters collection in opts.adapters. 83 | if (!_.isFunction(adapter)) { 84 | return done(new Error('Provide a function as adapter')) 85 | } 86 | 87 | // either replaced or the same. Regardless 88 | // this sets what is called by mapRoutes. 89 | locals = { 90 | context: context, 91 | adapter: adapter, 92 | auth: auth, 93 | options: options, 94 | } 95 | 96 | // If we have routes in the msg map them and 97 | // let the matter handle the callback 98 | if (routes) { 99 | mapRoutes.call(seneca, { routes: routes }, done) 100 | } else { 101 | // no routes to process, let the 102 | // caller know everything went ok. 103 | done(null, { ok: true }) 104 | } 105 | } 106 | 107 | // This is called as soon as the plugin is loaded (when it 108 | // returns). Any routes or customisations passed via options 109 | // will be processed now via a call to setServer. 110 | function init(msg, done) { 111 | var config = { 112 | context: opts.context, 113 | adapter: opts.adapter, 114 | routes: opts.routes, 115 | auth: opts.auth, 116 | options: opts.options, 117 | } 118 | 119 | setServer.call(this, config, done) 120 | } 121 | --------------------------------------------------------------------------------