diff --git a/controller/index.js b/controller/index.js index c3267192..6936a982 100644 --- a/controller/index.js +++ b/controller/index.js @@ -1,13 +1,16 @@ var pkg = require('../package'); -function controller( req, res ){ +function controller( req, res, next ){ + + // stats res.json({ name: pkg.name, version: { number: pkg.version } }); + } module.exports = controller; \ No newline at end of file diff --git a/controller/suggest.js b/controller/suggest.js index f2a2230c..c6967186 100644 --- a/controller/suggest.js +++ b/controller/suggest.js @@ -1,32 +1,35 @@ -var logger = require('../src/logger'), - responder = require('../src/responder'), - query = require('../query/suggest'), +var query = require('../query/suggest'), backend = require('../src/backend'); -module.exports = function( req, res, next ){ - - var reply = { - date: new Date().getTime(), - body: [] - }; +function controller( req, res, next ){ + // backend command var cmd = { index: 'pelias', - body: query( req.clean ) // generate query from clean params + body: query( req.clean ) }; - // Proxy request to ES backend & map response to a valid FeatureCollection + // query backend backend().client.suggest( cmd, function( err, data ){ - if( err ){ return responder.error( req, res, next, err ); } - if( data && data.pelias && data.pelias.length ){ + var docs = []; - // map options to reply body - reply.body = data['pelias'][0].options; + // handle backend errors + if( err ){ return next( err ); } + + // map response to a valid FeatureCollection + if( data && Array.isArray( data.pelias ) && data.pelias.length ){ + docs = data['pelias'][0].options || []; } - return responder.cors( req, res, reply ); + // respond + return res.status(200).json({ + date: new Date().getTime(), + body: docs + }); }); -}; \ No newline at end of file +} + +module.exports = controller; \ No newline at end of file diff --git a/express.js b/express.js deleted file mode 100644 index b48e0560..00000000 --- a/express.js +++ /dev/null @@ -1,14 +0,0 @@ - -var express = require('express'), - app = express(); - -// middleware modules -// app.use( require('cookie-parser')() ); - -// enable client-side caching of 60s by default -app.use(function(req, res, next){ - res.header('Cache-Control','public,max-age=60'); - next(); -}); - -module.exports = app; \ No newline at end of file diff --git a/index.js b/index.js index 15543369..ea0ad29a 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,47 @@ -var app = require('./express'); +var app = require('express')(); + +/** ----------------------- middleware ----------------------- **/ + +// generic headers +app.use(function(req, res, next){ + res.header('Charset','utf8'); + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET'); + res.header('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); + res.header('Access-Control-Allow-Credentials', true); + res.header('X-Powered-By', 'pelias'); + next(); +}); + +// jsonp middleware +// override json() to handle jsonp +app.use(function(req, res, next){ + + res._json = res.json; + res.json = function( data ){ + + // jsonp + if( req.query && req.query.callback ){ + res.header('Content-type','application/javascript'); + return res.send( req.query.callback + '('+ JSON.stringify( data ) + ');' ); + } + + // regular json + res.header('Content-type','application/json'); + return res._json( data ); + }; + + next(); +}); + +// enable client-side caching of 60s by default +app.use(function(req, res, next){ + res.header('Cache-Control','public,max-age=60'); + next(); +}); + +/** ----------------------- Routes ----------------------- **/ // api root app.get( '/', require('./controller/index') ); @@ -7,4 +49,10 @@ app.get( '/', require('./controller/index') ); // suggest API app.get( '/suggest', require('./sanitiser/suggest'), require('./controller/suggest') ); +/** ----------------------- error middleware ----------------------- **/ + +// handle application errors +app.use( require('./middleware/404') ); +app.use( require('./middleware/500') ); + app.listen( process.env.PORT || 3100 ); \ No newline at end of file diff --git a/middleware/404.js b/middleware/404.js new file mode 100644 index 00000000..e90ed3c3 --- /dev/null +++ b/middleware/404.js @@ -0,0 +1,8 @@ + +// handle not found errors +function middleware(req, res) { + res.header('Cache-Control','public,max-age=300'); // 5 minute cache + res.status(404).json({ error: 'not found: invalid path' }); +} + +module.exports = middleware; \ No newline at end of file diff --git a/middleware/500.js b/middleware/500.js new file mode 100644 index 00000000..d73c5684 --- /dev/null +++ b/middleware/500.js @@ -0,0 +1,9 @@ + +// handle application errors +function middleware(err, req, res, next) { + res.header('Cache-Control','no-cache'); + if( res.statusCode < 400 ){ res.status(500); } + res.json({ error: err }); +} + +module.exports = middleware; \ No newline at end of file diff --git a/package.json b/package.json index c9fb2fc1..559e7aed 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "start": "node index.js", "test": "npm run unit && npm run ciao", "unit": "node test/unit/run.js | tap-spec", - "ciao": "node node_modules/ciao/bin/ciao -c test/ciao.json test/ciao" + "ciao": "node node_modules/ciao/bin/ciao -c test/ciao.json test/ciao", + "docs": "cd test/ciao; node ../../node_modules/ciao/bin/ciao -c ../ciao.json . -d ../../docs" }, "repository": { "type": "git", diff --git a/sanitiser/suggest.js b/sanitiser/suggest.js index ea5296db..d7dfa60a 100644 --- a/sanitiser/suggest.js +++ b/sanitiser/suggest.js @@ -14,7 +14,7 @@ function sanitize( params, cb ){ // input text if('string' !== typeof params.input || !params.input.length){ - return cb( 'invalid input text length, must be >0' ); + return cb( 'invalid param \'input\': text length, must be >0' ); } clean.input = params.input; @@ -33,7 +33,7 @@ function sanitize( params, cb ){ }); for( var x=0; x 90 ){ - return cb( 'invalid lat, must be >0 and <90' ); + return cb( 'invalid param \'lat\': must be >0 and <90' ); } clean.lat = lat; // lon var lon = parseFloat( params.lon, 10 ); if( isNaN( lon ) || lon < -180 || lon > 180 ){ - return cb( 'invalid lon, must be >-180 and <180' ); + return cb( 'invalid param \'lon\': must be >-180 and <180' ); } clean.lon = lon; @@ -68,9 +68,13 @@ function sanitize( params, cb ){ } +// middleware module.exports = function( req, res, next ){ sanitize( req.query, function( err, clean ){ - if( err ){ next( err ); } + if( err ){ + res.status(400); // 400 Bad Request + return next(err); + } req.clean = clean; next(); }); diff --git a/src/responder.js b/src/responder.js deleted file mode 100644 index 80f8d880..00000000 --- a/src/responder.js +++ /dev/null @@ -1,33 +0,0 @@ - -// send a reply that is capable of JSON, CORS and JSONP -function cors( req, res, obj ){ - res.header('Charset','utf8'); - res.header('Cache-Control','public,max-age=60'); - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET'); - res.header('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); - res.header('Access-Control-Allow-Credentials', true); - res.header('X-Powered-By', 'pelias'); - - // jsonp - if( req.query && req.query.callback ){ - res.header('Content-type','application/javascript'); - return res.send( req.query.callback + '('+ JSON.stringify( obj ) + ');' ); - } - - // regular json - res.header('Content-type','application/json'); - return res.json( obj ); -} - -// send an error -function error( req, res, next, err ){ - console.error( 'application error:', err ); - // mask error from user (contains paths) - return cors( req, res, { error: 'application error' } ); -} - -module.exports = { - cors: cors, - error: error -}; \ No newline at end of file diff --git a/test/ciao/404.coffee b/test/ciao/404.coffee new file mode 100644 index 00000000..764bc8d5 --- /dev/null +++ b/test/ciao/404.coffee @@ -0,0 +1,17 @@ + +#> invalid path +path: '/notexist' + +#? not found +response.statusCode.should.equal 404 + +#? content-type header correctly set +response.should.have.header 'Content-Type','application/json; charset=utf-8' + +#? cache-control header correctly set +response.should.have.header 'Cache-Control','public,max-age=300' + +#? should respond in json with server info +should.exist json +should.exist json.error +json.error.should.equal 'not found: invalid path' \ No newline at end of file diff --git a/test/ciao/cors.coffee b/test/ciao/cors.coffee new file mode 100644 index 00000000..bd97c5de --- /dev/null +++ b/test/ciao/cors.coffee @@ -0,0 +1,9 @@ + +#> cross-origin resource sharing +path: '/' + +#? access control headers correctly set +response.should.have.header 'Access-Control-Allow-Origin','*' +response.should.have.header 'Access-Control-Allow-Methods','GET' +response.should.have.header 'Access-Control-Allow-Headers','X-Requested-With,content-type' +response.should.have.header 'Access-Control-Allow-Credentials','true' \ No newline at end of file diff --git a/test/ciao/index.coffee b/test/ciao/index.coffee index 960f6551..2a9dba88 100644 --- a/test/ciao/index.coffee +++ b/test/ciao/index.coffee @@ -8,9 +8,15 @@ response.statusCode.should.equal 200 #? content-type header correctly set response.should.have.header 'Content-Type','application/json; charset=utf-8' +#? charset header correctly set +response.should.have.header 'Charset','utf8' + #? cache-control header correctly set response.should.have.header 'Cache-Control','public,max-age=60' +#? vanity header correctly set +response.should.have.header 'X-Powered-By','pelias' + #? should respond in json with server info should.exist json should.exist json.name diff --git a/test/ciao/jsonp.coffee b/test/ciao/jsonp.coffee new file mode 100644 index 00000000..d83f0eaa --- /dev/null +++ b/test/ciao/jsonp.coffee @@ -0,0 +1,10 @@ + +#> jsonp +path: '/?callback=test' + +#? content-type header correctly set +response.should.have.header 'Content-Type','application/javascript; charset=utf-8' + +#? should respond with jsonp +should.exist response.body +response.body.substr(0,5).should.equal 'test('; \ No newline at end of file diff --git a/test/ciao/suggest/success.coffee b/test/ciao/suggest/success.coffee new file mode 100644 index 00000000..b5a58c44 --- /dev/null +++ b/test/ciao/suggest/success.coffee @@ -0,0 +1,15 @@ + +#> valid suggest query +path: '/suggest?input=a&lat=0&lon=0' + +#? 200 ok +response.statusCode.should.equal 200 + +#? valid response +now = new Date().getTime() +should.exist json +should.not.exist json.error +should.exist json.date +json.date.should.be.within now-1000, now+1000 +should.exist json.body +json.body.should.be.instanceof Array \ No newline at end of file