├── .travis.yml ├── .gitignore ├── api ├── lib │ ├── factory.js │ └── resource.js ├── models │ └── post.json └── index.js ├── readme.md ├── src ├── views │ ├── post.js │ ├── home.js │ └── new-post.js └── index.js ├── index.js ├── package.json └── tests └── api.spec.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | - '5' 5 | - '4' 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | npm-debug.log* 4 | .nyc_output 5 | .env 6 | db/ 7 | db-test/ -------------------------------------------------------------------------------- /api/lib/factory.js: -------------------------------------------------------------------------------- 1 | var LevelRest = require('level-rest-parser') 2 | var RestParser = require('rest-parser') 3 | 4 | module.exports = function (db, model) { 5 | var Model = new RestParser(LevelRest(db, { 6 | schema: require('../models/' + model + '.json') 7 | })) 8 | return Model 9 | } 10 | -------------------------------------------------------------------------------- /api/models/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "id": { 5 | "type": "number" 6 | }, 7 | "title": { 8 | "type": "string", 9 | "required": true 10 | }, 11 | "subtitle": { 12 | "type": "string" 13 | }, 14 | "date": { 15 | "type": "string" 16 | }, 17 | "content": { 18 | "type": "string", 19 | "required": true 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # choo-example 2 | [![Build Status](https://img.shields.io/travis/YerkoPalma/choo-example/master.svg?style=flat-square)](https://travis-ci.org/YerkoPalma/choo-example) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) 3 | 4 | > Full stack example with choo front-end 5 | 6 | ## What's this? 7 | 8 | This is the same app from [simple-example][simple-example], but with 9 | [choo][choo] front end. 10 | 11 | ## License 12 | [MIT](/license) 13 | 14 | [choo]: https://github.com/choojs/choo 15 | [simple-example]: https://github.com/YerkoPalma/simple-example 16 | -------------------------------------------------------------------------------- /src/views/post.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | 3 | function homView (state) { 4 | var post = state.posts.filter(function (p) { 5 | return p.id === state.params.post 6 | })[0] 7 | return html`
8 |
9 |

10 | ${post.title} 11 |

12 |

13 | ${post.subtitle} 14 |

15 | 16 |
17 |
18 |

19 | ${post.content} 20 |

21 |
22 |
` 23 | } 24 | module.exports = homView 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var bankai = require('bankai') 3 | var api = require('./api') 4 | 5 | var apiHandler = api.start() 6 | var assets = bankai('./src') 7 | var server = http.createServer(handler) 8 | server.listen(process.env.PORT || 8080) 9 | 10 | function handler (req, res) { 11 | var url = req.url 12 | if (/^\/api\/v/.test(url)) { 13 | apiHandler(req, res) 14 | } else if (url === '/bundle.js') { 15 | assets.js(req, res).pipe(res) 16 | } else if (url === '/bundle.css') { 17 | assets.css(req, res).pipe(res) 18 | } else if (url === '/') { 19 | assets.html(req, res).pipe(res) 20 | } else if (req.headers['accept'].indexOf('html') > 0) { 21 | assets.html(req, res).pipe(res) 22 | } else { 23 | assets.static(req).pipe(res) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "keywords": [ 3 | "choo", 4 | "full-stack", 5 | "web", 6 | "example" 7 | ], 8 | "name": "choo-example", 9 | "version": "1.0.0", 10 | "description": "Full stack example with choo front-end", 11 | "main": "index.js", 12 | "scripts": { 13 | "build": "ENV=production bankai build --verbose --assets=assets --js [ --transform envify ] src/index.js public/", 14 | "start": "node index.js", 15 | "test": "standard --verbose | snazzy && ENV=test tape 'tests/**/*.js' | tap-summary" 16 | }, 17 | "author": "YerkoPalma", 18 | "repository": "YerkoPalma/choo-example", 19 | "license": "MIT", 20 | "dependencies": { 21 | "bankai": "^8.1.1", 22 | "choo": "^6.0.0-3", 23 | "cookie-cutter": "^0.2.0", 24 | "level": "^1.7.0", 25 | "level-rest-parser": "^2.0.0", 26 | "merry": "^5.3.3", 27 | "nanostack": "^1.0.4", 28 | "rest-parser": "^1.0.6", 29 | "sheetify": "^6.1.0", 30 | "tachyons": "^4.7.4" 31 | }, 32 | "devDependencies": { 33 | "envify": "^4.1.0", 34 | "memdb": "^1.3.1", 35 | "snazzy": "^7.0.0", 36 | "standard": "^10.0.2", 37 | "tap-summary": "^3.0.2", 38 | "tape": "^4.7.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | var merry = require('merry') 2 | var level = require('level') 3 | var Resource = require('./lib/resource') 4 | var Model = require('./lib/factory') 5 | var Nanostack = require('nanostack') 6 | 7 | var app = merry() 8 | var stack = Nanostack() 9 | // push to middleware 10 | stack.push(function timeElapsed (ctx, next) { 11 | if (ctx.req.method === 'GET' && ctx.params.id === 'fake') { 12 | next(null, function (err, val, next) { 13 | if (err) console.error(err) 14 | next({ code: 500, message: 'What are you doing?' }) 15 | }) 16 | } else if (ctx.req.method === 'GET' && !ctx.params.id) { 17 | next(null, function (err, val, next) { 18 | if (err) console.error(err) 19 | ctx.res.setHeader('awesome-header', 'Header set') 20 | next() 21 | }) 22 | } else { 23 | next() 24 | } 25 | }) 26 | var resource = Resource(app, stack) 27 | 28 | var db = process.env.ENV !== 'production' 29 | ? require('memdb')() : level(process.env.DB) 30 | var Post = Model(db, 'post') 31 | 32 | var opt = { 33 | version: 1, 34 | path: 'post' 35 | } 36 | 37 | resource(Post, opt) 38 | 39 | app.route('default', function (req, res, ctx) { 40 | ctx.send(404, { message: 'not found' }) 41 | }) 42 | 43 | module.exports = app 44 | -------------------------------------------------------------------------------- /src/views/home.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | 3 | function homView (state) { 4 | var posts = state.posts || [] 5 | return html`
6 |

News 7 | 8 | Add post 9 | 10 | chevronRight icon 11 | 12 | 13 | 14 |

15 | ${posts.length === 0 ? 'No posts yet' : ''} 16 | ${posts.map(function (post) { 17 | return html`
18 | 19 |
20 |
21 |

${post.title}

22 |

23 | ${post.subtitle} 24 |

25 |

${post.date}

26 |
27 |
28 |
29 |
` 30 | })} 31 |
` 32 | } 33 | module.exports = homView 34 | -------------------------------------------------------------------------------- /src/views/new-post.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | // var addPost = require('../store/actions').addPost 3 | 4 | function homView (state, emit) { 5 | return html`
6 |
16 |
17 | 18 | 19 | Required 20 |
21 |
22 | 23 | 24 |
25 |
26 | 27 | 28 | Required 29 |
30 |
31 | 32 |
33 |
34 |
` 35 | } 36 | module.exports = homView 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var css = require('sheetify') 2 | var choo = require('choo') 3 | var http = require('http') 4 | var cookie = require('cookie-cutter') 5 | var homeView = require('./views/home') 6 | var postView = require('./views/post') 7 | var newPostView = require('./views/new-post') 8 | 9 | css('tachyons') 10 | 11 | // initialize choo 12 | var app = choo() 13 | 14 | app.use(function (state, emitter) { 15 | state.router = window.RouterInstance 16 | state.posts = [] 17 | emitter.on('addPost', function (post) { 18 | makeRequest('POST', '/api/v1/post', post, function (body, res) { 19 | state.posts.push(body.data) 20 | emitter.emit(state.events.PUSHSTATE, '/') 21 | }) 22 | }) 23 | }) 24 | 25 | app.route('/', homeView) 26 | app.route('/new', newPostView) 27 | app.route('/:post', postView) 28 | 29 | // start app 30 | var tree = app.start() 31 | document.body.appendChild(tree) 32 | 33 | function makeRequest (method, route, data, cb) { 34 | var headers = {'Content-Type': 'application/json'} 35 | if (cookie.get('token')) headers = Object.assign(headers, {'x-session-token': cookie.get('token')}) 36 | var req = http.request({ method: method, path: route, headers: headers }, function (res) { 37 | if (res.headers && res.headers['x-session-token']) { 38 | if (res.headers['timeout']) { 39 | cookie.set('token', res.headers['x-session-token'], { expires: res.headers['timeout'] }) 40 | } else { 41 | cookie.set('token', res.headers['x-session-token']) 42 | } 43 | } 44 | res.on('error', function (err) { 45 | // t.error(err) 46 | throw err 47 | }) 48 | var body = [] 49 | res.on('data', function (chunk) { 50 | body.push(chunk) 51 | }) 52 | res.on('end', function () { cb(JSON.parse(body.toString()), res) }) 53 | }) 54 | req.on('error', function (err) { 55 | // t.error(err) 56 | throw err 57 | }) 58 | if (data) { 59 | req.write(JSON.stringify(data)) 60 | } 61 | req.end() 62 | } 63 | -------------------------------------------------------------------------------- /api/lib/resource.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | function resource (app, stack) { 4 | return function (model, opt) { 5 | var prefix = '/api/v' + (opt.version || '1') 6 | var route = opt.path 7 | // index, create 8 | app.route(['GET', 'POST'], path.join(prefix, route), handler()) 9 | // show, update, delete 10 | app.route(['GET', 'PUT', 'DELETE'], path.join(prefix, route, '/:id'), handler()) 11 | 12 | if (opt.overwrite && Array.isArray(opt.overwrite)) { 13 | opt.overwrite.map(function (overwrite) { 14 | app.route(overwrite.method, overwrite.route, handler(overwrite.handler)) 15 | }) 16 | } 17 | function handler (fn) { 18 | return function (req, res, ctx) { 19 | var dispatcher = fn || dispatch(model) 20 | if (stack && stack._middleware.length > 0) { 21 | ctx.ondone = function (err) { 22 | if (err) throw err 23 | } 24 | stack.walk(ctx, function (err, data) { 25 | if (err) { 26 | if (err.code && err.message) { 27 | ctx.send(err.code, { message: err.message }) 28 | } else if (err instanceof Error) { 29 | ctx.send(500, { message: err.message }) 30 | } else { 31 | ctx.send(500, { message: 'Unknown error' }) 32 | } 33 | } else { 34 | dispatcher(req, res, ctx) 35 | } 36 | }) 37 | } else { 38 | dispatcher(req, res, ctx) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | function dispatch (model) { 45 | return function (req, res, ctx) { 46 | model.dispatch(req, Object.assign({ valueEncoding: 'json' }, ctx.params), function (err, data) { 47 | if (err) { 48 | if (err.notFound) { 49 | ctx.send(404, { message: 'resource not found' }) 50 | } else { 51 | ctx.send(500, { message: 'internal server error' }) 52 | } 53 | } else { 54 | if (!data) { 55 | if (req.method !== 'DELETE') { 56 | ctx.send(404, { message: 'resource not found' }) 57 | } else { 58 | ctx.send(200, { id: ctx.params.id }, { 'content-type': 'json' }) 59 | } 60 | } else { 61 | ctx.send(200, JSON.stringify(data), { 'content-type': 'json' }) 62 | } 63 | } 64 | }) 65 | } 66 | } 67 | module.exports = resource 68 | module.exports.dispatch = dispatch 69 | -------------------------------------------------------------------------------- /tests/api.spec.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var http = require('http') 3 | var api = require('../api') 4 | var server 5 | 6 | tape('setup', function (t) { 7 | var handler = api.start() 8 | server = http.createServer(handler) 9 | server.listen(8080) 10 | t.end() 11 | }) 12 | 13 | tape('/api/v1/post', function (t) { 14 | t.test('POST', function (assert) { 15 | assert.plan(3) 16 | var post = { title: 'Foo bar', content: 'lorem ipsum' } 17 | 18 | makeRequest('POST', '/api/v1/post', post, function (body, res) { 19 | assert.equal(res.statusCode, 200) 20 | assert.equal(post.title, body.data.title) 21 | assert.equal(post.content, body.data.content) 22 | }) 23 | }) 24 | 25 | t.test('GET and middleware', function (assert) { 26 | assert.plan(2) 27 | 28 | makeRequest('GET', '/api/v1/post', null, function (body, res) { 29 | assert.equal(res.statusCode, 200) 30 | assert.equal(res.headers['awesome-header'], 'Header set') 31 | }) 32 | }) 33 | 34 | t.test('GET /:id', function (assert) { 35 | assert.plan(3) 36 | var post = { title: 'Foo bar wow', content: 'lorem ipsum' } 37 | 38 | makeRequest('POST', '/api/v1/post', post, function (body, res) { 39 | makeRequest('GET', '/api/v1/post/' + body.data.id, null, function (body, res) { 40 | assert.equal(res.statusCode, 200) 41 | assert.equal('Foo bar wow', body.title) 42 | assert.equal('lorem ipsum', body.content) 43 | }) 44 | }) 45 | }) 46 | 47 | t.test('PUT', function (assert) { 48 | assert.plan(4) 49 | var post = { title: 'Foo bar wow', content: 'lorem ipsum' } 50 | 51 | makeRequest('POST', '/api/v1/post', post, function (body, res) { 52 | post.title = 'New Foo Title' 53 | assert.equal('Foo bar wow', body.data.title) 54 | makeRequest('PUT', '/api/v1/post/' + body.data.id, post, function (body, res) { 55 | assert.equal(res.statusCode, 200) 56 | assert.equal('New Foo Title', body.data.title) 57 | assert.equal('lorem ipsum', body.data.content) 58 | }) 59 | }) 60 | }) 61 | 62 | t.test('DELETE', function (assert) { 63 | assert.plan(3) 64 | var post = { title: 'Foo bar wow', content: 'lorem ipsum' } 65 | 66 | makeRequest('POST', '/api/v1/post', post, function (body, res) { 67 | assert.equal(res.statusCode, 200) 68 | var id = body.data.id 69 | makeRequest('DELETE', '/api/v1/post/' + id, null, function (body, res) { 70 | assert.equal(res.statusCode, 200) 71 | makeRequest('GET', '/api/v1/post/' + id, null, function (body, res) { 72 | assert.equal(res.statusCode, 404) 73 | }) 74 | }) 75 | }) 76 | }) 77 | 78 | t.test('overwrite', function (assert) { 79 | assert.end() 80 | }) 81 | 82 | t.test('middleware can cancel request', function (assert) { 83 | makeRequest('GET', '/api/v1/post/fake', null, function (body, res) { 84 | assert.equal(res.statusCode, 500) 85 | assert.equal(body.message, 'What are you doing?') 86 | t.end() 87 | }) 88 | }) 89 | 90 | function makeRequest (method, route, data, cb) { 91 | var req = http.request({ port: 8080, method: method, path: route, headers: {'Content-Type': 'application/json'} }, function (res) { 92 | res.on('error', function (err) { 93 | t.error(err) 94 | }) 95 | var body = [] 96 | res.on('data', function (chunk) { 97 | body.push(chunk) 98 | }) 99 | res.on('end', function () { 100 | var bodyString = body.toString() 101 | cb(bodyString ? JSON.parse(bodyString) : '{}', res) 102 | }) 103 | }) 104 | req.on('error', function (err) { 105 | t.error(err) 106 | }) 107 | if (data) { 108 | req.write(JSON.stringify(data)) 109 | } 110 | req.end() 111 | } 112 | }) 113 | 114 | tape('teardown', function (t) { 115 | server.close() 116 | t.end() 117 | }) 118 | --------------------------------------------------------------------------------