├── .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 | [](https://travis-ci.org/YerkoPalma/choo-example) [](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``
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``
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 |
--------------------------------------------------------------------------------