Skip to content
This repository has been archived by the owner on Dec 5, 2022. It is now read-only.

Commit

Permalink
Merge 9a2f5fc into 7abb4ae
Browse files Browse the repository at this point in the history
  • Loading branch information
mfellner committed Feb 1, 2017
2 parents 7abb4ae + 9a2f5fc commit 6950e97
Show file tree
Hide file tree
Showing 13 changed files with 772 additions and 930 deletions.
7 changes: 2 additions & 5 deletions packages/tessellate-server/.flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,5 @@ esproposal.class_static_fields=enable
esproposal.class_instance_fields=enable
experimental.strict_type_args=true

suppress_type=$FlowIssue
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue

[version]
^0.36.0
suppress_type=$FlowIgnore
suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore
77 changes: 77 additions & 0 deletions packages/tessellate-server/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,80 @@
# tessellate-server

Web service runtime for tessellate services.

### TessellateServer

```
import { TessellateServer } from 'tessellate-server'
```

##### constructor(options: Options = {})

* `name: string` Optional application name (see [koa.app.name](https://github.com/koajs/koa/blob/v2.x/docs/api/index.md#settings))

##### use(middleware: Middleware, defer: boolean = false): TessellateServer

Add koa [Middleware](https://github.com/koajs/koa/wiki#middleware) that runs **before** any routes are handled. If `defer` is set to `true`, the middleware will run **after** all routes. Also see [koa.app.use](https://github.com/koajs/koa/blob/v2.x/docs/api/index.md#appusefunction).

##### start(port: number | string, metricsPort?: number | string): Promise<TessellateServer>

Start the koa application server and [prometheus](https://github.com/siimon/prom-client) metrics server on the specified ports. The default value for `metricsPort` is `port + 1`.

#### TessellateServer.router

[koa-rx-router](https://github.com/mfellner/koa-router-rx) instance. Use it to add routes.

##### stop(): Promise<any>

Stop all koa servers.

### nconf

```javascript
import { nconf } from 'tessellate-server'
```

Wrapper around [nconf](https://github.com/indexzero/nconf) with default values and convenience methods.

* `set(key: string, value: any)` - see [nconf](https://github.com/indexzero/nconf)
* `get(key: string)` - see [nconf](https://github.com/indexzero/nconf)
* `getObject(key: string): Object` - see `get`
* `getString(key: string): string` - see `get`
* `argv(args: Object)` - see [nconf](https://github.com/indexzero/nconf#argv)
* `defaults(defaults: Object)` - see [nconf](https://github.com/indexzero/nconf)

### Problem

```javascript
import { Problem } from 'tessellate-server'
```

A throwable Error class modeled after [Zalando Problem](https://github.com/zalando/problem).

### Example

Run `npm start` or check out the code below:

```javascript
import { TessellateServer, Problem } from '../src'
import { Observable } from 'rxjs'

const server = new TessellateServer()

server.use((ctx, next) => {
console.log('Hi, this is middleware.')
return next()
})

server.router.get('/', o => o.mapTo('Hello!'))

server.router.get('/error', o => o.switchMapTo(Observable.throw(
new Problem({
title: 'Teapot',
detail: 'I am a teapot.',
status: 418
}))
))

server.start(3001)
```
5 changes: 5 additions & 0 deletions packages/tessellate-server/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Example config file
FOO: bar
BAR:
what: is
code: 42
19 changes: 16 additions & 3 deletions packages/tessellate-server/example/server.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
// @flow

import { TessellateServer } from '../src'
import { TessellateServer, Problem } from '../src'
import { Observable } from 'rxjs'

const server = new TessellateServer()

server.router.get('/', observable => observable.mapTo('Hello, tessellate!'))
server.use((ctx, next) => {
console.log('Hi, this is middleware.')
return next()
})

server.router.get('/', o => o.mapTo('Hello!'))

server.router.get('/error', o => o.switchMapTo(Observable.throw(
new Problem({
title: 'Teapot',
detail: 'I am a teapot.',
status: 418
}))
))

server.start(3001)
console.log('listening on http://localhost:3001')
39 changes: 21 additions & 18 deletions packages/tessellate-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,40 @@
"url": "https://github.com/zalando-incubator/tessellate.git"
},
"scripts": {
"clean": "rimraf dist/*",
"dist": "babel -d dist src",
"start": "babel-node example/server.js",
"dist": "webpack",
"test": "NODE_ENV=test MORGAN_THRESHOLD=500 jest --coverage",
"test": "NODE_ENV=test MORGAN_THRESHOLD=999 jest --coverage",
"flow-check": "flow check",
"flow-typed-install": "flow-typed install"
"flow-gen-files": "flow-copy-source -v src dist",
"flow-typed-install": "rimraf flow-typed/npm/*_vx.x.x.js && flow-typed install -o",
"prepublish": "npm run clean && npm run dist && npm run flow-gen-files"
},
"dependencies": {
"js-yaml": "3.7.0",
"koa": "2.0.0-alpha.7",
"koa": "2.0.0",
"koa-bodyparser": "3.2.0",
"koa-compose": "3.2.1",
"koa-morgan": "1.0.1",
"koa-router-rx": "0.1.2",
"koa-router-rx": "0.3.3",
"mz": "2.6.0",
"nconf": "0.8.4",
"prom-client": "6.2.0",
"prometheus-gc-stats": "0.3.1",
"rxjs": "5.0.0-rc.4"
"prom-client": "7.0.1",
"prometheus-gc-stats": "0.3.2"
},
"devDependencies": {
"babel-cli": "6.18.0",
"babel-core": "6.18.2",
"babel-jest": "17.0.2",
"babel-loader": "6.2.8",
"babel-cli": "6.22.2",
"babel-core": "6.22.1",
"babel-jest": "18.0.0",
"babel-plugin-syntax-flow": "6.18.0",
"babel-plugin-transform-flow-strip-types": "6.18.0",
"babel-plugin-transform-flow-strip-types": "6.22.0",
"babel-preset-latest-minimal": "1.1.2",
"flow-bin": "0.36.0",
"flow-bin": "0.38.0",
"flow-copy-source": "1.1.0",
"flow-typed": "2.0.0",
"jest": "17.0.3",
"supertest": "2.0.1",
"supertest-as-promised": "4.0.2",
"webpack": "2.1.0-beta.25"
"jest": "18.1.0",
"rimraf": "2.5.4",
"supertest": "3.0.0"
},
"jest": {
"testEnvironment": "node",
Expand Down
2 changes: 0 additions & 2 deletions packages/tessellate-server/src/MetricsApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import RxRouter from 'koa-router-rx'
import prometheus from 'prom-client'
import prometheusGCStats from 'prometheus-gc-stats'

import type { Epic } from 'koa-router-rx'

export default class MetricsApp {
app: Koa;
router: RxRouter;
Expand Down
49 changes: 42 additions & 7 deletions packages/tessellate-server/src/TessellateServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@

import Koa from 'koa'
import http from 'http'
import compose from 'koa-compose'
import morgan from 'koa-morgan'
import bodyParser from 'koa-bodyparser'
import RxRouter from 'koa-router-rx'
import MetricsApp from './MetricsApp'
import nconf from './nconf'
import error from './error'

import type { Server } from 'http'
import type { Middleware } from 'koa'
import type { Server, IncomingMessage, ServerResponse } from 'http'

type Options = {
name?: string;
};
type Listener = (req: IncomingMessage, res: ServerResponse) => void;

function startServer(listener: () => void, port: number): Promise<Server> {
function startServer(listener: Listener, port: number): Promise<Server> {
return new Promise((resolve, reject) => {
let server = http
.createServer(listener)
Expand All @@ -29,35 +33,66 @@ function stopServer(server: ?Server): Promise<void> {
})
}

function additionalMiddleware(middleware: Array<Middleware>): Middleware {
let composed, length
return (ctx, next) => {
if (!composed || middleware.length !== length) {
composed = compose(middleware)
length = middleware.length
}
return composed(ctx, next)
}
}

export default class TessellateServer {
app: Koa;
metrics: Koa;
router: RxRouter;
appServer: ?Server;
metricsServer: ?Server;
middleware: Array<Middleware>;

constructor(options: Options = {}) {
this.app = new Koa()
this.app.name = options.name
this.router = new RxRouter()
this.metrics = new MetricsApp().app
this.middleware = []

const morganFormat = String(nconf.get('MORGAN_FORMAT'))
const morganThresh = parseInt(nconf.get('MORGAN_THRESHOLD'))
const morganSkip = (req, res) => res.statusCode < morganThresh
const morganSkip = (req: IncomingMessage, res: ServerResponse) => res.statusCode < morganThresh

this.app
.use(morgan(morganFormat, {skip: morganSkip}))
.use(error)
.use(error())
.use(bodyParser({enableTypes: ['json']}))
.use(additionalMiddleware(this.middleware))
.use(this.router.routes())
.use(this.router.allowedMethods())
}

async start(port: number, metricsPort: number = port + 1): Promise<TessellateServer> {
use(middleware: Middleware, defer: boolean = false): TessellateServer {
if (defer) {
this.middleware.push(async (ctx, next) => {
await next()
await middleware(ctx, next)
})
} else {
this.middleware.push(middleware)
}
return this
}

async start(port: number | string, metricsPort?: number | string): Promise<TessellateServer> {
if (!port) throw new Error('No port specified!')

const portNumber = parseInt(port)
const metricsPortNumber = parseInt(metricsPort)

const [appServer, metricsServer] = await Promise.all([
startServer(this.app.callback(), port),
startServer(this.metrics.callback(), metricsPort)
startServer(this.app.callback(), portNumber),
startServer(this.metrics.callback(), metricsPortNumber || portNumber + 1)
])

this.appServer = appServer
Expand Down
15 changes: 7 additions & 8 deletions packages/tessellate-server/src/error.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// @flow

import type { Middleware } from 'koa'

export class Problem extends Error {
title: string;
detail: ?string;
Expand All @@ -17,16 +19,13 @@ export class Problem extends Error {
}
}

export default async function error(ctx: Object, next: () => Promise<any>): Promise<any> {
try {
return await next()
} catch (err) {
console.error(err)
export default function middleware(): Middleware {
return async (ctx, next) => next().catch(err => {
ctx.status = err.status || err.code || 500
ctx.body = {
title: err.title || err.message,
detail: err.detail,
title: err.title || err.name,
detail: err.detail || err.message,
status: ctx.status
}
}
})
}
1 change: 1 addition & 0 deletions packages/tessellate-server/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

export { default as TessellateServer } from './TessellateServer'
export { default as nconf } from './nconf'
export { Problem as Problem } from './error'
52 changes: 44 additions & 8 deletions packages/tessellate-server/src/nconf.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,47 @@ function readYamlOrJsonFile(file: string): Object {

const readConfigFile = () => readYamlOrJsonFile(path.resolve(process.cwd(), 'config'))

export default nconf.use('memory')
.argv()
.env()
.add('config', {type: 'literal', store: readConfigFile()})
.defaults({
MORGAN_FORMAT: 'common',
MORGAN_THRESHOLD: 0
})
class IllegalTypeError extends Error {
constructor(message: string) {
super(message)
}
}

function parseConfigValue(value: mixed): Object {
if (typeof value === 'string') {
try {
return JSON.parse(value)
} catch(e) {
throw new IllegalTypeError('Cannot parse: ' + value)
}
} else if (typeof value === 'object' && value) {
return value
} else {
throw new IllegalTypeError('Not a valid object: ' + JSON.stringify(value))
}
}

nconf
.use('memory')
.argv()
.env()
.add('config', {type: 'literal', store: readConfigFile()})
.defaults({
MORGAN_FORMAT: 'common',
MORGAN_THRESHOLD: 0
})

export default {
set: (key: string, value: any) => nconf.set(key, value),
get: (key: string) => nconf.get(key),
getObject: (key: string) => parseConfigValue(nconf.get(key)),
getString: (key: string) => '' + nconf.get(key),
argv: function(args: Object) {
nconf.argv(args)
return this
},
defaults: function(defaults: Object) {
nconf.defaults(defaults)
return this
}
}
Loading

0 comments on commit 6950e97

Please sign in to comment.