├── .babelrc ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── config ├── api.webpack.config.js ├── client.config.js ├── client.webpack.config.js └── index.js ├── docker-compose.yml ├── ecosystem.config.js ├── package.json ├── src ├── Dockerfile ├── api │ ├── Dockerfile │ ├── index.js │ ├── knexfile.js │ ├── models │ │ ├── Account.js │ │ ├── RootMutation.js │ │ ├── RootQuery.js │ │ ├── Transaction.js │ │ ├── TransactionType.js │ │ └── User.js │ └── schema.js ├── client │ ├── Dockerfile │ ├── components │ │ └── Header │ │ │ ├── Header.js │ │ │ ├── Header.scss │ │ │ └── index.js │ ├── containers │ │ └── AppContainer.js │ ├── devServer.js │ ├── index.js │ ├── layouts │ │ └── CoreLayout │ │ │ ├── CoreLayout.js │ │ │ ├── CoreLayout.scss │ │ │ └── index.js │ ├── lib │ │ └── apolloClient.js │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── routes │ │ ├── Counter │ │ │ ├── components │ │ │ │ └── Counter.js │ │ │ ├── containers │ │ │ │ └── CounterContainer.js │ │ │ ├── index.js │ │ │ └── modules │ │ │ │ └── counter.js │ │ ├── Home │ │ │ ├── assets │ │ │ │ ├── graphql-logo.svg │ │ │ │ ├── react-logo.png │ │ │ │ └── redux-logo.png │ │ │ ├── components │ │ │ │ ├── HomeView.js │ │ │ │ └── HomeView.scss │ │ │ └── index.js │ │ ├── Users │ │ │ ├── components │ │ │ │ └── Users.js │ │ │ ├── containers │ │ │ │ └── UsersContainer.js │ │ │ ├── index.js │ │ │ └── modules │ │ │ │ └── users.js │ │ └── index.js │ ├── store │ │ ├── configureStore.js │ │ ├── location.js │ │ └── reducers.js │ └── styles │ │ ├── _base.scss │ │ └── core.scss ├── db │ ├── Dockerfile │ ├── lib │ │ └── db.js │ ├── migrations │ │ └── 20161228171145_initial-schema.js │ └── seeds │ │ └── 000-initial-seed.js └── nginx │ ├── Dockerfile │ ├── certs │ └── .gitignore │ ├── nginx.conf │ ├── scripts │ └── build-nginx.sh │ └── sites │ ├── node-https.template │ └── node.template └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react" 5 | ], 6 | "plugins": [ 7 | [ 8 | "transform-object-rest-spread", 9 | { 10 | "useBuiltIns": true 11 | } 12 | ], 13 | // https://github.com/babel/babel/issues/2877 14 | [ 15 | "transform-runtime", 16 | { 17 | "polyfill": false 18 | } 19 | ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | *.temp 4 | *.tmp 5 | *.todo 6 | dist 7 | .pm2 8 | .git 9 | *.md 10 | .cache 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # special property that should be specified at the 4 | # top of the file outside of any sections. Set to true to stop 5 | # .editorconfig files search on current file. 6 | root = true 7 | 8 | [*] 9 | # File character encoding 10 | # Possible values - latin1, utf-8, utf-16be, utf-16le 11 | charset = utf-8 12 | # Indentation style 13 | # Possible values - tab, space 14 | indent_style = space 15 | # Indentation size in single-spaced characters 16 | # Possible values - an integer, tab 17 | indent_size = 2 18 | # Line ending file format 19 | # Possible values - lf, crlf, cr 20 | end_of_line = lf 21 | # Denotes whether file should end with a newline 22 | # Possible values - true, false 23 | insert_final_newline = true 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = false 27 | max_line_length = 80 28 | 29 | [*.md] 30 | trim_trailing_whitespace = false 31 | max_line_length = 0 32 | 33 | [Makefile] 34 | indent_style = tab 35 | indent_size = 8 36 | 37 | [COMMIT_EDITMSG] 38 | max_line_length = 0 39 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | */node_modules/ 2 | */.vscode 3 | dist 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "globals": { 8 | "__DEV__": true, 9 | "__PROD__": true 10 | }, 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:react/recommended" 14 | ], 15 | "parserOptions": { 16 | "ecmaVersion": 2017, 17 | "ecmaFeatures": { 18 | "experimentalObjectRestSpread": true, 19 | "classes": true, 20 | "jsx": true 21 | }, 22 | "sourceType": "module" 23 | }, 24 | "plugins": [ 25 | "react", 26 | "graphql" 27 | ], 28 | "rules": { 29 | "indent": [ 30 | "error", 31 | 2 32 | ], 33 | "max-len": [ 34 | "error", 35 | { 36 | "code": 80, 37 | "tabWidth": 4, 38 | "ignoreComments": true, 39 | "ignoreUrls": true, 40 | "ignoreStrings": true, 41 | "ignoreTemplateLiterals": true, 42 | "ignoreRegExpLiterals": true 43 | } 44 | ], 45 | "linebreak-style": [ 46 | "error", 47 | "unix" 48 | ], 49 | "quotes": [ 50 | "error", 51 | "single" 52 | ], 53 | "semi": [ 54 | "error", 55 | "never" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # temp 40 | *.sqlite3 41 | *.temp 42 | *.tmp 43 | *.todo 44 | data 45 | dist 46 | .pm2 47 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/dist": true, 4 | "**/.pm2": true, 5 | "**/.min.*": true, 6 | "**/*.lock": true 7 | // "node_modules": true 8 | }, 9 | "eslint.enable": true 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jan Carlo Viray 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Use bash not sh 2 | SHELL := /bin/bash 3 | 4 | # ./node_modules/.bin on the PATH 5 | # `export` is only limited to this process and does not export to $(shell) 6 | export PATH := ./node_modules/.bin:$(PATH) 7 | 8 | # default values that can be overwritten. ifndef = if not defined in environment 9 | # example: `NODE_ENV=production make build` will overwrite this 10 | ifndef NODE_ENV 11 | NODE_ENV = development 12 | endif 13 | 14 | ifndef DEBUG 15 | DEBUG = app:* 16 | endif 17 | 18 | ifndef CLIENT_PORT 19 | CLIENT_PORT = 3000 20 | endif 21 | 22 | ifndef API_PORT 23 | API_PORT = 8080 24 | endif 25 | 26 | PROJECT_NAME = $(shell cat package.json | grep '"name"' | head -1 | awk -F: '{ print $$2 }' | sed 's/[",]//g' | tr -d '[[:space:]]') 27 | KNEXFILE = ./src/api/knexfile.js 28 | DOCKER_COMPOSE_FILE = ./docker-compose.yml 29 | DEVDB = ./$(shell grep -o "\([[:alpha:]]*\.sqlite3\)" ${KNEXFILE}) 30 | DEFAULT_MIGRATION_NAME = migration 31 | DEFAULT_SEED_NAME = $(shell date +%s)-new-seed 32 | 33 | .PHONY: build 34 | build: clean build-client build-api 35 | 36 | .PHONY: build-client 37 | build-client: 38 | NODE_ENV=production webpack --config ./config/client.webpack.config.js 39 | 40 | .PHONY: build-api 41 | build-api: 42 | NODE_ENV=production webpack --config ./config/api.webpack.config.js 43 | 44 | .PHONY: install 45 | install: 46 | rm -rf ./node_modules 47 | type yarn && yarn install || npm install 48 | [[ -f $(DEVDB) ]] || $(MAKE) setup-db 49 | 50 | .PHONY: start 51 | start: kill 52 | @if [[ $(NODE_ENV) == "production" ]] ; then \ 53 | echo "In production mode:" ; \ 54 | $(MAKE) build ; \ 55 | NODE_ENV=$(NODE_ENV) DEBUG=$(DEBUG) CLIENT_PORT=$(CLIENT_PORT) API_PORT=$(API_PORT) \ 56 | PM2_HOME='.pm2' pm2 start ecosystem.config.js --name=$(PROJECT_NAME) --env production ; \ 57 | echo "client port at $(CLIENT_PORT) and api port at $(API_PORT)" ; \ 58 | else \ 59 | echo "In development mode:" ; \ 60 | NODE_ENV=$(NODE_ENV) DEBUG=$(DEBUG) CLIENT_PORT=$(CLIENT_PORT) API_PORT=$(API_PORT) \ 61 | PM2_HOME='.pm2' pm2 start ecosystem.config.js --name=$(PROJECT_NAME) --env development ; \ 62 | echo "client port at $(CLIENT_PORT) and api port at $(API_PORT)" ; \ 63 | fi 64 | 65 | .PHONY: kill 66 | kill: 67 | PM2_HOME='.pm2' pm2 kill 68 | @-rm -rf .pm2/* 69 | 70 | .PHONY: monit 71 | monit: 72 | PM2_HOME='.pm2' pm2 monit 73 | 74 | .PHONY: lint 75 | lint: 76 | eslint . --fix 77 | 78 | .PHONY: migrate 79 | migrate: 80 | NODE_ENV=$(NODE_ENV) \ 81 | knex --knexfile $(KNEXFILE) migrate:latest 82 | 83 | .PHONY: rollback 84 | rollback: 85 | NODE_ENV=$(NODE_ENV) \ 86 | knex --knexfile $(KNEXFILE) migrate:rollback 87 | 88 | .PHONY: seed 89 | seed: 90 | NODE_ENV=$(NODE_ENV) \ 91 | knex --knexfile $(KNEXFILE) seed:run 92 | 93 | .PHONY: new-migration 94 | new-migration: 95 | NODE_ENV=$(NODE_ENV) \ 96 | knex --knexfile $(KNEXFILE) migrate:make $(DEFAULT_MIGRATION_NAME) 97 | 98 | .PHONY: new-seed 99 | new-seed: 100 | NODE_ENV=$(NODE_ENV) \ 101 | knex --knexfile $(KNEXFILE) seed:make $(DEFAULT_SEED_NAME) 102 | 103 | .PHONY: setup-db 104 | setup-db: 105 | [[ ! -f $(DEVDB) ]] || rm $(DEVDB) 106 | NODE_ENV=$(NODE_ENV) $(MAKE) migrate 107 | NODE_ENV=$(NODE_ENV) $(MAKE) seed 108 | 109 | .PHONY: do-build 110 | do-build: 111 | NODE_ENV=$(NODE_ENV) DEBUG=$(DEBUG) CLIENT_PORT=$(CLIENT_PORT) API_PORT=$(API_PORT) \ 112 | docker-compose -f $(DOCKER_COMPOSE_FILE) -p $(PROJECT_NAME) build 113 | 114 | .PHONY: do-up 115 | do-up: 116 | NODE_ENV=$(NODE_ENV) DEBUG=$(DEBUG) CLIENT_PORT=$(CLIENT_PORT) API_PORT=$(API_PORT) \ 117 | docker-compose -f $(DOCKER_COMPOSE_FILE) -p $(PROJECT_NAME) up 118 | 119 | .PHONY: do-down 120 | do-down: 121 | NODE_ENV=$(NODE_ENV) DEBUG=$(DEBUG) CLIENT_PORT=$(CLIENT_PORT) API_PORT=$(API_PORT) \ 122 | docker-compose -f $(DOCKER_COMPOSE_FILE) -p $(PROJECT_NAME) down 123 | 124 | .PHONY: deploy-client 125 | deploy-client: 126 | 127 | .PHONY: deploy-api 128 | deploy-api: 129 | 130 | # `-` means don't break targets dependent on this 131 | # even if it causes error like if files do not exist 132 | .PHONY: clean 133 | clean: 134 | -rm -rf ./dist 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL React Starter 2 | 3 | GraphQL Node.js Starter is a hot-reloading boilerplate using [Node.js](https://nodejs.org/), [JavaScript](https://developer.mozilla.org/docs/Web/JavaScript) through [Babel](http://babeljs.io/) and using [GraphQL](http://graphql.org/) for API creations and consumption. For client-side, this uses [React](https://facebook.github.io/react/), [React-Router](https://github.com/ReactTraining/react-router) and [Redux](https://github.com/reactjs/redux). For the database portion, this uses [PostgreSQL](https://www.postgresql.org/) and a query-builder [Knex.js](http://knexjs.org/) 4 | 5 | The purpose of this starter kit is to be as close to being a real-world starter while keeping it simple and flexible. 6 | 7 | **Current Status: still in development, but feel free to fork and contribute.** 8 | 9 | ## Installation 10 | 11 | ```shell 12 | # Installs dependencies and sets up the development database. This uses `yarn` 13 | # if you have it but defaults to `npm` if you don't 14 | make install 15 | ``` 16 | 17 | ## Development 18 | 19 | ```shell 20 | # In development mode, everything is streamed from the source code. No files are 21 | # created (except the in-memory compilation of webpack-middleware). This allows 22 | # hot reloading to happen. 23 | 24 | # start both api and client process 25 | make start 26 | 27 | # Uses PM2 for watch, management, monitoring. The meta data is inside this 28 | # directory's .pm2. You must prepend that in order to access pm2 commands. 29 | PM2_HOME='.pm2' ./node_modules/.bin/pm2 list 30 | 31 | # alternatively, you can use: 32 | npm run pm2 [list|help|..] 33 | ``` 34 | 35 | ## Production 36 | 37 | ```shell 38 | # Compiles and minifies both api and client into ./dist 39 | make build 40 | 41 | # Next, compress and copy the entire ./dist to a proper server. You do not need 42 | # to copy anything else from the repository. 43 | 44 | # Run the client. `http-server` is just an example server. Use Nginx, Apache or 45 | # whatever you like to run the client. Everything you need is compiled in .dist. 46 | NODE_ENV=production CLIENT_PORT=3000 API_PORT=8080 http-server ./dist 47 | 48 | # Run the API. You must use `node` here. All ES2015+ syntax have been compiled. 49 | # If you are getting unexpected token errors, upgrade your node binaries. Note 50 | # that this is expecting a PostgreSQL server. You can change the options in the 51 | # `knexfile.js` and use a different database if you want. 52 | NODE_ENV=production API_PORT=8080 node ./api/index.js 53 | ``` 54 | 55 | ## Docker 56 | 57 | ```shell 58 | make do-up 59 | ``` 60 | 61 | ## Roadmap and Status 62 | 63 | - [x] Hot Reloading 64 | - [x] React 65 | - [x] Redux 66 | - [x] Webpack 2 67 | - [x] React Router 68 | - [x] ESLint 69 | - [x] .editorconfig 70 | - [x] GraphQL integration with Express 71 | - [x] Modularize Schema and Resolvers 72 | - [x] Database Schema with relationships (1-1/1-M/M-M) 73 | - [x] Separate API Server and Client server 74 | - [x] Production vs Development build 75 | - [x] GraphQL client integration: Apollo 76 | - [x] GraphQL + Redux 77 | - [x] GraphQL query example 78 | - [x] GraphQL mutation example 79 | - [x] Convert scripts to Makefile 80 | - [x] Process Management 81 | - [x] Dockerize Each Layer 82 | - [x] docker-compose up 83 | 84 | ## Schema-First Design Steps 85 | 86 | This centers your application development around your feature requirements, skewed a bit towards UI. Having the graphql abstraction allows you to focus on features rather than modeling your data and queries based on a rigid database schema. 87 | 88 | ### Define Schema 89 | 90 | Describe the [graphql schema](http://graphql.org/learn/schema/) centered around your front-end requirements. This is not the same as Database Design, though in many cases, the schema could be a direct representation of your table relationships. 91 | 92 | ### Define Resolvers 93 | 94 | Define the [resolvers](http://graphql.org/learn/execution/#root-fields-resolvers), to match entities from your schema 95 | 96 | ### Create Mocks 97 | 98 | Mocking APIs are typically time consuming and often becomes a waste as API changes. [graphql-tools](http://dev.apollodata.com/tools/graphql-tools/mocking.html) has a mocking library that allows you to map values based on types or field names. Very useful, especially if synchronized with mocking library like faker.js or casual.js 99 | 100 | ### Create or Update Database 101 | 102 | Being that GraphQL is an abstraction that is somewhat geared towards UI requirements, there is no need to map a one-to-one schema between GraphQL schema and Database Schema. Through the resolver, we can morph and transform and even fetch extra data without being constricted with the database schema. This enables faster iteration and prototyping. 103 | 104 | ## Dependencies 105 | 106 | - [apollo-client](https://github.com/apollostack/apollo-client): A simple yet functional GraphQL client. 107 | - [body-parser](): Node.js body parsing middleware 108 | - [cors](https://github.com/expressjs/cors): middleware for dynamically or statically enabling CORS in express/connect applications 109 | - [cssnano](): A modular minifier, built on top of the PostCSS ecosystem. 110 | - [express](): Fast, unopinionated, minimalist web framework 111 | - [graphql](https://github.com/graphql/graphql-js): A Query Language and Runtime which can target any service. 112 | - [graphql-server-express](https://github.com/apollostack/graphql-server/tree/master/packages): Production-ready Node.js GraphQL server for Express and Connect 113 | - [graphql-tag](https://github.com/apollostack/graphql-tag): A JavaScript template literal tag that parses GraphQL queries 114 | - [graphql-tools](https://github.com/apollostack/graphql-tools): A set of useful tools for GraphQL 115 | - [knex](https://github.com/tgriesser/knex): A batteries-included SQL query & schema builder for Postgres, MySQL and SQLite3 and the Browser 116 | - [lodash](): Lodash modular utilities. 117 | - [normalize.css](): A modern alternative to CSS resets 118 | - [pg](https://github.com/brianc/node-postgres): PostgreSQL client - pure javascript & libpq with the same API 119 | - [pretty-error](https://github.com/AriaMinaei/pretty-error): See nodejs errors with less clutter 120 | - [react](): React is a JavaScript library for building user interfaces. 121 | - [react-apollo](https://github.com/apollostack/react-apollo): React data container for Apollo Client 122 | - [react-dom](): React package for working with the DOM. 123 | - [react-redux](https://github.com/reactjs/react-redux): Official React bindings for Redux 124 | - [react-router](): A complete routing library for React 125 | - [redux](https://github.com/reactjs/redux): Predictable state container for JavaScript apps 126 | - [redux-thunk](https://github.com/gaearon/redux-thunk): Thunk middleware for Redux. 127 | 128 | ## Dev Dependencies 129 | 130 | - [babel-cli](): Babel command line. 131 | - [babel-core](): Babel compiler core. 132 | - [babel-loader](https://github.com/babel/babel-loader): babel module loader for webpack 133 | - [babel-plugin-transform-object-rest-spread](): Compile object rest and spread to ES5 134 | - [babel-plugin-transform-runtime](): Externalise references to helpers and builtins, automatically polyfilling your code without polluting globals 135 | - [babel-preset-env](): A Babel preset for each environment. 136 | - [babel-preset-react](): Babel preset for all React plugins. 137 | - [better-npm-run](https://github.com/benoror/better-npm-run): Better NPM scripts runner 138 | - [casual](): Fake data generator 139 | - [compression](): Node.js compression middleware 140 | - [css-loader](https://github.com/webpack/css-loader): css loader module for webpack 141 | - [debug](https://github.com/visionmedia/debug): small debugging utility 142 | - [eslint](): An AST-based pattern checker for JavaScript. 143 | - [eslint-plugin-babel](https://github.com/babel/eslint-plugin-babel): an eslint rule plugin companion to babel-eslint 144 | - [eslint-plugin-graphql](https://github.com/apollostack/eslint-plugin-graphql): GraphQL ESLint plugin. 145 | - [eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react): React specific linting rules for ESLint 146 | - [extract-text-webpack-plugin](https://github.com/webpack/extract-text-webpack-plugin): Extract text from bundle into a file. 147 | - [file-loader](https://github.com/webpack/file-loader): file loader module for webpack 148 | - [html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin): Simplifies creation of HTML files to serve your webpack bundles 149 | - [ip](https://github.com/indutny/node-ip): 150 | - [json-loader](https://github.com/webpack/json-loader): json loader module for webpack 151 | - [node-sass](https://github.com/sass/node-sass): Wrapper around libsass 152 | - [nodemon](https://github.com/remy/nodemon): Simple monitor script for use during development of a node.js app. 153 | - [package-json-to-readme](): Generate a README.md from package.json contents 154 | - [pm2](https://github.com/Unitech/pm2): Production process manager for Node.JS applications with a built-in load balancer. 155 | - [postcss-loader](): PostCSS loader for webpack 156 | - [react-hot-loader](https://github.com/gaearon/react-hot-loader): Tweak React components in real time. 157 | - [redbox-react](https://github.com/commissure/redbox-react): A redbox (rsod) component to display your errors. 158 | - [redux-logger](https://github.com/theaqua/redux-logger): Logger for Redux 159 | - [sass-loader](https://github.com/jtangelder/sass-loader): Sass loader for webpack 160 | - [sqlite3](https://github.com/mapbox/node-sqlite3): Asynchronous, non-blocking SQLite3 bindings 161 | - [style-loader](https://github.com/webpack/style-loader): style loader module for webpack 162 | - [url-loader](https://github.com/webpack/url-loader): url loader module for webpack 163 | - [webpack](https://github.com/webpack/webpack): Packs CommonJs/AMD modules for the browser. Allows to split your codebase into multiple bundles, which can be loaded on demand. Support loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff. 164 | - [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware): Offers a dev middleware for webpack, which arguments a live bundle to a directory 165 | - [webpack-dev-server](https://github.com/webpack/webpack-dev-server): Serves a webpack app. Updates the browser on changes. 166 | - [webpack-hot-middleware](https://github.com/glenjamin/webpack-hot-middleware): Webpack hot reloading you can attach to your own server 167 | - [webpack-node-externals](https://github.com/liady/webpack-node-externals): Easily exclude node_modules in Webpack bundle 168 | 169 | ## License 170 | 171 | MIT 172 | -------------------------------------------------------------------------------- /config/api.webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' // eslint-disable-line strict 2 | 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | const log = require('debug')('app:config:webpack') 6 | const nodeExternals = require('webpack-node-externals') 7 | 8 | const clientConfig = require('./client.config') 9 | const pkg = require('../package.json') 10 | 11 | const __PROD__ = process.env.NODE_ENV === 'production' 12 | // const __DEV__ = process.env.NODE_ENV === 'development' 13 | 14 | const vendor = [ 15 | 'apollo-client', 16 | 'react', 17 | 'react-apollo', 18 | 'react-dom', 19 | 'react-redux', 20 | 'react-router', 21 | 'redux', 22 | 'redux-thunk' 23 | ] 24 | 25 | vendor.forEach((name) => { 26 | if (pkg.dependencies[name]) return true 27 | log(`Vendor "${name}" was not found in package.json. It will not be included in the bundle`) 28 | }) 29 | 30 | const config = { 31 | // The base directory, an absolute path, for resolving entry points and loaders from configuration. 32 | // `entry` and `module.rules.loader` options are resolved relative to this directory 33 | context: path.resolve(__dirname, './'), 34 | 35 | // The point or points to enter the application. At this point, the application starts executing. 36 | // If an array is passed, all items will be executed. Simple rule: one entry point per HTML page. 37 | // SPA: one entry point. MPA: multiple entry points https://webpack.js.org/configuration/entry-context 38 | entry: entry(), 39 | 40 | // Options related to how webpack emits results 41 | output: output(), 42 | 43 | // enhance debugging by adding meta info for the browser devtools: 'source-map' | 'eval' 44 | devtool: devtool(), 45 | 46 | // options for resolving module requests (does not apply to resolving to loaders) 47 | resolve: resolve(), 48 | 49 | plugins: plugins(), 50 | 51 | // The configuration regarding modules 52 | module: modules(), 53 | 54 | // the environment in which the bundle should run 55 | // changes chunk loading behavior and available modules: web | node | webworker etc... 56 | target: 'node', 57 | 58 | node: { 59 | __dirname: false, 60 | __filename: false, 61 | }, 62 | 63 | externals: nodeExternals(), 64 | 65 | name: 'api' 66 | } 67 | 68 | function entry() { 69 | let client = [path.resolve(clientConfig.paths.api, './index')] 70 | return client 71 | } 72 | 73 | function output() { 74 | return { 75 | // the target directory for all output files must be an absolute path 76 | // path: path.join(__dirname, '/dist', '/assets'), 77 | path: path.join(clientConfig.paths.dist, './api'), 78 | 79 | // the filename template for entry chunks: "bundle.js" "[name].js" "[chunkhash].js" 80 | filename: 'index.js', 81 | 82 | // the url to the output directory resolved relative to the HTML page 83 | publicPath: 'api/', 84 | } 85 | } 86 | 87 | function devtool() { 88 | return __PROD__ ? 'source-map' : 'eval-source-map' 89 | } 90 | 91 | function plugins() { 92 | log('Enable common plugins: Define, LoaderOptions, HtmlWebpack, CommonsChunk') 93 | const common = [ 94 | // map variables 95 | new webpack.DefinePlugin(clientConfig.globals), 96 | ] 97 | 98 | if (__PROD__) { 99 | log('Enable plugins for production: OccurrenceOrder, UglifyJs') 100 | return common.concat([ 101 | // optimize order of modules based on how often it is used 102 | new webpack.optimize.OccurrenceOrderPlugin(), 103 | 104 | // uglify and minify javascript files 105 | new webpack.optimize.UglifyJsPlugin({ 106 | compress: { 107 | unused: true, 108 | dead_code: true, 109 | warnings: false 110 | } 111 | }) 112 | ]) 113 | } 114 | } 115 | 116 | function resolve() { 117 | return { 118 | modules: [ 119 | 'node_modules', 120 | clientConfig.paths.api 121 | ], 122 | extensions: ['.js', '.jsx', '.json'] 123 | } 124 | } 125 | 126 | function modules() { 127 | return { 128 | rules: [ 129 | { 130 | test: /\.json$/, 131 | use: [ 132 | { loader: 'json-loader' } 133 | ] 134 | }, 135 | { 136 | test: /\.(js|jsx)$/, 137 | include: [ 138 | clientConfig.paths.api 139 | ], 140 | exclude: [ 141 | /node_modules/, 142 | ], 143 | use: [ 144 | { loader: 'babel-loader' } 145 | ] 146 | }, 147 | { 148 | test: /\.woff(\?.*)?$/, 149 | use: [{ loader: 'url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff' }] 150 | }, 151 | { 152 | test: /\.woff2(\?.*)?$/, 153 | use: [{ loader: 'url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff2' }] 154 | }, 155 | { 156 | test: /\.otf(\?.*)?$/, 157 | use: [{ loader: 'file-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=font/opentype' }] 158 | }, 159 | { 160 | test: /\.ttf(\?.*)?$/, 161 | use: [{ loader: 'url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/octet-stream' }] 162 | }, 163 | { 164 | test: /\.eot(\?.*)?$/, 165 | use: [{ loader: 'file-loader?prefix=fonts/&name=[path][name].[ext]' }] 166 | }, 167 | { 168 | test: /\.svg(\?.*)?$/, 169 | use: [{ loader: 'url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=image/svg+xml' }] 170 | }, 171 | { 172 | test: /\.(png|jpg)$/, 173 | use: [{ loader: 'url-loader?limit=8192' }] 174 | } 175 | ], 176 | } 177 | } 178 | 179 | module.exports = config 180 | -------------------------------------------------------------------------------- /config/client.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const ip = require('ip') 3 | 4 | module.exports = { 5 | env: process.env.NODE_ENV, 6 | paths: { 7 | dist: path.resolve(__dirname, '../dist'), 8 | public: path.resolve(__dirname, '../src/client/public'), 9 | client: path.resolve(__dirname, '../src/client'), 10 | api: path.resolve(__dirname, '../src/api/') 11 | }, 12 | devServer: { 13 | client_port: process.env.CLIENT_PORT || 3000, 14 | host: ip.address(), 15 | }, 16 | globals: { 17 | 'process.env': { 18 | 'NODE_ENV': JSON.stringify(process.env.NODE_ENV), 19 | 'CLIENT_PORT': process.env.CLIENT_PORT, 20 | 'API_PORT': process.env.API_PORT, 21 | }, 22 | '__DEV__': process.env.NODE_ENV === 'development', 23 | '__PROD__': process.env.NODE_ENV === 'production', 24 | '__CLIENT_PORT__': process.env.CLIENT_PORT, 25 | '__API_PORT__': process.env.API_PORT, 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /config/client.webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' // eslint-disable-line strict 2 | 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | const cssnano = require('cssnano') 6 | const log = require('debug')('app:config:webpack') 7 | const HtmlWebpackPlugin = require('html-webpack-plugin') 8 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 9 | 10 | const clientConfig = require('./client.config') 11 | const pkg = require('../package.json') 12 | 13 | const __PROD__ = process.env.NODE_ENV === 'production' 14 | const __DEV__ = process.env.NODE_ENV === 'development' 15 | 16 | const vendor = [ 17 | 'apollo-client', 18 | 'react', 19 | 'react-apollo', 20 | 'react-dom', 21 | 'react-redux', 22 | 'react-router', 23 | 'redux', 24 | 'redux-thunk' 25 | ] 26 | 27 | vendor.forEach((name) => { 28 | if (pkg.dependencies[name]) return true 29 | log(`Vendor "${name}" was not found in package.json. It will not be included in the bundle`) 30 | }) 31 | 32 | const config = { 33 | // The base directory, an absolute path, for resolving entry points and loaders from configuration. 34 | // `entry` and `module.rules.loader` options are resolved relative to this directory 35 | context: path.resolve(__dirname, './'), 36 | 37 | // The point or points to enter the application. At this point, the application starts executing. 38 | // If an array is passed, all items will be executed. Simple rule: one entry point per HTML page. 39 | // SPA: one entry point. MPA: multiple entry points https://webpack.js.org/configuration/entry-context 40 | entry: entry(), 41 | 42 | // Options related to how webpack emits results 43 | output: output(), 44 | 45 | // enhance debugging by adding meta info for the browser devtools: 'source-map' | 'eval' 46 | devtool: devtool(), 47 | 48 | // options for resolving module requests (does not apply to resolving to loaders) 49 | resolve: resolve(), 50 | 51 | plugins: plugins(), 52 | 53 | // The configuration regarding modules 54 | module: modules(), 55 | 56 | // the environment in which the bundle should run 57 | // changes chunk loading behavior and available modules: web | node | webworker etc... 58 | target: 'web', 59 | 60 | name: 'client' 61 | } 62 | 63 | function entry() { 64 | let entry = { app: null, vendor } 65 | let app = [path.resolve(clientConfig.paths.client, './index')] 66 | if (!__PROD__) app.unshift('webpack-hot-middleware/client?reload=true') 67 | entry.app = app 68 | return entry 69 | } 70 | 71 | function output() { 72 | return { 73 | // the target directory for all output files must be an absolute path 74 | // path: path.join(__dirname, '/dist', '/assets'), 75 | path: clientConfig.paths.dist, 76 | 77 | // the filename template for entry chunks: "bundle.js" "[name].js" "[chunkhash].js" 78 | filename: '[name].[hash].js', 79 | 80 | // the url to the output directory resolved relative to the HTML page 81 | // uncomment this if you're not using docker 82 | // publicPath: __PROD__ ? '/' : `http://${clientConfig.devServer.host}:${clientConfig.devServer.client_port}/` 83 | publicPath: '/', 84 | } 85 | } 86 | 87 | function devtool() { 88 | return __PROD__ ? 'source-map' : 'eval-source-map' 89 | } 90 | 91 | function plugins() { 92 | log('Enable common plugins: Define, LoaderOptions, HtmlWebpack, CommonsChunk') 93 | const common = [ 94 | // map variables 95 | new webpack.DefinePlugin(clientConfig.globals), 96 | 97 | new webpack.LoaderOptionsPlugin({ 98 | options: { 99 | context: __dirname, 100 | postcss: [ 101 | cssnano({ 102 | autoprefixer: { 103 | add: true, 104 | remove: true, 105 | browsers: ['last 2 versions'] 106 | }, 107 | discardComments: { 108 | removeAll: true 109 | }, 110 | discardUnused: false, 111 | mergeIdents: false, 112 | reduceIdents: false, 113 | safe: true, 114 | sourcemap: true 115 | }) 116 | ], 117 | sassLoader: { 118 | includePaths: path.resolve(clientConfig.paths.client, './styles') 119 | } 120 | } 121 | }), 122 | 123 | // parse html 124 | new HtmlWebpackPlugin({ 125 | template: path.resolve(clientConfig.paths.public, './index.html'), 126 | hash: false, 127 | favicon: path.resolve(clientConfig.paths.public, './favicon.ico'), 128 | filename: 'index.html', 129 | inject: 'body', 130 | minify: { collapseWhitespace: true } 131 | }), 132 | 133 | // split bundles 134 | new webpack.optimize.CommonsChunkPlugin({ 135 | names: ['vendor'] 136 | }) 137 | ] 138 | 139 | if (__PROD__) { 140 | log('Enable plugins for production: OccurrenceOrder, UglifyJs') 141 | return common.concat([ 142 | // optimize order of modules based on how often it is used 143 | new webpack.optimize.OccurrenceOrderPlugin(), 144 | 145 | // uglify and minify javascript files 146 | new webpack.optimize.UglifyJsPlugin({ 147 | compress: { 148 | unused: true, 149 | dead_code: true, 150 | warnings: false 151 | } 152 | }) 153 | ]) 154 | } else { 155 | log('Enable plugins for development: HotModuleReplacement, NamedModules, NoErrors') 156 | return common.concat([ 157 | // enable HMR globally 158 | new webpack.HotModuleReplacementPlugin(), 159 | // prints more readable module names in the browser console on HMR updates 160 | new webpack.NamedModulesPlugin(), 161 | new webpack.NoErrorsPlugin() 162 | ]) 163 | } 164 | } 165 | 166 | function resolve() { 167 | return { 168 | modules: [ 169 | 'node_modules', 170 | clientConfig.paths.client 171 | ], 172 | extensions: ['.js', '.jsx', '.json'] 173 | } 174 | } 175 | 176 | function modules() { 177 | // cssnano minimizes already so disable minimize in cssLoader 178 | const cssLoader = 'css-loader?modules&sourceMap&-minimizeimportLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]' 179 | 180 | return { 181 | rules: [ 182 | { 183 | test: /\.json$/, 184 | use: [ 185 | { loader: 'json-loader' } 186 | ] 187 | }, 188 | { 189 | test: /\.(js|jsx)$/, 190 | include: [ 191 | clientConfig.paths.client 192 | ], 193 | exclude: [ 194 | /node_modules/, 195 | ], 196 | use: __PROD__ ? [{ loader: 'babel-loader' }] 197 | : [ 198 | { loader: 'react-hot-loader' }, 199 | { loader: 'babel-loader' } 200 | ] 201 | }, 202 | { 203 | test: /\.css$/, 204 | loaders: [ 205 | 'style-loader', 206 | cssLoader, 207 | 'postcss-loader', 208 | ] 209 | }, 210 | { 211 | test: /\.scss$/, 212 | loaders: [ 213 | 'style-loader', 214 | cssLoader, 215 | 'postcss-loader', 216 | 'sass-loader?sourceMap' 217 | ] 218 | }, 219 | { 220 | test: /\.woff(\?.*)?$/, 221 | use: [{ loader: 'url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff' }] 222 | }, 223 | { 224 | test: /\.woff2(\?.*)?$/, 225 | use: [{ loader: 'url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff2' }] 226 | }, 227 | { 228 | test: /\.otf(\?.*)?$/, 229 | use: [{ loader: 'file-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=font/opentype' }] 230 | }, 231 | { 232 | test: /\.ttf(\?.*)?$/, 233 | use: [{ loader: 'url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/octet-stream' }] 234 | }, 235 | { 236 | test: /\.eot(\?.*)?$/, 237 | use: [{ loader: 'file-loader?prefix=fonts/&name=[path][name].[ext]' }] 238 | }, 239 | { 240 | test: /\.svg(\?.*)?$/, 241 | use: [{ loader: 'url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=image/svg+xml' }] 242 | }, 243 | { 244 | test: /\.(png|jpg)$/, 245 | use: [{ loader: 'url-loader?limit=8192' }] 246 | } 247 | ], 248 | } 249 | } 250 | 251 | if (!__DEV__) { 252 | log('Adding ExtractTextPlugin to CSS loaders.') 253 | 254 | // extract all text from css 255 | config.module.rules.filter((rules) => { 256 | return rules.loaders && rules.loaders.find((name) => { 257 | return /css/.test(name.split('?')[0]) 258 | }) 259 | }).forEach((loader) => { 260 | const first = loader.loaders[0] 261 | const rest = loader.loaders.slice(1) 262 | loader.loader = ExtractTextPlugin.extract({ 263 | fallbackLoader: first, 264 | loader: rest.join('!') 265 | }) 266 | delete loader.loaders 267 | }) 268 | 269 | // place all extracted text into a file 270 | config.plugins.push( 271 | new ExtractTextPlugin({ filename: '[name].[contenthash].css', allChunks: true }) 272 | ) 273 | } 274 | 275 | module.exports = config 276 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const webpack = require('./client.webpack.config') 2 | const client = require('./client.config') 3 | 4 | module.exports = { 5 | webpack, 6 | client 7 | } 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | # https://docs.docker.com/compose/compose-file 4 | # - `ports` for public, and `expose` for linked containers 5 | # - `args` when passing env vars on custom dockerfiles and `environment` for prebuilt images 6 | 7 | services: 8 | 9 | api: 10 | build: 11 | context: . 12 | dockerfile: ./src/api/Dockerfile 13 | args: 14 | - NODE_ENV=${NODE_ENV} 15 | - API_PORT=${API_PORT} 16 | - DEBUG=${DEBUG} 17 | container_name: api 18 | depends_on: 19 | - db 20 | restart: on-failure:10 21 | networks: 22 | - back 23 | expose: 24 | - "${API_PORT}" 25 | 26 | client: 27 | build: 28 | context: . 29 | dockerfile: ./src/client/Dockerfile 30 | args: 31 | - NODE_ENV=${NODE_ENV} 32 | - CLIENT_PORT=${CLIENT_PORT} 33 | - API_PORT=${API_PORT} 34 | - DEBUG=${DEBUG} 35 | container_name: client 36 | depends_on: 37 | - api 38 | restart: on-failure:10 39 | networks: 40 | - front 41 | expose: 42 | - "${CLIENT_PORT}" 43 | 44 | db: 45 | image: postgres:9.6 46 | container_name: db 47 | restart: on-failure:10 48 | volumes: 49 | - "./data:/var/lib/postgresql/data" 50 | networks: 51 | - back 52 | environment: 53 | - POSTGRES_USER=user 54 | - POSTGRES_PASSWORD=password 55 | - POSTGRES_DB=db 56 | expose: 57 | - "5432" 58 | 59 | nginx: 60 | build: 61 | context: ./src/nginx/ 62 | dockerfile: ./Dockerfile 63 | args: 64 | - CLIENT_PORT=${CLIENT_PORT} 65 | - API_PORT=${API_PORT} 66 | - WEB_SSL=false 67 | - SELF_SIGNED=false 68 | - NO_DEFAULT=false 69 | container_name: nginx 70 | depends_on: 71 | - client 72 | - api 73 | restart: on-failure:10 74 | networks: 75 | - front 76 | - back 77 | ports: 78 | - "80:80" 79 | - "443:443" 80 | tty: true 81 | 82 | networks: 83 | front: 84 | back: 85 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | const __PROD__ = process.env.NODE_ENV === 'production' 2 | 3 | module.exports = { 4 | /** 5 | * Application configuration section 6 | * http://pm2.keymetrics.io/docs/usage/application-declaration/ 7 | */ 8 | apps: [ 9 | 10 | // First application 11 | { 12 | name: 'api', 13 | script: __PROD__ ? './dist/api/index.js' : './src/api/index.js', 14 | watch: __PROD__ ? false : ['./src/api'], 15 | ignore_watch: ['node_modules'], 16 | env: { 17 | NODE_ENV: 'development', 18 | DEBUG: process.env.DEBUG, 19 | API_PORT: process.env.API_PORT 20 | }, 21 | env_production: { 22 | NODE_ENV: 'production', 23 | DEBUG: process.env.DEBUG, 24 | API_PORT: process.env.API_PORT 25 | } 26 | }, 27 | 28 | // Second application 29 | { 30 | name: 'client', 31 | // for production mode, just use a node webserver for now.. 32 | script: './src/client/devServer.js', 33 | // we have hot-loading already so no need for watch in client 34 | watch: false, 35 | ignore_watch: ['node_modules'], 36 | env: { 37 | NODE_ENV: 'development', 38 | DEBUG: process.env.DEBUG, 39 | CLIENT_PORT: process.env.CLIENT_PORT, 40 | API_PORT: process.env.API_PORT 41 | }, 42 | env_production: { 43 | NODE_ENV: 'production', 44 | DEBUG: process.env.DEBUG, 45 | CLIENT_PORT: process.env.CLIENT_PORT, 46 | API_PORT: process.env.API_PORT 47 | } 48 | } 49 | ], 50 | 51 | /** 52 | * Deployment section 53 | * http://pm2.keymetrics.io/docs/usage/deployment/ 54 | */ 55 | // deploy: { 56 | // production: { 57 | // user: 'node', 58 | // host: '212.83.163.1', 59 | // ref: 'origin/master', 60 | // repo: 'git@github.com:repo.git', 61 | // path: '/var/www/production', 62 | // 'post-deploy': 'npm install && pm2 startOrRestart ecosystem.json --env production' 63 | // }, 64 | // dev: { 65 | // user: 'node', 66 | // host: '212.83.163.1', 67 | // ref: 'origin/master', 68 | // repo: 'git@github.com:repo.git', 69 | // path: '/var/www/development', 70 | // 'post-deploy': 'npm install && pm2 startOrRestart ecosystem.json --env dev', 71 | // env: { 72 | // NODE_ENV: 'dev' 73 | // } 74 | // } 75 | // } 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-react-starter", 3 | "author": "Jan Carlo Viray ", 4 | "license": "MIT", 5 | "version": "1.0.0", 6 | "description": "", 7 | "main": "server.js", 8 | "keywords": [ 9 | "Node.js", 10 | "GraphQL", 11 | "React", 12 | "Knex.js", 13 | "Postgres", 14 | "Apollo", 15 | "Redux" 16 | ], 17 | "homepage": "https://github.com/jancarloviray/graphql-react-starter#graphql-react-starter", 18 | "scripts": { 19 | "pm2": "PM2_HOME=.pm2 pm2 -- ", 20 | "doc": "readme package.json >> README.md" 21 | }, 22 | "dependencies": { 23 | "apollo-client": "^0.7.1", 24 | "body-parser": "^1.15.2", 25 | "cors": "^2.8.1", 26 | "cssnano": "^3.10.0", 27 | "express": "^4.14.0", 28 | "graphql": "^0.8.0", 29 | "graphql-server-express": "^0.4.3", 30 | "graphql-tag": "^1.2.2", 31 | "graphql-tools": "^0.9.0", 32 | "knex": "^0.12.6", 33 | "lodash": "^4.17.3", 34 | "normalize.css": "^5.0.0", 35 | "pg": "^6.1.2", 36 | "pretty-error": "^2.0.2", 37 | "react": "^15.4.1", 38 | "react-apollo": "^0.8.1", 39 | "react-dom": "^15.4.1", 40 | "react-redux": "^5.0.2", 41 | "react-router": "^3.0.0", 42 | "redux": "^3.6.0", 43 | "redux-thunk": "^2.1.0" 44 | }, 45 | "devDependencies": { 46 | "babel-cli": "^6.18.0", 47 | "babel-core": "^6.21.0", 48 | "babel-loader": "^6.2.10", 49 | "babel-plugin-transform-object-rest-spread": "^6.20.2", 50 | "babel-plugin-transform-runtime": "^6.15.0", 51 | "babel-preset-env": "^1.1.4", 52 | "babel-preset-react": "^6.16.0", 53 | "better-npm-run": "^0.0.14", 54 | "casual": "^1.5.8", 55 | "compression": "^1.6.2", 56 | "css-loader": "^0.26.1", 57 | "debug": "^2.6.0", 58 | "eslint": "^3.12.2", 59 | "eslint-plugin-babel": "^4.0.0", 60 | "eslint-plugin-graphql": "^0.4.3", 61 | "eslint-plugin-react": "^6.8.0", 62 | "extract-text-webpack-plugin": "^2.0.0-beta.4", 63 | "file-loader": "^0.9.0", 64 | "html-webpack-plugin": "^2.26.0", 65 | "ip": "^1.1.4", 66 | "json-loader": "^0.5.4", 67 | "node-sass": "^4.1.1", 68 | "nodemon": "^1.11.0", 69 | "package-json-to-readme": "^2.0.0", 70 | "pm2": "latest", 71 | "postcss-loader": "^1.2.2", 72 | "react-hot-loader": "^1.3.1", 73 | "redbox-react": "^1.3.3", 74 | "redux-logger": "^2.7.4", 75 | "sass-loader": "^4.1.1", 76 | "sqlite3": "^3.1.8", 77 | "style-loader": "^0.13.1", 78 | "url-loader": "^0.5.7", 79 | "webpack": "2.2.0-rc.3", 80 | "webpack-dev-middleware": "^1.9.0", 81 | "webpack-dev-server": "2.2.0-rc.0", 82 | "webpack-hot-middleware": "^2.15.0", 83 | "webpack-node-externals": "^1.5.4" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jancarloviray/graphql-react-starter/43e158dacf306db41ae9d15dfb5524103c49bcf3/src/Dockerfile -------------------------------------------------------------------------------- /src/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:boron-alpine 2 | 3 | RUN npm install yarn --global --no-progress --silent --depth 0 4 | 5 | WORKDIR /tmp 6 | COPY ./package.json /tmp/ 7 | RUN yarn install 8 | 9 | WORKDIR /app 10 | # prevent MemoryFileSystem.readFileSync error 11 | RUN mkdir dist 12 | RUN cp -a /tmp/node_modules /app/node_modules && cp -a /tmp/package.json /app/package.json 13 | 14 | COPY ./config/ /app/config 15 | COPY .babelrc /app/.babelrc 16 | COPY ./src/api /app/src/api 17 | COPY ./src/db /app/src/db 18 | 19 | ARG API_PORT=8080 20 | ENV API_PORT=${API_PORT} 21 | ARG NODE_ENV=production 22 | ENV NODE_ENV=${NODE_ENV} 23 | ARG DEBUG=* 24 | ENV DEBUG=${DEBUG} 25 | 26 | CMD ["node","/app/src/api/index.js"] 27 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const { graphqlExpress, graphiqlExpress } = require('graphql-server-express') 3 | const { printSchema } = require('graphql') 4 | const bodyParser = require('body-parser') 5 | const cors = require('cors') 6 | const PrettyError = require('pretty-error') 7 | const debug = require('debug') 8 | 9 | const db = require('../db/lib/db') 10 | const schema = require('./schema') 11 | 12 | const pe = new PrettyError() 13 | pe.start() 14 | pe.skipNodeFiles() 15 | pe.skipPackage('express') 16 | 17 | const log = debug('app:api') 18 | 19 | const GRAPHQL_PORT = process.env.API_PORT || 8080 20 | 21 | const app = express() 22 | 23 | app.use('*', cors()) 24 | 25 | app.use('/graphql', bodyParser.json(), graphqlExpress((/*req*/) => { 26 | let user // = req.session.user 27 | return { 28 | schema, 29 | pretty: true, 30 | allowUndefinedInResolve: false, 31 | context: { 32 | user, 33 | }, 34 | } 35 | })) 36 | 37 | app.use('/graphiql', graphiqlExpress({ 38 | endpointURL: '/graphql', 39 | query: ` 40 | { 41 | users { 42 | name 43 | accounts { 44 | type 45 | total 46 | } 47 | } 48 | } 49 | ` 50 | })) 51 | 52 | app.use('/schema', (req, res) => { 53 | res.set('Content-Type', 'text/plain') 54 | res.send(printSchema(schema)) 55 | }) 56 | 57 | // catch all 58 | app.use((err, req, res, next) => { 59 | process.stderr.write(pe.render(err)) 60 | next() 61 | }) 62 | 63 | const server = app.listen(GRAPHQL_PORT, () => { 64 | log(`GraphQL Server is now running on http://localhost:${GRAPHQL_PORT}/graphql`) 65 | }) 66 | 67 | const term = [ 68 | 'exit', 69 | 'uncaughtException', 70 | 'SIGTERM', 71 | 'SIGINT' 72 | ] 73 | 74 | term.forEach((message) => { 75 | process.on(message, () => { 76 | log(`Application received: ${message}. Application will now cleanup and terminate.`) 77 | db.destroy() 78 | server.close() 79 | process.exit(1) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /src/api/knexfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const config = { 4 | development: { 5 | client: 'sqlite3', 6 | debug: process.env.NODE_ENV === 'development', 7 | connection: { 8 | filename: path.resolve(__dirname, '../db/devdb.sqlite3'), 9 | }, 10 | seeds: { 11 | directory: '../db/seeds' 12 | }, 13 | migrations: { 14 | directory: '../db/migrations', 15 | tableName: 'knex_migrations' 16 | }, 17 | useNullAsDefault: true, 18 | pool: { 19 | min: 1, 20 | max: 10, 21 | afterCreate: function (conn, cb) { 22 | conn.run('PRAGMA foreign_keys=ON', cb) 23 | } 24 | } 25 | }, 26 | production: { 27 | client: 'pg', 28 | debug: process.env.NODE_ENV === 'development', 29 | connection: { 30 | host: process.env.POSTGRES_HOST || 'db', 31 | user: process.env.POSTGRES_USER || 'postgres', 32 | database: process.env.POSTGRES_DB || 'postgres', 33 | password: process.env.POSTGRES_PASSWORD 34 | }, 35 | migrations: { 36 | directory: '../db/migrations', 37 | tableName: 'knex_migrations' 38 | }, 39 | } 40 | } 41 | 42 | module.exports = config 43 | -------------------------------------------------------------------------------- /src/api/models/Account.js: -------------------------------------------------------------------------------- 1 | const db = require('../../db/lib/db') 2 | 3 | exports.schema = [` 4 | type Account { 5 | accountId: Int! 6 | name: String 7 | # Defines checking or saving 8 | type: String 9 | total: Int! 10 | createdDate: String 11 | updatedDate: String 12 | 13 | # Defines who owns this account 14 | owners: [User] 15 | } 16 | `] 17 | 18 | exports.resolvers = { 19 | Account: { 20 | owners({ accountId }) { 21 | return db 22 | .select('Users.*') 23 | .from('Users_Accounts') 24 | .join('Accounts', 'Users_Accounts.accountId', 'Accounts.accountId') 25 | .join('Users', 'Users_Accounts.userId', 'Users.userId') 26 | .where('Accounts.accountId', accountId) || null 27 | } 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /src/api/models/RootMutation.js: -------------------------------------------------------------------------------- 1 | const db = require('../../db/lib/db') 2 | 3 | exports.schema = [` 4 | type Mutation { 5 | # Creates a new User and returns its Id 6 | createUser( 7 | name: String!, 8 | # Id of another User who referred this current one 9 | refId: Int 10 | ): [Int] 11 | 12 | # Creates a new Account and returns its Id 13 | createAccount( 14 | name: String, 15 | type: String, 16 | total: Int 17 | ): [Int] 18 | 19 | # Creates a new Transaction and returns its Id 20 | createTransaction( 21 | transactionTypeId: Int, 22 | accountId: Int, 23 | amount: Int, 24 | note: String 25 | ): [Int] 26 | } 27 | `] 28 | 29 | exports.resolvers = { 30 | Mutation: { 31 | createUser(_, args) { 32 | return db.insert(args).into('Users').returning('*') 33 | }, 34 | createAccount(_, args) { 35 | return db.insert(args).into('Accounts').returning('*') 36 | }, 37 | createTransaction(_, args) { 38 | args.sessionId = 'someRandomSesseionGUID' 39 | return db.insert(args).into('Transactions').returning('*') 40 | } 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /src/api/models/RootQuery.js: -------------------------------------------------------------------------------- 1 | const db = require('../../db/lib/db') 2 | 3 | exports.schema = [` 4 | type Query { 5 | users( 6 | userId: Int 7 | ): [User] 8 | 9 | accounts( 10 | accountId: Int 11 | ): [Account] 12 | 13 | transactions( 14 | transactionId: Int 15 | transactionTypeId: Int 16 | ): [Transaction] 17 | 18 | transactionTypes: [TransactionType] 19 | 20 | getWithdrawals( 21 | accountId: Int! 22 | ): [Transaction] 23 | 24 | getDeposits( 25 | accountId: Int! 26 | ): [Transaction] 27 | } 28 | `] 29 | 30 | exports.resolvers = { 31 | Query: { 32 | users(root, { userId }) { 33 | return userId ? 34 | db.select('*').from('Users').where({ userId }) : 35 | db.select('*').from('Users') 36 | }, 37 | accounts(_, { accountId }) { 38 | return accountId ? 39 | db.select('*').from('Accounts').where({ accountId }) : 40 | db.select('*').from('Accounts') 41 | }, 42 | transactions(_, args) { 43 | return db.select('*').from('Transactions').where(args) 44 | }, 45 | transactionTypes() { 46 | return db.select('*').from('TransactionTypes') 47 | }, 48 | getWithdrawals(_, { accountId }) { 49 | return db 50 | .select('*') 51 | .from('Transactions') 52 | .where({ 53 | transactionTypeId: 1, 54 | accountId, 55 | }) || null 56 | }, 57 | getDeposits(_, { accountId }) { 58 | return db 59 | .select('*') 60 | .from('Transactions') 61 | .where({ 62 | transactionTypeId: 2, 63 | accountId, 64 | }) || null 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/api/models/Transaction.js: -------------------------------------------------------------------------------- 1 | const db = require('../../db/lib/db') 2 | 3 | exports.schema = [` 4 | type Transaction { 5 | transactionId: Int! 6 | transactionTypeId: Int 7 | amount: Int! 8 | note: String 9 | createdDate: String 10 | 11 | # What account is this transaction 12 | # made to? 13 | account: Account 14 | } 15 | `] 16 | 17 | exports.resolvers = { 18 | Transaction: { 19 | account({ accountId }) { 20 | return db 21 | .select('*') 22 | .from('Accounts') 23 | .where({ accountId: accountId }) 24 | .first() || null 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/api/models/TransactionType.js: -------------------------------------------------------------------------------- 1 | exports.schema = [` 2 | # Used for Transaction.type and defines 3 | # whether an account is checking or savings 4 | type TransactionType { 5 | transactionTypeId: Int! 6 | name: String 7 | } 8 | `] 9 | 10 | exports.resolvers = { 11 | TransactionType: {} 12 | } 13 | -------------------------------------------------------------------------------- /src/api/models/User.js: -------------------------------------------------------------------------------- 1 | const db = require('../../db/lib/db') 2 | 3 | exports.schema = [` 4 | type User { 5 | userId: Int! 6 | createdDate: String 7 | name: String! 8 | 9 | # Referrer is an existing User 10 | # that gets credit for referring a 11 | # new User. 12 | referrer: User 13 | 14 | # User's currently available accounts 15 | accounts: [Account] 16 | } 17 | `] 18 | 19 | exports.resolvers = { 20 | User: { 21 | referrer({ refId }) { 22 | return refId ? db 23 | .select('*') 24 | .from('Users') 25 | .where({ userId: refId }) 26 | .first() : null 27 | }, 28 | accounts({ userId }) { 29 | return db 30 | .select('Accounts.*') 31 | .from('Users_Accounts') 32 | .join('Accounts', 'Users_Accounts.accountId', 'Accounts.accountId') 33 | .join('Users', 'Users_Accounts.userId', 'Users.userId') 34 | .where('Users.userId', userId) || null 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/api/schema.js: -------------------------------------------------------------------------------- 1 | const { makeExecutableSchema } = require('graphql-tools') 2 | const { merge } = require('lodash') 3 | 4 | const Account = require('./models/Account') 5 | const RootMutation = require('./models/RootMutation') 6 | const RootQuery = require('./models/RootQuery') 7 | const Transaction = require('./models/Transaction') 8 | const TransactionType = require('./models/TransactionType') 9 | const User = require('./models/User') 10 | 11 | const schema = [` 12 | schema { 13 | query: Query 14 | mutation: Mutation 15 | } 16 | `] 17 | 18 | const typeDefs = [ 19 | ...Account.schema, 20 | ...RootMutation.schema, 21 | ...RootQuery.schema, 22 | ...Transaction.schema, 23 | ...TransactionType.schema, 24 | ...User.schema, 25 | ...schema, 26 | ] 27 | 28 | const resolvers = merge( 29 | Account.resolvers, 30 | RootMutation.resolvers, 31 | RootQuery.resolvers, 32 | Transaction.resolvers, 33 | TransactionType.resolvers, 34 | User.resolvers 35 | ) 36 | 37 | module.exports = makeExecutableSchema({ typeDefs, resolvers }) 38 | -------------------------------------------------------------------------------- /src/client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:boron-alpine 2 | 3 | RUN npm install yarn --global --no-progress --silent --depth 0 4 | 5 | WORKDIR /tmp 6 | COPY ./package.json /tmp/ 7 | RUN yarn install 8 | 9 | WORKDIR /app 10 | # prevent MemoryFileSystem.readFileSync error 11 | RUN mkdir dist 12 | RUN cp -a /tmp/node_modules /app/node_modules && cp -a /tmp/package.json /app/package.json 13 | 14 | COPY ./config/ /app/config 15 | COPY .babelrc /app/.babelrc 16 | COPY ./src/client /app/src/client 17 | 18 | ARG API_PORT=8080 19 | ENV API_PORT=${API_PORT} 20 | ARG CLIENT_PORT=8080 21 | ENV CLIENT_PORT=${CLIENT_PORT} 22 | ARG NODE_ENV=production 23 | ENV NODE_ENV=${NODE_ENV} 24 | ARG DEBUG=* 25 | ENV DEBUG=${DEBUG} 26 | 27 | # TODO: this one runs development way still.. 28 | CMD ["node","/app/src/client/devServer.js"] 29 | -------------------------------------------------------------------------------- /src/client/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { IndexLink, Link } from 'react-router' 4 | import './Header.scss' 5 | 6 | export const Header = () => ( 7 |
8 |

GraphQL React Starter

9 | Home 10 | {' · '} 11 | Users 12 | {' · '} 13 | Counter 14 |
15 | ) 16 | 17 | export default Header 18 | -------------------------------------------------------------------------------- /src/client/components/Header/Header.scss: -------------------------------------------------------------------------------- 1 | .route--active { 2 | font-weight: bold; 3 | text-decoration: underline; 4 | } -------------------------------------------------------------------------------- /src/client/components/Header/index.js: -------------------------------------------------------------------------------- 1 | 2 | import Header from './Header' 3 | 4 | export default Header -------------------------------------------------------------------------------- /src/client/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { browserHistory, Router } from 'react-router' 3 | import { Provider } from 'react-redux' 4 | import { ApolloProvider } from 'react-apollo' 5 | 6 | class AppContainer extends Component { 7 | static get propTypes() { 8 | return { 9 | client: PropTypes.object.isRequired, 10 | routes: PropTypes.object.isRequired, 11 | store: PropTypes.object.isRequired 12 | } 13 | } 14 | 15 | shouldComponentUpdate() { 16 | return false 17 | } 18 | 19 | render() { 20 | const { routes, store, client } = this.props 21 | return ( 22 | 23 | 24 |
25 | 26 |
27 |
28 |
29 | ) 30 | } 31 | } 32 | 33 | export default AppContainer 34 | -------------------------------------------------------------------------------- /src/client/devServer.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const debug = require('debug') 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | const compression = require('compression') 6 | const fs = require('fs') 7 | 8 | const config = require('../../config') 9 | 10 | const log = debug('app:client') 11 | const app = express() 12 | 13 | // gzip 14 | app.use(compression()) 15 | 16 | if (config.client.env === 'development') { 17 | const compiler = webpack(config.webpack) 18 | 19 | log(`Enabling webpack-dev-middleware. publicPath at ${config.webpack.output.publicPath}`) 20 | app.use(require('webpack-dev-middleware')(compiler, { 21 | publicPath: config.webpack.output.publicPath, 22 | hot: true, 23 | noInfo: false, 24 | quiet: false, 25 | stats: { colors: true, chunks: false, chunkModules: false } 26 | })) 27 | 28 | log('Enabling webpack-hot-middleware') 29 | app.use(require('webpack-hot-middleware')(compiler)) 30 | 31 | log(`Adding express static middleware with dist at ${config.client.paths.dist}`) 32 | app.use(express.static(config.client.paths.dist)) 33 | 34 | const indexHtml = path.join(compiler.outputPath, 'index.html') 35 | log(`All routes directed to ${indexHtml}`) 36 | 37 | app.use('*', (req, res, next) => { 38 | // https://github.com/ampedandwired/html-webpack-plugin/issues/145 39 | compiler.outputFileSystem.readFile(indexHtml, (err, result) => { 40 | if (err) return next(err) 41 | res.set('content-type', 'text/html') 42 | res.status(200).send(result) 43 | res.end() 44 | }) 45 | }) 46 | } else { 47 | // Here, we can also do Universal Rendering. Check this post for more information: 48 | // https://github.com/ReactTraining/react-router/blob/master/docs/guides/ServerRendering.md 49 | log('WARNING: The server is being run outside development mode. This is not recommended in production. Precompile the project first, and serve the static files using something like Nginx.') 50 | app.use(express.static(config.client.paths.dist)) 51 | 52 | const indexHtml = path.join(config.client.paths.dist, 'index.html') 53 | log(`All routes directed to ${indexHtml}`) 54 | 55 | app.use('*', (req, res, next) => { 56 | fs.readFile(indexHtml, (err, result) => { 57 | if (err) return next(err) 58 | res.set('content-type', 'text/html') 59 | res.status(200).send(result) 60 | res.end() 61 | }) 62 | }) 63 | } 64 | 65 | app.listen(config.client.devServer.client_port) 66 | log(`Server started at http://localhost:${config.client.devServer.client_port}`) 67 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import configureStore from './store/configureStore' 4 | import AppContainer from './containers/AppContainer' 5 | import { apolloClient } from './lib/apolloClient' 6 | 7 | // instantiate store 8 | const initialState = window.__INITIAL_STATE__ 9 | const store = configureStore(initialState) 10 | 11 | // render setup 12 | const MOUNT_NODE = document.getElementById('root') 13 | 14 | let render = () => { 15 | const routes = require('./routes/index').default(store) 16 | ReactDOM.render( 17 | , 18 | MOUNT_NODE 19 | ) 20 | } 21 | 22 | if (__DEV__) { 23 | if (module.hot) { 24 | const renderApp = render 25 | const renderError = (error) => { 26 | const RedBox = require('redbox-react').default 27 | ReactDOM.render(, MOUNT_NODE) 28 | } 29 | 30 | render = () => { 31 | try { 32 | renderApp() 33 | } catch (error) { 34 | console.error(error) // eslint-disable-line 35 | renderError(error) 36 | } 37 | } 38 | 39 | module.hot.accept('./routes/index', () => setImmediate(() => { 40 | ReactDOM.unmountComponentAtNode(MOUNT_NODE) 41 | render() 42 | })) 43 | } 44 | } 45 | 46 | render() 47 | -------------------------------------------------------------------------------- /src/client/layouts/CoreLayout/CoreLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from '../../components/Header' 3 | import './CoreLayout.scss' 4 | import '../../styles/core.scss' 5 | 6 | export const CoreLayout = ({ children }) => ( 7 |
8 |
9 |
10 | {children} 11 |
12 |
13 | ) 14 | 15 | CoreLayout.propTypes = { 16 | children: React.PropTypes.element.isRequired 17 | } 18 | 19 | export default CoreLayout 20 | -------------------------------------------------------------------------------- /src/client/layouts/CoreLayout/CoreLayout.scss: -------------------------------------------------------------------------------- 1 | .core-layout__viewport { 2 | padding-top: 4rem; 3 | } -------------------------------------------------------------------------------- /src/client/layouts/CoreLayout/index.js: -------------------------------------------------------------------------------- 1 | import CoreLayout from './CoreLayout' 2 | 3 | export default CoreLayout -------------------------------------------------------------------------------- /src/client/lib/apolloClient.js: -------------------------------------------------------------------------------- 1 | import ApolloClient, { createNetworkInterface } from 'apollo-client' 2 | 3 | export const apolloClient = new ApolloClient({ 4 | networkInterface: createNetworkInterface({ 5 | uri: '/graphql' 6 | }) 7 | }) 8 | 9 | export const apolloMiddleware = apolloClient.middleware() 10 | 11 | export const apolloReducer = apolloClient.reducer() 12 | 13 | export default apolloClient 14 | -------------------------------------------------------------------------------- /src/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jancarloviray/graphql-react-starter/43e158dacf306db41ae9d15dfb5524103c49bcf3/src/client/public/favicon.ico -------------------------------------------------------------------------------- /src/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My App 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/client/routes/Counter/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Counter = (props) => { 4 | return ( 5 |
6 |

Counter: {props.counter}

7 | 10 | {' '} 11 | 14 |
15 | ) 16 | } 17 | 18 | Counter.propTypes = { 19 | counter: React.PropTypes.number.isRequired, 20 | doubleAsync: React.PropTypes.func.isRequired, 21 | increment: React.PropTypes.func.isRequired 22 | } 23 | 24 | export default Counter 25 | -------------------------------------------------------------------------------- /src/client/routes/Counter/containers/CounterContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { increment, doubleAsync } from '../modules/Counter' 3 | 4 | /* This is a container component. Notice it does not contain any JSX, 5 | nor does it import React. This component is **only** responsible for 6 | wiring in the actions and state necessary to render a presentational 7 | component - in this case, the counter: */ 8 | 9 | import Counter from '../components/Counter' 10 | 11 | /* Object of action creators (can also be function that returns object). 12 | Keys will be passed as props to presentational components. Here we are 13 | implementing our wrapper around increment; the component doesn't care */ 14 | 15 | const mapDispatchToProps = { 16 | increment: () => increment(1), 17 | doubleAsync 18 | } 19 | 20 | const mapStateToProps = (state) => ({ 21 | counter: state.counter 22 | }) 23 | 24 | /* Note: mapStateToProps is where you should use `reselect` to create selectors, ie: 25 | import { createSelector } from 'reselect' 26 | const counter = (state) => state.counter 27 | const tripleCount = createSelector(counter, (count) => count * 3) 28 | const mapStateToProps = (state) => ({ 29 | counter: tripleCount(state) 30 | }) 31 | Selectors can compute derived data, allowing Redux to store the minimal possible state. 32 | Selectors are efficient. A selector is not recomputed unless one of its arguments change. 33 | Selectors are composable. They can be used as input to other selectors. 34 | https://github.com/reactjs/reselect */ 35 | 36 | export default connect(mapStateToProps, mapDispatchToProps)(Counter) 37 | -------------------------------------------------------------------------------- /src/client/routes/Counter/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { injectReducer } from '../../store/reducers' 3 | 4 | export default (store) => ({ 5 | path: 'counter', 6 | /* Async getComponent is only invoked when route matches */ 7 | getComponent(nextState, cb) { 8 | /* Webpack - use 'require.ensure' to create a split point 9 | and embed an async module loader (jsonp) when bundling */ 10 | require.ensure([], (require) => { 11 | /* Webpack - use require callback to define 12 | dependencies for bundling */ 13 | const Counter = require('./containers/CounterContainer').default 14 | const reducer = require('./modules/Counter').default 15 | 16 | /* Add the reducer to the store on key 'counter' */ 17 | injectReducer(store, { key: 'counter', reducer }) 18 | 19 | /* Return getComponent */ 20 | cb(null, Counter) 21 | 22 | /* Webpack named bundle */ 23 | }, 'counter') 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /src/client/routes/Counter/modules/counter.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | export const COUNTER_INCREMENT = 'COUNTER_INCREMENT' 3 | export const COUNTER_DOUBLE_ASYNC = 'COUNTER_DOUBLE_ASYNC' 4 | 5 | // Actions 6 | export function increment(value = 1) { 7 | return { 8 | type: COUNTER_INCREMENT, 9 | payload: value 10 | } 11 | } 12 | 13 | /* This is a thunk, meaning it is a function that immediately 14 | returns a function for lazy evaluation. It is incredibly useful for 15 | creating async actions, especially when combined with redux-thunk! */ 16 | 17 | export const doubleAsync = () => { 18 | return (dispatch, getState) => { 19 | return new Promise((resolve) => { 20 | setTimeout(() => { 21 | dispatch({ 22 | type: COUNTER_DOUBLE_ASYNC, 23 | payload: getState().counter 24 | }) 25 | resolve() 26 | }, 200) 27 | }) 28 | } 29 | } 30 | 31 | export const actions = { 32 | increment, 33 | doubleAsync 34 | } 35 | 36 | // Action Handlers 37 | const ACTION_HANDLERS = { 38 | [COUNTER_INCREMENT]: (state, action) => state + action.payload, 39 | [COUNTER_DOUBLE_ASYNC]: (state) => state * 2 40 | } 41 | 42 | // Reducer 43 | const initialState = 0 44 | export default function counterReducer(state = initialState, action) { 45 | const handler = ACTION_HANDLERS[action.type] 46 | 47 | return handler ? handler(state, action) : state 48 | } 49 | -------------------------------------------------------------------------------- /src/client/routes/Home/assets/graphql-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 72 | -------------------------------------------------------------------------------- /src/client/routes/Home/assets/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jancarloviray/graphql-react-starter/43e158dacf306db41ae9d15dfb5524103c49bcf3/src/client/routes/Home/assets/react-logo.png -------------------------------------------------------------------------------- /src/client/routes/Home/assets/redux-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jancarloviray/graphql-react-starter/43e158dacf306db41ae9d15dfb5524103c49bcf3/src/client/routes/Home/assets/redux-logo.png -------------------------------------------------------------------------------- /src/client/routes/Home/components/HomeView.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReduxLogo from '../assets/redux-logo.png' 3 | import ReactLogo from '../assets/react-logo.png' 4 | import GraphQLLogo from '../assets/graphql-logo.svg' 5 | import css from './HomeView.scss' 6 | 7 | export const HomeView = () => ( 8 |
9 | GraphQL Logo 13 | GraphQL Logo 17 | Redux Logo 21 |
22 | ) 23 | 24 | export default HomeView -------------------------------------------------------------------------------- /src/client/routes/Home/components/HomeView.scss: -------------------------------------------------------------------------------- 1 | .logo { 2 | width: 200px; 3 | max-height: 100px; 4 | margin: 1.5rem auto; 5 | } 6 | .container { 7 | padding: 10 0px; 8 | } -------------------------------------------------------------------------------- /src/client/routes/Home/index.js: -------------------------------------------------------------------------------- 1 | import HomeView from './components/HomeView' 2 | 3 | // Sync route definition 4 | export default { 5 | component: HomeView 6 | } -------------------------------------------------------------------------------- /src/client/routes/Users/components/Users.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Users = (props) => { 4 | const users = props.data.users || [] 5 | let input = 'Hello World' 6 | 7 | return ( 8 |
9 |

Users: {users.length}

10 | (input = n)} placeholder='Name' /> 11 | 16 |
    17 | {users.map((user) => { 18 | return ( 19 |
  • 20 | {user.name} 21 |
      22 |
    • {`Accounts: ${user.accounts.length}`}
    • 23 | {user.accounts.map((account) => { 24 | return ( 25 |
    • 27 | {`${account.accountId} Type: ${account.type} Total: ${account.total}`} 28 |
    • 29 | ) 30 | })} 31 |
    32 |
  • 33 | ) 34 | })} 35 |
36 |
37 | ) 38 | } 39 | 40 | Users.propTypes = { 41 | data: React.PropTypes.object.isRequired, 42 | doubleAsync: React.PropTypes.func.isRequired, 43 | increment: React.PropTypes.func.isRequired 44 | } 45 | 46 | export default Users 47 | -------------------------------------------------------------------------------- /src/client/routes/Users/containers/UsersContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { increment, doubleAsync } from '../modules/Users' 3 | 4 | import { graphql, compose } from 'react-apollo' 5 | import gql from 'graphql-tag' 6 | 7 | /* This is a container component. Notice it does not contain any JSX, 8 | nor does it import React. This component is **only** responsible for 9 | wiring in the actions and state necessary to render a presentational 10 | component - in this case, the Users: */ 11 | 12 | import Users from '../components/Users' 13 | 14 | /* Object of action creators (can also be function that returns object). 15 | Keys will be passed as props to presentational components. Here we are 16 | implementing our wrapper around increment; the component doesn't care */ 17 | 18 | const USERS_QUERY = gql` 19 | query { 20 | users { 21 | userId 22 | name 23 | accounts { 24 | accountId 25 | type 26 | total 27 | } 28 | } 29 | } 30 | ` 31 | 32 | const createUser = gql` 33 | mutation createUser($name: String!){ 34 | createUser(name: $name) 35 | }, 36 | ` 37 | 38 | const mapDispatchToProps = { 39 | increment: () => increment(1), 40 | doubleAsync 41 | } 42 | 43 | /* Note: mapStateToProps is where you should use `reselect` to create selectors, ie: 44 | import { createSelector } from 'reselect' 45 | const Users = (state) => state.users 46 | const tripleCount = createSelector(users, (count) => count * 3) 47 | const mapStateToProps = (state) => ({ 48 | users: tripleCount(state) 49 | }) 50 | Selectors can compute derived data, allowing Redux to store the minimal possible state. 51 | Selectors are efficient. A selector is not recomputed unless one of its arguments change. 52 | Selectors are composable. They can be used as input to other selectors. 53 | https://github.com/reactjs/reselect */ 54 | 55 | export default compose( 56 | graphql(USERS_QUERY, { 57 | // too aggressive, but just for demonstration 58 | options: { pollInterval: 5000 } 59 | }), 60 | graphql(createUser, { 61 | props: ({ mutate }) => ({ 62 | createUser: (name) => mutate({ variables: { name } }) 63 | }) 64 | }), 65 | connect(null, mapDispatchToProps) 66 | )(Users) 67 | -------------------------------------------------------------------------------- /src/client/routes/Users/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { injectReducer } from '../../store/reducers' 3 | 4 | export default (store) => ({ 5 | path: 'users', 6 | /* Async getComponent is only invoked when route matches */ 7 | getComponent(nextState, cb) { 8 | /* Webpack - use 'require.ensure' to create a split point 9 | and embed an async module loader (jsonp) when bundling */ 10 | require.ensure([], (require) => { 11 | /* Webpack - use require callback to define 12 | dependencies for bundling */ 13 | const Users = require('./containers/UsersContainer').default 14 | const reducer = require('./modules/Users').default 15 | 16 | /* Add the reducer to the store on key 'users' */ 17 | injectReducer(store, { key: 'users', reducer }) 18 | 19 | /* Return getComponent */ 20 | cb(null, Users) 21 | 22 | /* Webpack named bundle */ 23 | }, 'users') 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /src/client/routes/Users/modules/users.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | export const USERS_INCREMENT = 'USERS_INCREMENT' 3 | export const USERS_DOUBLE_ASYNC = 'USERS_DOUBLE_ASYNC' 4 | 5 | // Actions 6 | export function increment(value = 1) { 7 | return { 8 | type: USERS_INCREMENT, 9 | payload: value 10 | } 11 | } 12 | 13 | export const doubleAsync = () => { 14 | return (dispatch, getState) => { 15 | return new Promise((resolve) => { 16 | setTimeout(() => { 17 | dispatch({ 18 | type: USERS_DOUBLE_ASYNC, 19 | payload: getState().users 20 | }) 21 | resolve() 22 | }, 200) 23 | }) 24 | } 25 | } 26 | 27 | export const actions = { 28 | increment, 29 | doubleAsync 30 | } 31 | 32 | // Action Handlers 33 | const ACTION_HANDLERS = { 34 | [USERS_INCREMENT]: (state, action) => state + action.payload, 35 | [USERS_DOUBLE_ASYNC]: (state) => state * 2 36 | } 37 | 38 | // Reducer 39 | const initialState = 0 40 | export default function usersReducer(state = initialState, action) { 41 | const handler = ACTION_HANDLERS[action.type] 42 | 43 | return handler ? handler(state, action) : state 44 | } 45 | -------------------------------------------------------------------------------- /src/client/routes/index.js: -------------------------------------------------------------------------------- 1 | // Routing References: 2 | // https://github.com/ReactTraining/react-router/blob/master/docs/Introduction.md 3 | // https://github.com/ReactTraining/react-router/blob/master/docs/API.md 4 | // Overview: https://github.com/ReactTraining/react-router/tree/master/docs/guides 5 | // Route Configuration: https://github.com/ReactTraining/react-router/blob/master/docs/guides/RouteConfiguration.md 6 | // Route Matching: https://github.com/ReactTraining/react-router/blob/master/docs/guides/RouteMatching.md 7 | // Histories: https://github.com/ReactTraining/react-router/blob/master/docs/guides/Histories.md 8 | // Index Routes: https://github.com/ReactTraining/react-router/blob/master/docs/guides/IndexRoutes.md 9 | // Dynamic Routing: https://github.com/ReactTraining/react-router/blob/master/docs/guides/DynamicRouting.md 10 | // Confirming Navigation: https://github.com/ReactTraining/react-router/blob/master/docs/guides/ConfirmingNavigation.md 11 | 12 | // We only need to import the modules necessary for initial render 13 | import CoreLayout from '../layouts/CoreLayout' 14 | import Home from './Home' 15 | import CounterRoute from './Counter' 16 | import UsersRoute from './Users' 17 | 18 | /* Note: Instead of using JSX, we recommend using react-router 19 | PlainRoute objects to build route definitions. */ 20 | 21 | export const createRoutes = (store) => ({ 22 | path: '/', 23 | component: CoreLayout, 24 | indexRoute: Home, 25 | childRoutes: [ 26 | CounterRoute(store), 27 | UsersRoute(store) 28 | ] 29 | }) 30 | 31 | /* Note: childRoutes can be chunked or otherwise loaded programmatically 32 | using getChildRoutes with the following signature: 33 | 34 | getChildRoutes (location, cb) { 35 | require.ensure([], (require) => { 36 | cb(null, [ 37 | // Remove imports! 38 | require('./Counter').default(store) 39 | ]) 40 | }) 41 | } 42 | 43 | However, this is not necessary for code-splitting! It simply provides 44 | an API for async route definitions. Your code splitting should occur 45 | inside the route `getComponent` function, since it is only invoked 46 | when the route exists and matches. 47 | */ 48 | 49 | export default createRoutes -------------------------------------------------------------------------------- /src/client/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux' 2 | import { browserHistory } from 'react-router' 3 | import createLogger from 'redux-logger' 4 | import thunk from 'redux-thunk' 5 | import { apolloMiddleware } from '../lib/apolloClient' 6 | 7 | import { updateLocation } from './location' 8 | import makeRootReducer from './reducers' 9 | 10 | export default (initialState = {}) => { 11 | const enhancers = [] 12 | const middleware = [apolloMiddleware, thunk] 13 | 14 | let composeEnhancers = compose 15 | 16 | if (__DEV__) { 17 | // logs redux events in browser console 18 | const logger = createLogger({ 19 | duration: true, 20 | collapsed: false, 21 | diff: true, 22 | }) 23 | 24 | middleware.push(logger) 25 | 26 | // https://github.com/zalmoxisus/redux-devtools-extension 27 | if (typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { 28 | composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 29 | // if there’s an exception in reducers, show the error message 30 | // and don't dispatch next action 31 | shouldCatchErrors: true, 32 | actionBlacklist: [], 33 | }) 34 | } 35 | } 36 | 37 | // compose store enhancers 38 | const enhancer = composeEnhancers( 39 | applyMiddleware(...middleware), 40 | ...enhancers 41 | ) 42 | 43 | // instantiate store 44 | const store = createStore( 45 | makeRootReducer(), 46 | initialState, 47 | enhancer 48 | ) 49 | 50 | store.asyncReducers = {} 51 | 52 | // To unsubscribe, invoke `store.unsubscribeHistory()` anytime 53 | store.unsubscribeHistory = browserHistory.listen(updateLocation(store)) 54 | 55 | // enable hot loading on reducers 56 | if (module.hot) { 57 | module.hot.accept('./reducers', () => { 58 | const reducers = require('./reducers').default 59 | store.replaceReducer(reducers(store.asyncReducers)) 60 | }) 61 | } 62 | 63 | return store 64 | } 65 | -------------------------------------------------------------------------------- /src/client/store/location.js: -------------------------------------------------------------------------------- 1 | // Constant 2 | export const LOCATION_CHANGE = 'LOCATION_CHANGE' 3 | 4 | // Action 5 | export function locationChange(location = '/') { 6 | return { 7 | type: LOCATION_CHANGE, 8 | payload: location 9 | } 10 | } 11 | 12 | // Action Creator 13 | export const updateLocation = ({ dispatch }) => { 14 | return (nextLocation) => dispatch(locationChange(nextLocation)) 15 | } 16 | 17 | // Reducer 18 | const initialState = null 19 | export default function locationReducer(state = initialState, action) { 20 | return action.type === LOCATION_CHANGE ? action.payload : state 21 | } -------------------------------------------------------------------------------- /src/client/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import locationReducer from './location' 3 | import { apolloReducer } from '../lib/apolloClient' 4 | 5 | export const makeRootReducer = (asyncReducers) => { 6 | return combineReducers({ 7 | location: locationReducer, 8 | apollo: apolloReducer, 9 | ...asyncReducers 10 | }) 11 | } 12 | 13 | export const injectReducer = (store, { key, reducer }) => { 14 | if (Object.hasOwnProperty.call(store.asyncReducers, key)) return 15 | 16 | store.asyncReducers[key] = reducer 17 | store.replaceReducer(makeRootReducer(store.asyncReducers)) 18 | } 19 | 20 | export default makeRootReducer -------------------------------------------------------------------------------- /src/client/styles/_base.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Application Settings Go Here 3 | ------------------------------------ 4 | This file acts as a bundler for all variables/mixins/themes, so they 5 | can easily be swapped out without `core.scss` ever having to know. 6 | For example: 7 | @import './variables/colors'; 8 | @import './variables/components'; 9 | @import './themes/default'; 10 | */ -------------------------------------------------------------------------------- /src/client/styles/core.scss: -------------------------------------------------------------------------------- 1 | 2 | @import 'base'; 3 | @import '~normalize.css/normalize'; 4 | 5 | // Some best-practice CSS that's useful for most apps 6 | // Just remove them if they're not what you want 7 | html { 8 | box-sizing: border-box; 9 | } 10 | 11 | html, 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | height: 100%; 16 | } 17 | 18 | *, 19 | *:before, 20 | *:after { 21 | box-sizing: inherit; 22 | } -------------------------------------------------------------------------------- /src/db/Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jancarloviray/graphql-react-starter/43e158dacf306db41ae9d15dfb5524103c49bcf3/src/db/Dockerfile -------------------------------------------------------------------------------- /src/db/lib/db.js: -------------------------------------------------------------------------------- 1 | const knex = require('knex') 2 | const knexfile = require('../../api/knexfile') 3 | const debug = require('debug') 4 | const util = require('util') 5 | 6 | const log = debug('app:db') 7 | 8 | const env = process.env.NODE_ENV 9 | const config = knexfile[env] 10 | 11 | log(`Connected to ${config.client} with parameters: ${util.inspect(config.connection, { colors: true })}`) 12 | 13 | module.exports = knex(config) 14 | -------------------------------------------------------------------------------- /src/db/migrations/20161228171145_initial-schema.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | ┌──refId:userId 4 | ▼ │ 5 | ┌───────────────┐ │ ┌─────────────────┐ ┌──────────────────────┐ 6 | │ │ │ │ │ │ │ 7 | │ userId:pk │ │ │ accountId:pk │ │ transactionId:pk │ 8 | │ refId:fk │ │ │ name │ │ transactionTypeId:fk │ 9 | │ createdDate │──┘ │ type │◀────│ accountId:fk │ 10 | │ name │ │ total │ │ sessionId │ 11 | │ │ │ createdDate │ │ amount │ 12 | │ │ │ updatedDate │ │ note │ 13 | │ │ │ │ │ createdDate │ 14 | └───────────────┘ └─────────────────┘ └──────────────────────┘ 15 | ▲ ▲ ▲ 16 | │ ┌──────────────────┐ │ ┌──────────────────────┐ 17 | │ │ │ │ │ │ 18 | └─│ userId:pk,fk │─┘ │ transactionTypeId:pk │ 19 | │ accountId:pk,fk │ │ name │ 20 | └──────────────────┘ └──────────────────────┘ 21 | */ 22 | 23 | exports.up = function (knex) { 24 | return knex.schema 25 | // Drop Existing Tables 26 | .dropTableIfExists('Users') 27 | .dropTableIfExists('Accounts') 28 | .dropTableIfExists('Users_Accounts') 29 | .dropTableIfExists('Transactions') 30 | .dropTableIfExists('TransactionTypes') 31 | // Create Tables 32 | .createTable('Users', (table) => { 33 | table.increments('userId').primary() 34 | table.integer('refId').references('Users.userId').comment('Represents referral Id') 35 | table.text('name').notNullable() 36 | table.dateTime('createdDate').defaultTo(knex.fn.now()) 37 | }) 38 | .createTable('Accounts', (table) => { 39 | table.increments('accountId').primary() 40 | table.string('name') 41 | table.string('type').notNullable() 42 | table.decimal('total', 10, 2).defaultTo(0) 43 | table.dateTime('createdDate').notNullable().defaultTo(knex.fn.now()) 44 | table.dateTime('updatedDate').notNullable().defaultTo(knex.fn.now()) 45 | }) 46 | .createTable('Users_Accounts', (table) => { 47 | table.integer('accountId').references('Accounts.accountId') 48 | table.integer('userId').references('Users.userId') 49 | table.primary(['accountId', 'userId']) 50 | }) 51 | .createTable('TransactionTypes', (table) => { 52 | table.increments('transactionTypeId') 53 | table.string('name').notNullable() 54 | }) 55 | .createTable('Transactions', (table) => { 56 | table.increments('transactionId').primary() 57 | table.integer('transactionTypeId').notNullable().references('TransactionTypes.transactionTypeId') 58 | table.integer('accountId').notNullable().references('Accounts.accountId') 59 | table.string('sessionId').notNullable().comment('Login session id') 60 | table.decimal('amount', 10, 2).notNullable().defaultTo(0) 61 | table.string('note') 62 | table.dateTime('createdDate').notNullable().defaultTo(knex.fn.now()) 63 | }) 64 | } 65 | 66 | exports.down = function (knex) { 67 | return knex.schema 68 | .dropTableIfExists('Users') 69 | .dropTableIfExists('Accounts') 70 | .dropTableIfExists('Users_Accounts') 71 | .dropTableIfExists('Transactions') 72 | .dropTableIfExists('TransactionTypes') 73 | } 74 | -------------------------------------------------------------------------------- /src/db/seeds/000-initial-seed.js: -------------------------------------------------------------------------------- 1 | const casual = require('casual') 2 | const _ = require('lodash') 3 | 4 | casual.seed(123) 5 | 6 | const numUsersSeed = 10 7 | const users = _.concat( 8 | { refId: null, name: casual.name }, 9 | _.times(numUsersSeed, () => ({ 10 | refId: casual.random_element([null, 1, 2, 3]), 11 | name: casual.name 12 | })) 13 | ) 14 | 15 | const numAccountSeed = 5 16 | const accounts = _.times(numAccountSeed, () => ({ 17 | name: casual.word, 18 | type: casual.random_element(['checking', 'savings']), 19 | total: casual.double(0, 100000), 20 | })) 21 | 22 | const users_accounts = _.times(10, () => ({ 23 | userId: casual.integer(1, numUsersSeed), 24 | accountId: casual.integer(1, numAccountSeed), 25 | })) 26 | 27 | const transactionTypes = [ 28 | { transactionTypeId: 1, name: 'withdraw' }, 29 | { transactionTypeId: 2, name: 'deposit' } 30 | ] 31 | 32 | // on sql insert from app, must subtract or add from account, based on 33 | // transaction type but this is ok for seed 34 | const numTransactionSeed = 100 35 | const transactions = _.times(numTransactionSeed, () => ({ 36 | transactionTypeId: casual.random_element([1, 2]), 37 | accountId: casual.integer(1, numAccountSeed), 38 | sessionId: casual.title.replace(/\s/g, ''), 39 | amount: casual.double(1, 100), 40 | note: casual.sentence, 41 | })) 42 | 43 | exports.seed = function (knex) { 44 | return knex.transaction((trx) => { 45 | Promise.all([ 46 | // delete tables 47 | knex('Users_Accounts').transacting(trx).del(), 48 | knex('Transactions').transacting(trx).del(), 49 | knex('Users').transacting(trx).del(), 50 | knex('Accounts').transacting(trx).del(), 51 | knex('TransactionTypes').transacting(trx).del(), 52 | // insert data 53 | knex('Users').transacting(trx).insert(users), 54 | knex('Accounts').transacting(trx).insert(accounts), 55 | knex('Users_Accounts').transacting(trx).insert(users_accounts), 56 | knex('TransactionTypes').transacting(trx).insert(transactionTypes), 57 | knex('Transactions').transacting(trx).insert(transactions), 58 | ]).then(trx.commit).catch(trx.rollback) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /src/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.11 2 | 3 | RUN mkdir /etc/nginx/sites-available && rm /etc/nginx/conf.d/default.conf 4 | ADD nginx.conf /etc/nginx/ 5 | 6 | COPY scripts /root/scripts/ 7 | COPY certs/* /etc/ssl/ 8 | 9 | COPY sites /etc/nginx/templates 10 | 11 | ARG CLIENT_PORT=3000 12 | ARG API_PORT=8080 13 | ARG WEB_SSL=false 14 | ARG SELF_SIGNED=false 15 | ARG NO_DEFAULT=false 16 | 17 | ENV CLIENT_PORT=$CLIENT_PORT 18 | ENV API_PORT=$API_PORT 19 | ENV WEB_SSL=$WEB_SSL 20 | ENV SELF_SIGNED=$SELF_SIGNED 21 | ENV NO_DEFAULT=$NO_DEFAULT 22 | 23 | RUN /bin/bash /root/scripts/build-nginx.sh 24 | 25 | CMD nginx 26 | -------------------------------------------------------------------------------- /src/nginx/certs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /src/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes auto; 3 | pid /run/nginx.pid; 4 | daemon off; 5 | 6 | events { 7 | worker_connections 2048; 8 | use epoll; 9 | } 10 | 11 | http { 12 | server_tokens off; 13 | sendfile on; 14 | tcp_nopush on; 15 | tcp_nodelay on; 16 | keepalive_timeout 15; 17 | types_hash_max_size 2048; 18 | client_max_body_size 20M; 19 | open_file_cache max=100; 20 | gzip on; 21 | gzip_disable "msie6"; 22 | 23 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 24 | ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS'; 25 | 26 | include /etc/nginx/mime.types; 27 | default_type application/octet-stream; 28 | 29 | include /etc/nginx/conf.d/*.conf; 30 | include /etc/nginx/sites-available/*; 31 | access_log /var/log/nginx/access.log; 32 | error_log /var/log/nginx/error.log; 33 | } 34 | -------------------------------------------------------------------------------- /src/nginx/scripts/build-nginx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for conf in /etc/nginx/templates/*.conf; do 4 | mv $conf "/etc/nginx/sites-available/"$(basename $conf) > /dev/null 5 | done 6 | 7 | for template in /etc/nginx/templates/*.template; do 8 | envsubst < $template > "/etc/nginx/sites-available/"$(basename $template)".conf" 9 | done 10 | 11 | if [[ "$NO_DEFAULT" = true ]]; then 12 | rm /etc/nginx/sites-available/node.template.conf 13 | rm /etc/nginx/sites-available/node-https.template.conf 14 | else 15 | if [[ "$WEB_SSL" = false ]]; then 16 | rm /etc/nginx/sites-available/node-https.template.conf 17 | fi 18 | fi 19 | 20 | if [[ "$WEB_SSL" = true && "$NO_DEFAULT" = false ]]; then 21 | if [[ "$SELF_SIGNED" = true ]]; then 22 | echo "---------------------------------------------------------" 23 | echo "NGINX: Generating certificates" 24 | echo "---------------------------------------------------------" 25 | openssl req \ 26 | -new \ 27 | -newkey rsa:4096 \ 28 | -days 1095 \ 29 | -nodes \ 30 | -x509 \ 31 | -subj "/C=FK/ST=Fake/L=Fake/O=Fake/CN=0.0.0.0" \ 32 | -keyout /etc/ssl/privkey1.pem \ 33 | -out /etc/ssl/cert1.pem 34 | chown www-data:www-data /etc/ssl/cert1.pem 35 | chown www-data:www-data /etc/ssl/privkey1.pem 36 | else 37 | echo "---------------------------------------------------------" 38 | echo "NGINX: Using certificates in 'nodock/nginx/certs/'" 39 | echo "---------------------------------------------------------" 40 | if [ -e /var/certs/cert1.pem ]; then 41 | cp /var/certs/cert1.pem /etc/ssl/cert1.pem 42 | fi 43 | if [ -e /var/certs/privkey1.pem ]; then 44 | cp /var/certs/privkey1.pem /etc/ssl/privkey1.pem 45 | fi 46 | fi 47 | fi 48 | -------------------------------------------------------------------------------- /src/nginx/sites/node-https.template: -------------------------------------------------------------------------------- 1 | # environment variables 2 | # CLIENT_PORT ${CLIENT_PORT} 3 | server { 4 | listen 443 default_server http2; 5 | 6 | ssl on; 7 | ssl_certificate /etc/ssl/cert1.pem; 8 | ssl_certificate_key /etc/ssl/privkey1.pem; 9 | 10 | location / { 11 | proxy_pass http://client:${CLIENT_PORT}; 12 | } 13 | 14 | location /graphql { 15 | proxy_pass http://api:${API_PORT}/graphql; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/nginx/sites/node.template: -------------------------------------------------------------------------------- 1 | # environment variables 2 | # CLIENT_PORT ${CLIENT_PORT} 3 | server { 4 | listen 80 default_server; 5 | 6 | location / { 7 | proxy_pass http://client:${CLIENT_PORT}; 8 | } 9 | 10 | location /graphql { 11 | proxy_pass http://api:${API_PORT}/graphql; 12 | } 13 | 14 | location /.well-known/acme-challenge/ { 15 | root /var/www/letsencrypt/; 16 | log_not_found off; 17 | } 18 | } 19 | --------------------------------------------------------------------------------