├── .gitignore ├── .babelrc ├── package.json ├── test └── index.js ├── README.md ├── src └── index.js └── lib └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", "stage-0" 4 | ], 5 | "plugins": [ 6 | "transform-runtime" 7 | ] 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cycle-express", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean": "rimraf lib", 8 | "build": "babel src -d lib", 9 | "build:watch": "babel src -w -d lib", 10 | "prepublish": "npm run clean && npm run build", 11 | "test:watch": "node-dev --respawn -r babel-core/register test", 12 | "test": "node -r babel-core/register test" 13 | }, 14 | "author": "whitecolor", 15 | "license": "ISC", 16 | "engines": { 17 | "node": ">=0.10.0" 18 | }, 19 | "dependencies": { 20 | "cuid": "^1.3.8" 21 | }, 22 | "devDependencies": { 23 | "@cycle/core": "^6.0.0", 24 | "@cycle/isolate": "^1.2.0", 25 | "ava": "^0.11.0", 26 | "babel-cli": "^6.7.5", 27 | "babel-plugin-transform-runtime": "^6.4.3", 28 | "babel-preset-es2015": "^6.6.0", 29 | "babel-preset-stage-0": "^6.5.0", 30 | "express": "^4.13.4", 31 | "rx": "^4.0.7", 32 | "rx-log": "github:whitecolor/rx-log", 33 | "supertest": "^1.1.0", 34 | "tape": "^4.4.0" 35 | }, 36 | "standard": { 37 | "ignore": "test.js" 38 | }, 39 | "ava": { 40 | "files": [ 41 | "test.js" 42 | ], 43 | "failFast": true, 44 | "_serial": true, 45 | "_tap": true, 46 | "verbose": true, 47 | "require": [ 48 | "babel-core/register" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import request from 'supertest' 3 | 4 | import {run} from '@cycle/core' 5 | import {Observable as O} from 'rx' 6 | import {makeRouterDriver} from '../lib' 7 | 8 | import 'rx-log' 9 | import test from 'tape' 10 | 11 | // simple DB driver with isolation 12 | const makeDBDriver = () => { 13 | const USERS = [ 14 | {id: '1', name: 'Charles Dickens', friends: [2]}, 15 | {id: '2', name: 'Theodore Dreiser', friends: [1, 4]}, 16 | {id: '3', name: 'Gabriel Marquez', friends: [2, 1]}, 17 | {id: '4', name: 'Lewis Carroll', friends: []}, 18 | {id: '5', name: 'Leo Tolstoy', friends: [1, 3, 4]} 19 | ] 20 | 21 | return function dbDriver (query$) { 22 | var res$ = query$.log('dbDriver') 23 | .map(query => query.id 24 | ? {query, data: USERS.filter(u => u.id === query.id)[0]} 25 | : query.ids 26 | ? {query, data: USERS.filter(u => query.ids.indexOf(u.id) >= 0)} 27 | : {query, data: USERS} 28 | ).replay(null, 1) 29 | res$.connect() 30 | return res$ 31 | } 32 | } 33 | 34 | let router = express.Router() 35 | let dbDriver = makeDBDriver() 36 | 37 | let app = express() 38 | app.use(router) 39 | 40 | 41 | 42 | const Main = ({router, db}) => { 43 | return { 44 | router: db.mergeAll(), 45 | db: O.merge([ 46 | 47 | ]) 48 | } 49 | } 50 | 51 | run(Main, { 52 | router: makeRouterDriver(), 53 | db: makeDBDriver() 54 | }) 55 | 56 | test.skip('GET /user/13', t => { 57 | request(app) 58 | .get('/user/13') 59 | //.expect(200) 60 | .expect(res => { 61 | console.log('13 status', res.status) 62 | t.is(res.body.id, '1') 63 | t.end() 64 | }) 65 | .end(output) 66 | }) 67 | 68 | test.skip('GET /user/12', t => { 69 | request(app) 70 | .get('/user/13') 71 | //.expect(200) 72 | .expect(res => { 73 | console.log('13 status', res.status) 74 | t.is(res.body.id, '1') 75 | t.end() 76 | }) 77 | }) 78 | 79 | 80 | test('GET /user/1', t => { 81 | request(app) 82 | .get('/user/1') 83 | .expect(200) 84 | .expect(res => { 85 | t.is(res.body.id, '1') 86 | t.end() 87 | }) 88 | }) 89 | 90 | test('GET /user/2', t => { 91 | request(app) 92 | .get('/user/2') 93 | .expect(200) 94 | .expect(res => { 95 | t.is(res.body.id, '2') 96 | t.end() 97 | }) 98 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cycle-Express 2 | [Express.js](http://expressjs.com/) driver for [cycle.js](http://cycle.js.org/) 3 | 4 | This is a **experimental driver** that allows you to have express router requests as stream, 5 | and use it in cycle.js apps. 6 | 7 | 8 | ## API 9 | 10 | **streams of requests** 11 | 12 | API of router object is very similar to `express.Router` 13 | 14 | ``` 15 | router.METHOD(path) // -> stream of requests 16 | ``` 17 | 18 | ```js 19 | router.get('/api/users') 20 | .filter(req => req.user) 21 | ... 22 | ``` 23 | 24 | **nested streams of requests** 25 | 26 | Method `route` internally will create new `express.Router` 27 | and `use` (attach) it on current `Router` instance that will supply you with kind of *isolated* stream of request 28 | from the `path`. 29 | 30 | ``` 31 | let nestedRouter = router.route(path) // --> nested router attached on `path` 32 | nestedRouter.METHOD(path) // -> stream of requests 33 | ``` 34 | 35 | You can use this to pass such nested router to function: 36 | 37 | ```js 38 | RouteUser({router: router.route('/api/user'), db, log}) 39 | ``` 40 | 41 | **streams of responses** 42 | 43 | Each incoming request from stream has unique `id`, you should put this 44 | `id` into to sink stream, so driver could make find a corresponding response (usually referred as `res`) 45 | object. So, such object passed to router sink steam: 46 | 47 | ```js 48 | { 49 | id: '123', 50 | status: 202, 51 | send: {a: 1, b: 2} 52 | } 53 | ``` 54 | 55 | will set response `status` to `202` and send (calling `send` method on express's `res` object) `{a: 1, b: 2}` 56 | 57 | 58 | 59 | ## Example 60 | 61 | ```js 62 | import {run} from '@cycle/core' 63 | import {Observable as O} from 'rx' 64 | import {makeRouterDriver} from 'cycle-express' 65 | import {Router} from 'express' 66 | 67 | const router = Router() 68 | 69 | const Main = ({router, cb}) => { 70 | return { 71 | router: db.map(result => ({ 72 | id: result.query.id, // send back to router object that contains original request id 73 | send: result.data 74 | )) 75 | db: router.map((req) => ({ 76 | req: req.id, // mark db query with original request id 77 | id: req.params.id // this is just "db query API" for the sake of simplicity 78 | })) 79 | } 80 | } 81 | 82 | run(Main, { 83 | router: makeRouterDriver(router), 84 | db: simpleFlatDbDriver 85 | }) 86 | 87 | ``` -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | import {Router} from 'express' 3 | import cuid from 'cuid' 4 | import methods from 'methods' 5 | 6 | var terminateMethods = [ 7 | 'download', 8 | 'end', 9 | 'json', 10 | 'jsonp', 11 | 'redirect', 12 | 'render', 13 | 'send', 14 | 'sendFile', 15 | 'sendStatus' 16 | ] 17 | 18 | var terminateMethodsMap = terminateMethods 19 | .reduce((methods, m) => { 20 | methods[m] = true 21 | return methods 22 | }, {}) 23 | 24 | const isExpressRouter = (router) => router && typeof router.use === 'function' 25 | 26 | export const makeRouterDriver = (routerOptions, options = {}) => { 27 | const router = isExpressRouter(routerOptions) 28 | ? routerOptions : Router(routerOptions) 29 | 30 | let _requests = {} 31 | 32 | const createDriverRouter = (router) => { 33 | const driverRouter = {} 34 | const createReq$ = (m, path) => { 35 | let req$ = Observable.create(observer => { 36 | router[m](path, (req, res, next) => { 37 | var id = req.id || cuid() 38 | _requests[id] = { 39 | req: req, 40 | res: res 41 | } 42 | req.id = id 43 | observer.onNext(req) 44 | }) 45 | }).replay(null, 1) 46 | req$.connect() 47 | return req$ 48 | } 49 | methods.concat('all').forEach(m => { 50 | driverRouter[m] = (path) => createReq$(m, path) 51 | }) 52 | driverRouter.method = createReq$ 53 | driverRouter.route = (path, options) => { 54 | let nestedRouter = Router() 55 | router.use(path, nestedRouter) 56 | return createDriverRouter(nestedRouter, options || routerOptions) 57 | } 58 | return driverRouter 59 | } 60 | 61 | const handle = (response) => { 62 | var _r = _requests[response.id] 63 | if (_r) { 64 | let res = _r.res 65 | let terminate 66 | let methods = [] 67 | for (let key in response) { 68 | if (typeof res[key] === 'function') { 69 | if (terminateMethodsMap[key]) { 70 | terminate = key 71 | } else { 72 | methods.push(key) 73 | } 74 | } 75 | } 76 | terminate && methods.push(terminate) 77 | methods.forEach( 78 | m => res[m](response[m]) 79 | ) 80 | if (terminate) { 81 | delete _requests[response.id] 82 | } 83 | } else { 84 | throw new Error(`request with id ${response.id} not found`) 85 | } 86 | } 87 | return (response$) => { 88 | response$.forEach(handle) 89 | return createDriverRouter(router) 90 | } 91 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.makeRouterDriver = undefined; 7 | 8 | var _rx = require('rx'); 9 | 10 | var _express = require('express'); 11 | 12 | var _cuid = require('cuid'); 13 | 14 | var _cuid2 = _interopRequireDefault(_cuid); 15 | 16 | var _methods = require('methods'); 17 | 18 | var _methods2 = _interopRequireDefault(_methods); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | var terminateMethods = ['download', 'end', 'json', 'jsonp', 'redirect', 'render', 'send', 'sendFile', 'sendStatus']; 23 | 24 | var terminateMethodsMap = terminateMethods.reduce(function (methods, m) { 25 | methods[m] = true; 26 | return methods; 27 | }, {}); 28 | 29 | var isExpressRouter = function isExpressRouter(router) { 30 | return router && typeof router.use === 'function'; 31 | }; 32 | 33 | var makeRouterDriver = exports.makeRouterDriver = function makeRouterDriver(routerOptions) { 34 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 35 | 36 | var router = isExpressRouter(routerOptions) ? routerOptions : (0, _express.Router)(routerOptions); 37 | 38 | var _requests = {}; 39 | 40 | var createDriverRouter = function createDriverRouter(router) { 41 | var driverRouter = {}; 42 | var createReq$ = function createReq$(m, path) { 43 | var req$ = _rx.Observable.create(function (observer) { 44 | router[m](path, function (req, res, next) { 45 | var id = req.id || (0, _cuid2.default)(); 46 | _requests[id] = { 47 | req: req, 48 | res: res 49 | }; 50 | req.id = id; 51 | observer.onNext(req); 52 | }); 53 | }).replay(null, 1); 54 | req$.connect(); 55 | return req$; 56 | }; 57 | _methods2.default.concat('all').forEach(function (m) { 58 | driverRouter[m] = function (path) { 59 | return createReq$(m, path); 60 | }; 61 | }); 62 | driverRouter.method = createReq$; 63 | driverRouter.route = function (path, options) { 64 | var nestedRouter = (0, _express.Router)(); 65 | router.use(path, nestedRouter); 66 | return createDriverRouter(nestedRouter, options || routerOptions); 67 | }; 68 | return driverRouter; 69 | }; 70 | 71 | var handle = function handle(response) { 72 | var _r = _requests[response.id]; 73 | if (_r) { 74 | (function () { 75 | var res = _r.res; 76 | var terminate = void 0; 77 | var methods = []; 78 | for (var key in response) { 79 | if (typeof res[key] === 'function') { 80 | if (terminateMethodsMap[key]) { 81 | terminate = key; 82 | } else { 83 | methods.push(key); 84 | } 85 | } 86 | } 87 | terminate && methods.push(terminate); 88 | methods.forEach(function (m) { 89 | return res[m](response[m]); 90 | }); 91 | if (terminate) { 92 | delete _requests[response.id]; 93 | } 94 | })(); 95 | } else { 96 | throw new Error('request with id ' + response.id + ' not found'); 97 | } 98 | }; 99 | return function (response$) { 100 | response$.forEach(handle); 101 | return createDriverRouter(router); 102 | }; 103 | }; --------------------------------------------------------------------------------