Skip to content

Commit

Permalink
Dockerize + misc. bug fixes (#58)
Browse files Browse the repository at this point in the history
* dockerize: run demo server with just "docker-compose up"!

also adds a "yarn start:windows" npm script to use nodemon's legacy
watch mode when using Docker Desktop for Windows

* .travis.yml: update yarn version to latest + enable for node 12

* demo: update app to play nicely with paperplane v2+ (parseJson is async now)

* tests: get serve tests passing in docker

...and still have them pass if/when run locally on Windows/Mac/Linux

* error: fix handling of boomError to ensure they're serialized as json properly

without the deep merge, the `content-type: application/json` header set
by the json helper gets clobbered b/c boom errors have output.headers
set (to an empty object by default)

fleshed out tests related to boom errors thrown by paperplane to ensure
the response body is set correctly as well (these tests fail without the
change in lib/error.js)

* tests: assert formatting of response body for error cases

* demo: add routes for more types of errors that are automatically handled

* demo: log (error | what port we're listening on) on start up

* mount: handle any exceptions/rejected promises coming out of cry

Fixes #43

* .travis.yml: un-bump yarn version to get nyc/coverage working again

* [Security] Bump handlebars from 4.1.2 to 4.6.0

Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.1.2 to 4.6.0. **This update includes a security fix.**
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md)
- [Commits](handlebars-lang/handlebars.js@v4.1.2...v4.6.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
  • Loading branch information
Dane Bertram and dependabot-preview[bot] committed Jan 10, 2020
1 parent 961a46c commit c04edc3
Show file tree
Hide file tree
Showing 19 changed files with 154 additions and 34 deletions.
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.git
Dockerfile
.gitignore
node_modules
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.nyc_output
.yarn*
node_modules
/docker-compose.override.yml
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
language: node_js
node_js:
- '12'
- '10'
- '8'
- '6'
before_install:
- npm install -g yarn@1.10.1
- npm install -g yarn@1.10.1 # nyc does not run properly with newer yarn for some reason
install:
- yarn install --pure-lockfile
script:
Expand Down
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM node:12-stretch-slim

WORKDIR /usr/src/app

COPY package.json yarn.lock ./

RUN yarn install --pure-lockfile

COPY . .

EXPOSE 3000

CMD ["yarn", "start"]
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,15 @@ http.createServer(mount({ app })).listen(3000, logger)
```

So simple and functional, with an easily readable routing table and pure functions for the route handler. If that sounds like fun to you, then read the [Getting started guide](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md) or the [API docs](https://github.com/articulate/paperplane/blob/master/docs/API.md) to learn more.

## Example application

To help you learn the concepts used in paperplane, check out the [demo application](https://github.com/articulate/paperplane/blob/master/demo).

If you have docker installed, you can run the demo locally:

1. Clone this repo
1. If you're using Docker Desktop for Windows:
- `cp docker-compose.override.windows.yml docker-compose.override.yml`
1. `docker-compose up`
1. http://localhost:3000
10 changes: 7 additions & 3 deletions demo/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require('./lib/seed')()

const { compose } = require('ramda')
const { composeP } = require('ramda')
const future = require('redux-future').default
const http = require('http')
const { mount, parseJson } = require('..')
Expand All @@ -10,10 +10,14 @@ const routes = require('./routes')

const port = process.env.PORT || 3000

const app = compose(routes, parseJson)
const listening = err => err ? console.error(err) : console.info(`Listing on port: ${port}`)

const app = composeP(routes, parseJson)

const middleware = [ future ]

const server = http.createServer(mount({ app, logger, middleware }))

if (require.main === module) server.listen(port, logger)
if (require.main === module) server.listen(port, listening)

module.exports = server
16 changes: 11 additions & 5 deletions demo/routes/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
const { validate } = require('@articulate/funky')
const Boom = require('@hapi/boom')
const { NotAcceptable } = require('http-errors')
const Joi = require('joi')
const { always, compose, prop } = require('ramda')
const { json, methods, routes, send, serve } = require('../..')

const pages = require('./pages')
const users = require('./users')

const error = () => {
throw new Error('this code is broken')
}

module.exports = routes({
'/api/old-users': methods({
GET: users.oldUsers
Expand All @@ -26,7 +26,13 @@ module.exports = routes({

'/cookies': compose(json, prop('cookies')),

'/error': error,
'/errors/basic': () => { throw new Error('this code is broken') },

'/errors/boom': () => { throw Boom.conflict() },

'/errors/http': () => { throw new NotAcceptable() },

'/errors/joi': () => validate(Joi.string(), 123),

'/health': always(send()),

Expand Down
4 changes: 4 additions & 0 deletions docker-compose.override.windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
version: '2'
services:
app:
command: yarn start:windows # use nodemon's legacy watch mode to detect file changes on Windows
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: '2'
services:
app:
build: .
image: paperplane-demo
ports:
- 3000:3000
volumes:
- '.:/usr/src/app'
- '/usr/src/app/node_modules'
4 changes: 2 additions & 2 deletions lib/error.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
const {
assoc, cond, evolve, flip, merge, pick, pipe, prop, T
assoc, cond, evolve, flip, merge, mergeDeepRight, pick, pipe, prop, T
} = require('ramda')

const { json } = require('./json')

const boomError = ({ output: { payload, statusCode, headers } }) =>
merge(json(payload), { statusCode, headers })
mergeDeepRight(json(payload), { statusCode, headers })

exports.error = err =>
pipe(
Expand Down
10 changes: 8 additions & 2 deletions lib/mount.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { compose, curry, identity, merge, is, when, tap } = require('ramda')
const { compose, curry, identity, merge, is, when, tap, tryCatch } = require('ramda')

const { bufferStreams } = require('./bufferStreams')
const { cleanRequest } = require('./cleanRequest')
Expand Down Expand Up @@ -51,8 +51,14 @@ exports.mount = (opts={}) => {
.then(writeLambda(req))
.then(tap(res => logger({ req, res })))

const ignoreRejected = obj => Promise.resolve(obj).catch(identity)

const ignoreIfThrows = fn => tryCatch(fn, identity)

const safeCry = compose(ignoreRejected, ignoreIfThrows(cry))

const sob = req => err =>
cry(Object.assign(err, { req }))
safeCry(Object.assign(err, { req }))

const wrapError = req =>
when(is(Error), compose(error, tap(sob(req))))
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"coverage": "nyc report --reporter=text-lcov | coveralls",
"lint": "eslint .",
"start": "nodemon -w demo -w lib demo/index.js",
"start:windows": "yarn start -L",
"test": "mocha --reporter dot",
"test:ci": "yarn run lint && yarn run test:coverage && yarn run coverage",
"test:coverage": "nyc yarn run test"
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/static-file.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
testing testing
testing testing
2 changes: 1 addition & 1 deletion test/fixtures/sub/static-file.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
testing testing sub
testing testing sub
11 changes: 10 additions & 1 deletion test/methods.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { expect } = require('chai')
const { always: K } = require('ramda')
const http = require('http')
const request = require('supertest')
Expand All @@ -23,6 +24,14 @@ describe('methods', function() {
)

it('404 Not Founds when no matching method is found', () =>
agent.post('/').send({}).expect(404)
agent.post('/').send({})
.expect(404)
.then(res => {
expect(res.body).to.eql({
error: 'Not Found',
message: 'Not Found',
statusCode: 404
})
})
)
})
47 changes: 44 additions & 3 deletions test/mount.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,14 @@ describe('mount', () => {

describe('errors', () => {
it('defaults statusCode to 500', () =>
agent.get('/error').expect(500)
agent.get('/error')
.expect(500)
.then(res => {
expect(res.body).to.eql({
name: 'Error',
message: 'error'
})
})
)

it('catches and formats boom errors', () =>
Expand All @@ -184,14 +191,48 @@ describe('mount', () => {
.expect('www-authenticate', /Basic/)
.expect('www-authenticate', /realm="protected area"/)
.expect('www-authenticate', /error="error message"/)
.then(res => {
expect(res.body).to.eql({
statusCode: 401,
error: 'Unauthorized',
message: 'error message',
attributes: {
realm: 'protected area',
error: 'error message'
}
})
})
)

it('catches and formats http-errors', () =>
agent.get('/http').expect(404)
agent.get('/http')
.expect(404)
.then(res => {
expect(res.body).to.eql({
name: 'NotFoundError',
message: 'Not Found'
})
})
)

it('catches and formats joi errors', () =>
agent.get('/joi').expect(400)
agent.get('/joi')
.expect(400)
.then(res => {
expect(res.body).to.eql({
details: [{
message: '"value" must be a string',
path: [],
type: 'string.base',
context: {
value: 123,
label: 'value'
}
}],
message: '"value" must be a string',
name: 'ValidationError'
})
})
)
})

Expand Down
11 changes: 10 additions & 1 deletion test/routes.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { expect } = require('chai')
const { always: K, compose, pick } = require('ramda')
const http = require('http')
const request = require('supertest')
Expand Down Expand Up @@ -28,6 +29,14 @@ describe('routes', () => {
)

it('404 Not Founds for unmatched routes', () =>
agent.get('/not-found').expect(404)
agent.get('/not-found')
.expect(404)
.then(res => {
expect(res.body).to.eql({
error: 'Not Found',
message: 'Not Found',
statusCode: 404
})
})
)
})
5 changes: 2 additions & 3 deletions test/serve.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const { always: K } = require('ramda')
const { EOL } = require('os')
const http = require('http')
const request = require('supertest')

Expand All @@ -15,11 +14,11 @@ describe('serve', () => {
const agent = request.agent(server)

it('responds with found static files', () =>
agent.get('/pub/static-file.txt').expect(200, 'testing testing' + EOL)
agent.get('/pub/static-file.txt').expect(200, 'testing testing')
)

it('responds with found for sub paths', () =>
agent.get('/pub/sub/static-file.txt').expect(200, 'testing testing sub' + EOL)
agent.get('/pub/sub/static-file.txt').expect(200, 'testing testing sub')
)

it('404 Not Founds for missing static files', () =>
Expand Down
22 changes: 11 additions & 11 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -690,10 +690,10 @@ commander@2.9.0:
dependencies:
graceful-readlink ">= 1.0.0"

commander@~2.20.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
commander@~2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==

commondir@^1.0.1:
version "1.0.1"
Expand Down Expand Up @@ -1550,9 +1550,9 @@ growl@1.9.2:
integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==

handlebars@^4.0.11:
version "4.1.2"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67"
integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==
version "4.6.0"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.6.0.tgz#33af6c3eda930d7a924f5d8f1c6d8edc3180512e"
integrity sha512-i1ZUP7Qp2JdkMaFon2a+b0m5geE8Z4ZTLaGkgrObkEd+OkUKyRbRWw4KxuFCoHfdETSY1yf9/574eVoNSiK7pw==
dependencies:
neo-async "^2.6.0"
optimist "^0.6.1"
Expand Down Expand Up @@ -4078,11 +4078,11 @@ uglify-js@^2.6.1:
yargs "~3.10.0"

uglify-js@^3.1.4:
version "3.6.0"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5"
integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==
version "3.7.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.4.tgz#e6d83a1aa32ff448bd1679359ab13d8db0fe0743"
integrity sha512-tinYWE8X1QfCHxS1lBS8yiDekyhSXOO6R66yNOCdUJeojxxw+PX2BHAz/BWyW7PQ7pkiWVxJfIEbiDxyLWvUGg==
dependencies:
commander "~2.20.0"
commander "~2.20.3"
source-map "~0.6.1"

uglify-to-browserify@~1.0.0:
Expand Down

0 comments on commit c04edc3

Please sign in to comment.