Commit b0260714125b1a814ed76eaca838d164ae23101d
Merge pull request #13 from enspiral-craftworks/api
fetch and render the todos!Mikey committed on 1/8/2016, 6:06:59 AM
Parent: c8f38fdc12fc63564d14f8dc103917bdbbfcadcd
Parent: 76f81afa54247dc961664e888a428d28dbffb096
Files changed
README.md | changed |
api.js | changed |
app/api.js | changed |
app/config.js | changed |
app/package.json | changed |
app/render-browser.js | changed |
app/render.js | changed |
app/static.js | changed |
app/store.js | changed |
app/todos/reducer.js | changed |
app/todos/routes.js | changed |
app/todos/container.js | deleted |
app/todos/actions.js | added |
app/todos/components/todo-list.js | added |
app/todos/components/todo.js | added |
app/todos/containers/index.js | added |
app/todos/models.js | added |
app/todos/service.js | added |
app/util/fetch-element.js | changed |
app/client.js | added |
app/db.js | added |
app/services/index.js | deleted |
app/stack/index.js | deleted |
app/stack/services.js | deleted |
config/development.js | changed |
package.json | changed |
render.js | changed |
static.js | changed |
knexfile.js | added |
migrations/20160108004846_create_todos.js | added |
README.md | ||
---|---|---|
@@ -9,10 +9,10 @@ | ||
9 | 9 | - task runner: [npm scripts](http://substack.net/task_automation_with_npm_run) |
10 | 10 | - client bundler: [browserify](https://github.com/substack/browserify-handbook) |
11 | 11 | - es6/jsx transform: [babelify](https://www.npmjs.com/package/babelify) |
12 | 12 | - css transform: [cssify](https://www.npmjs.com/package/cssify) and [css-modules-require-hook](https://www.npmjs.com/package/css-modules-require-hook) |
13 | - - configuration: [evalify](https://www.npmjs.org/package/evalify) | |
14 | 13 | - bulk require: [bulkify](https://www.npmjs.org/package/bulkify) |
14 | +- configuration: [simple-rc](https://www.npmjs.org/package/simple-rc) | |
15 | 15 | - utility functions: [ramda](http://ramdajs.com/docs/) |
16 | 16 | - directory structure: |
17 | 17 | - `/config/` |
18 | 18 | - `/config/defaults.js` |
@@ -84,4 +84,27 @@ | ||
84 | 84 | - `components/*.js`: exports [dumb component](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) |
85 | 85 | - `components/*.css`: exports [css modules](http://glenmaddern.com/articles/css-modules) for respective component |
86 | 86 | - `service.js`: exports [`feathers`](http://feathersjs.com) service (recommended to use [`feathers-knex`](https://github.com/feathersjs/feathers-knex) and [`feathers-tcomb`](https://github.com/ahdinosaur/feathers-tcomb)) |
87 | 87 | - `client.js`: exports [`feathers-client`](https://github.com/feathersjs/feathers-client) |
88 | + | |
89 | +### setup postgres database | |
90 | + | |
91 | +install `docker` | |
92 | + | |
93 | +- to install db, run `npm run pg:pull` | |
94 | +- to create db, run `npm run pg:run` | |
95 | +- to start db, run `npm run pg:start` | |
96 | +- to stop db, run `npm run pg:stop` | |
97 | +- to remove db, run `npm run pg:rm` | |
98 | +- to show db logs, run `npm run pg:logs` | |
99 | + | |
100 | +run latest migrations with | |
101 | + | |
102 | +```shell | |
103 | +npm run knex -- migrate:latest | |
104 | +``` | |
105 | + | |
106 | +or run [any other `knex` command] with `npm run knex -- [command] [args]` | |
107 | + | |
108 | +## known issues | |
109 | + | |
110 | +- adding a new file won't always be noticed by `node-dev` or `watchify` due to usage of `bulk-require`). potential fix is to use `chokidar-cli` and some transform to watch for new files and re-run the script command |
api.js | ||
---|---|---|
@@ -1,8 +1,8 @@ | ||
1 | 1 | require('babel-core/register') |
2 | 2 | |
3 | -const config = require('app/config').default | |
4 | -const createApi = require('app/api').default | |
3 | +const config = require('app/config') | |
4 | +const createApi = require('app/api') | |
5 | 5 | const Url = require('url') |
6 | 6 | |
7 | 7 | const server = createApi(config) |
8 | 8 |
app/api.js | ||
---|---|---|
@@ -1,12 +1,34 @@ | ||
1 | +const bulk = require('bulk-require') | |
1 | 2 | import feathers from 'feathers' |
2 | -import { mapObjIndexed, reduce, toPairs } from 'ramda' | |
3 | +import hooks from 'feathers-hooks' | |
4 | +import rest from 'feathers-rest' | |
5 | +import bodyParser from 'body-parser' | |
6 | +import cors from 'cors' | |
7 | +import { map, mapObjIndexed, reduce, toPairs } from 'ramda' | |
3 | 8 | |
4 | -import services from 'app/services' | |
5 | -import config from 'app/config' | |
9 | +import memory from 'feathers-memory' | |
6 | 10 | |
7 | -export default function createServer (config) { | |
11 | +const services = Object.assign( | |
12 | + map( | |
13 | + (module) => module.service.default, | |
14 | + bulk(__dirname, '*/service.js') | |
15 | + ), | |
16 | + map( | |
17 | + (module) => module.services.map(m => m.default), | |
18 | + bulk(__dirname, '*/services/*.js') | |
19 | + ) | |
20 | +) | |
21 | + | |
22 | +export default module.exports = createApi | |
23 | + | |
24 | +function createApi (config) { | |
8 | 25 | const app = feathers() |
26 | + .use(cors()) | |
27 | + .configure(rest()) | |
28 | + .use(bodyParser.json()) | |
29 | + .use(bodyParser.urlencoded({ extended: true })) | |
30 | + .configure(hooks()) | |
9 | 31 | |
10 | 32 | useAll(app, services) |
11 | 33 | |
12 | 34 | return app |
app/config.js | ||
---|---|---|
@@ -1,3 +1,2 @@ | ||
1 | -import getConfig from 'simple-rc' | |
2 | - | |
3 | -export default getConfig() | |
1 | +process.env.NODE_ENV = process.env.NODE_ENV || 'development' | |
2 | +module.exports = module.exports['default'] = require('simple-rc')() |
app/package.json | ||
---|---|---|
@@ -7,9 +7,10 @@ | ||
7 | 7 | "transform": [ |
8 | 8 | ["cssify", { "modules": true } ], |
9 | 9 | "babelify", |
10 | 10 | "envify", |
11 | - "bulkify" | |
11 | + "bulkify", | |
12 | + ["evalify", { "files": "**/app/config.js" } ] | |
12 | 13 | ] |
13 | 14 | }, |
14 | 15 | "rc": { |
15 | 16 | "files": [ |
app/render-browser.js | ||
---|---|---|
@@ -18,9 +18,9 @@ | ||
18 | 18 | |
19 | 19 | syncReduxAndRouter(history, store) |
20 | 20 | |
21 | 21 | const main = ( |
22 | - <Router createElement={fetchElement} history={history}> | |
22 | + <Router createElement={fetchElement(store)} history={history}> | |
23 | 23 | { routes } |
24 | 24 | </Router> |
25 | 25 | ) |
26 | 26 |
app/render.js | ||
---|---|---|
@@ -14,9 +14,11 @@ | ||
14 | 14 | import createStore from 'app/store' |
15 | 15 | import routes from 'app/routes' |
16 | 16 | import fetchAllData from 'app/util/fetch-all-data' |
17 | 17 | |
18 | -export default function createRender (config) { | |
18 | +export default module.exports = createRender | |
19 | + | |
20 | +function createRender (config) { | |
19 | 21 | const staticUrl = Url.format(config.static.url) |
20 | 22 | |
21 | 23 | return http.createServer(render) |
22 | 24 |
app/static.js | ||
---|---|---|
@@ -1,7 +1,7 @@ | ||
1 | 1 | import http from 'http' |
2 | 2 | |
3 | -export default function createStatic (config) { | |
3 | +export default module.exports = function createStatic (config) { | |
4 | 4 | const ecstatic = config.livereload ? |
5 | 5 | require('ecstatic-lr') : require('ecstatic') |
6 | 6 | |
7 | 7 | return http.createServer( |
app/store.js | ||
---|---|---|
@@ -18,11 +18,13 @@ | ||
18 | 18 | applyMiddleware(...middleware) |
19 | 19 | ) |
20 | 20 | |
21 | 21 | if (process.env.NODE_ENV === 'development') { |
22 | - storeEnhancers.push( | |
23 | - applyMiddleware(logger()) | |
24 | - ) | |
22 | + if (process.browser) { | |
23 | + storeEnhancers.push( | |
24 | + applyMiddleware(logger()) | |
25 | + ) | |
26 | + } | |
25 | 27 | |
26 | 28 | storeEnhancers.push(DevTools.instrument()) |
27 | 29 | |
28 | 30 | if (module.browser) { |
app/todos/reducer.js | ||
---|---|---|
@@ -1,11 +1,5 @@ | ||
1 | -export default function todos (state = {}, action) { | |
2 | - switch (action.type) { | |
3 | - case 'CREATE_TODO': | |
4 | - return { | |
5 | - ...state, | |
6 | - [action.payload.id]: action.payload | |
7 | - } | |
8 | - default: | |
9 | - return state | |
10 | - } | |
11 | -} | |
1 | +import { createReducer } from 'feathers-action' | |
2 | + | |
3 | +import { Todos } from './models' | |
4 | + | |
5 | +export default createReducer(Todos) |
app/todos/routes.js | ||
---|---|---|
@@ -1,6 +1,8 @@ | ||
1 | 1 | import React from 'react' |
2 | -import { Route } from 'react-router' | |
2 | +import { Route, IndexRoute } from 'react-router' | |
3 | 3 | |
4 | -import TodosContainer from './container' | |
4 | +import IndexContainer from './containers/index' | |
5 | 5 | |
6 | -export default <Route path="todos" component={TodosContainer} /> | |
6 | +export default <Route path="todos"> | |
7 | + <IndexRoute component={IndexContainer} /> | |
8 | +</Route> |
app/todos/container.js | ||
---|---|---|
@@ -1,14 +1,0 @@ | ||
1 | -import React from 'react' | |
2 | -import { connect } from 'react-redux' | |
3 | - | |
4 | -class TodosContainer extends React.Component { | |
5 | - render () { | |
6 | - return <div> | |
7 | - todo list! | |
8 | - </div> | |
9 | - } | |
10 | -} | |
11 | - | |
12 | -export default connect( | |
13 | - (state) => ({}) | |
14 | -)(TodosContainer) |
app/todos/actions.js | ||
---|---|---|
@@ -1,0 +1,7 @@ | ||
1 | +import { createActions } from 'feathers-action' | |
2 | + | |
3 | +import client from 'app/client' | |
4 | + | |
5 | +import { Todos } from './models' | |
6 | + | |
7 | +export default createActions(client, Todos) |
app/todos/components/todo-list.js | ||
---|---|---|
@@ -1,0 +1,13 @@ | ||
1 | +import React from 'react' | |
2 | + | |
3 | +export default class TodoList extends React.Component { | |
4 | + render () { | |
5 | + return <ul> | |
6 | + { | |
7 | + React.Children.map(this.props.children, todo => { | |
8 | + return <li>{ todo }</li> | |
9 | + }) | |
10 | + } | |
11 | + </ul> | |
12 | + } | |
13 | +} |
app/todos/components/todo.js | ||
---|---|---|
@@ -1,0 +1,9 @@ | ||
1 | +import React from 'react' | |
2 | + | |
3 | +export default class Todo extends React.Component { | |
4 | + render () { | |
5 | + return <div> | |
6 | + { this.props.todo.text } | |
7 | + </div> | |
8 | + } | |
9 | +} |
app/todos/containers/index.js | ||
---|---|---|
@@ -1,0 +1,29 @@ | ||
1 | +import React from 'react' | |
2 | +import { connect } from 'react-redux' | |
3 | +import { map, values } from 'ramda' | |
4 | + | |
5 | +import actions from '../actions' | |
6 | +import TodoList from '../components/todo-list' | |
7 | +import Todo from '../components/todo' | |
8 | + | |
9 | +class TodosContainer extends React.Component { | |
10 | + static fetchData = (getState, dispatch, location, params) => { | |
11 | + return dispatch(actions.find()) | |
12 | + } | |
13 | + | |
14 | + render () { | |
15 | + return <TodoList> | |
16 | + { | |
17 | + values(map(todo => { | |
18 | + return <Todo todo={todo} /> | |
19 | + }, this.props.todos)) | |
20 | + } | |
21 | + </TodoList> | |
22 | + } | |
23 | +} | |
24 | + | |
25 | +export default connect( | |
26 | + (state) => ({ | |
27 | + todos: state.todos.records | |
28 | + }) | |
29 | +)(TodosContainer) |
app/todos/models.js | ||
---|---|---|
@@ -1,0 +1,9 @@ | ||
1 | +import t from 'tcomb' | |
2 | + | |
3 | +export const Todo = t.struct({ | |
4 | + id: t.Number, | |
5 | + text: t.String, | |
6 | + complete: t.Boolean | |
7 | +}, 'Todo') | |
8 | + | |
9 | +export const Todos = t.list(Todo, 'Todos') |
app/todos/service.js | ||
---|---|---|
@@ -1,0 +1,16 @@ | ||
1 | +import knexService from 'feathers-knex' | |
2 | +import validate from 'feathers-tcomb' | |
3 | + | |
4 | +import db from 'app/db' | |
5 | + | |
6 | +import { Todo } from './models' | |
7 | + | |
8 | +export default knexService({ | |
9 | + Model: db, | |
10 | + name: 'todos' | |
11 | +}) | |
12 | +//.extend({ | |
13 | +// setup: function (app) { | |
14 | +// validate(app.service('todos'), Todo) | |
15 | +// } | |
16 | +//}) |
app/util/fetch-element.js | ||
---|---|---|
@@ -1,12 +1,15 @@ | ||
1 | 1 | import React from 'react' |
2 | 2 | |
3 | -export default function fetchElement (Component, props) { | |
4 | - if (Component.fetchData) { | |
5 | - Component.fetchData( | |
6 | - store.getState, store.dispatch, | |
7 | - props.location, props.params | |
8 | - ) | |
3 | +export default function fetchElement (store) { | |
4 | + return function (Component, props) { | |
5 | + if (Component.fetchData) { | |
6 | + process.nextTick(function () { | |
7 | + Component.fetchData( | |
8 | + store.getState, store.dispatch, | |
9 | + props.location, props.params | |
10 | + ) | |
11 | + }) | |
12 | + } | |
13 | + return React.createElement(Component, props) | |
9 | 14 | } |
10 | - return React.createElement(Component, props) | |
11 | 15 | } |
12 | - |
app/client.js | ||
---|---|---|
@@ -1,0 +1,11 @@ | ||
1 | +import feathers from 'feathers-client' | |
2 | +import fetch from 'isomorphic-fetch' | |
3 | +import Url from 'url' | |
4 | + | |
5 | +import config from 'app/config' | |
6 | + | |
7 | +const clientUrl = Url.format(config.api.url) | |
8 | +const client = feathers(clientUrl) | |
9 | + .configure(feathers.fetch(fetch)) | |
10 | + | |
11 | +export default client |
app/db.js | ||
---|---|---|
@@ -1,0 +1,5 @@ | ||
1 | +import knex from 'knex' | |
2 | + | |
3 | +import config from 'app/config' | |
4 | + | |
5 | +export default knex(config.db) |
app/services/index.js | ||
---|---|---|
@@ -1,13 +1,0 @@ | ||
1 | -const bulk = require('bulk-require') | |
2 | -import { map } from 'ramda' | |
3 | - | |
4 | -export default { | |
5 | - ...map( | |
6 | - (module) => m.service.default, | |
7 | - bulk(__dirname, '*/service.js') | |
8 | - ), | |
9 | - ...map( | |
10 | - (module) => module.services.map(m => m.default), | |
11 | - bulk(__dirname, '*/services/*.js') | |
12 | - ) | |
13 | -} |
app/stack/index.js | ||
---|---|---|
@@ -1,34 +1,0 @@ | ||
1 | -import feathers from 'feathers' | |
2 | -import { mapObjIndexed, reduce, toPairs } from 'ramda' | |
3 | - | |
4 | -const stackCreators = { | |
5 | - services: require('./services'), | |
6 | - static: require('./static'), | |
7 | - render: require('./render') | |
8 | -} | |
9 | - | |
10 | -export default function createStack(config) { | |
11 | - const stacks = createStacks(config) | |
12 | - | |
13 | - const app = feathers() | |
14 | - | |
15 | - useAll(app, stacks) | |
16 | - | |
17 | - return app | |
18 | -} | |
19 | - | |
20 | -function createStacks (config) { | |
21 | - return mapObjIndexed( | |
22 | - (stackCreator, name) => { | |
23 | - return stackCreator(config[name]) | |
24 | - }, | |
25 | - stackCreators | |
26 | - ) | |
27 | -} | |
28 | - | |
29 | -function useAll (app, services) { | |
30 | - return reduce((app, [name, service]) => { | |
31 | - return app.use(service) | |
32 | - }, app, toPairs(services)) | |
33 | -} | |
34 | - |
app/stack/services.js |
---|
config/development.js | ||
---|---|---|
@@ -1,2 +1,17 @@ | ||
1 | +const join = require('path').join | |
2 | + | |
1 | 3 | module.exports = { |
4 | + db: { | |
5 | + client: 'pg', | |
6 | + connection: { | |
7 | + host : 'localhost', | |
8 | + user : 'postgres', | |
9 | + //password : 'postgres', | |
10 | + database : 'postgres' | |
11 | + }, | |
12 | + pool: { | |
13 | + min: 0, | |
14 | + max: 1 | |
15 | + } | |
16 | + } | |
2 | 17 | } |
package.json | ||
---|---|---|
@@ -2,14 +2,21 @@ | ||
2 | 2 | "name": "business-stack", |
3 | 3 | "version": "0.0.0", |
4 | 4 | "description": "real-world production-quality TodoMVC example", |
5 | 5 | "scripts": { |
6 | + "knex": "knex", | |
6 | 7 | "postinstall": "lnfs app node_modules/app", |
7 | 8 | "lint": "snazzy", |
8 | 9 | "format": "snazzy --format", |
9 | 10 | "test": "npm-run-all -p test:*", |
10 | 11 | "test:spec": "node app/spec", |
11 | 12 | "test:feature": "node app/features", |
13 | + "pg:pull": "docker pull postgres", | |
14 | + "pg:run": "docker run -d -p 5432:5432 --name=business-postgres postgres", | |
15 | + "pg:start": "docker start business-postgres", | |
16 | + "pg:stop": "docker stop business-postgres", | |
17 | + "pg:rm": "docker rm business-postgres", | |
18 | + "pg:logs": "docker logs business-postgres", | |
12 | 19 | "dev:render-browser": "BABEL_ENV=hot watchify app/render -o build/bundle.js -dv -p browserify-hmr", |
13 | 20 | "dev:render-node": "node-dev render", |
14 | 21 | "dev:assets": "cpx \"app/assets/**/*\" build -w", |
15 | 22 | "dev:livereload": "wtch -d build -e html,css,png,gif,jpg | garnish --level debug", |
@@ -55,8 +62,9 @@ | ||
55 | 62 | "babel-plugin-react-transform": "^2.0.0", |
56 | 63 | "browserify-hmr": "^0.3.1", |
57 | 64 | "cuke-tap": "^1.0.2", |
58 | 65 | "ecstatic-lr": "^1.0.1", |
66 | + "feathers-memory": "^0.5.1", | |
59 | 67 | "garnish": "^5.0.1", |
60 | 68 | "glob": "^6.0.2", |
61 | 69 | "jsdom": "^7.1.0", |
62 | 70 | "node-dev": "^2.7.1", |
@@ -83,18 +91,27 @@ | ||
83 | 91 | "babelify": "^7.2.0", |
84 | 92 | "browserify": "github:ahdinosaur/node-browserify", |
85 | 93 | "bulk-require": "^0.2.1", |
86 | 94 | "bulkify": "^1.1.1", |
95 | + "cors": "^2.7.1", | |
87 | 96 | "cpx": "^1.2.1", |
88 | 97 | "css-modules-require-hook": "^2.1.0", |
89 | 98 | "cssify": "github:ahdinosaur/cssify", |
90 | 99 | "ecstatic": "^1.4.0", |
91 | 100 | "envify": "^3.4.0", |
92 | - "evalify": "^1.0.1", | |
93 | - "feathers": "^1.2.0", | |
101 | + "evalify": "github:ahdinosaur/evalify#minimatch", | |
102 | + "feathers": "^2.0.0-pre.1", | |
103 | + "feathers-action": "^1.0.1", | |
104 | + "feathers-client": "^0.5.0", | |
105 | + "feathers-hooks": "^0.5.1", | |
106 | + "feathers-knex": "^2.0.0", | |
107 | + "feathers-rest": "^1.0.0", | |
108 | + "feathers-tcomb": "^1.0.0", | |
94 | 109 | "history": "^1.13.1", |
110 | + "isomorphic-fetch": "^2.2.0", | |
95 | 111 | "lnfs-cli": "^1.0.1", |
96 | 112 | "npm-run-all": "^1.3.2", |
113 | + "pg": "^4.4.3", | |
97 | 114 | "pinkie-promise": "^2.0.0", |
98 | 115 | "predirect": "^1.1.0", |
99 | 116 | "ramda": "^0.18.0", |
100 | 117 | "react": "^0.14.3", |
@@ -105,7 +122,8 @@ | ||
105 | 122 | "redux-simple-router": "0.0.10", |
106 | 123 | "redux-thunk": "^1.0.0", |
107 | 124 | "send-data": "^8.0.0", |
108 | 125 | "simple-rc": "^1.0.0", |
126 | + "tcomb": "^2.5.2", | |
109 | 127 | "uglifyify": "^3.0.1" |
110 | 128 | } |
111 | 129 | } |
render.js | ||
---|---|---|
@@ -1,9 +1,9 @@ | ||
1 | 1 | require('babel-core/register') |
2 | 2 | require('css-modules-require-hook') |
3 | 3 | |
4 | -const config = require('app/config').default | |
5 | -const createRender = require('app/render').default | |
4 | +const config = require('app/config') | |
5 | +const createRender = require('app/render') | |
6 | 6 | const Url = require('url') |
7 | 7 | |
8 | 8 | const server = createRender(config) |
9 | 9 |
static.js | ||
---|---|---|
@@ -1,8 +1,8 @@ | ||
1 | 1 | require('babel-core/register') |
2 | 2 | |
3 | -const config = require('app/config').default | |
4 | -const createStatic = require('app/static').default | |
3 | +const config = require('app/config') | |
4 | +const createStatic = require('app/static') | |
5 | 5 | const Url = require('url') |
6 | 6 | |
7 | 7 | const server = createStatic(config) |
8 | 8 |
knexfile.js | ||
---|---|---|
@@ -1,0 +1,1 @@ | ||
1 | +module.exports = require('app/config').db |
migrations/20160108004846_create_todos.js | ||
---|---|---|
@@ -1,0 +1,11 @@ | ||
1 | +exports.up = function(knex, Promise) { | |
2 | + return knex.schema.createTableIfNotExists('todos', function(table) { | |
3 | + table.increments('id') | |
4 | + table.string('text') | |
5 | + table.boolean('complete') | |
6 | + }) | |
7 | +} | |
8 | + | |
9 | +exports.down = function(knex, Promise) { | |
10 | + return knex.schema.dropTableIfExists('todos') | |
11 | +} |
Built with git-ssb-web