mirror of https://github.com/pelias/api.git
Peter Johnson @insertcoffee
10 years ago
23 changed files with 713 additions and 17 deletions
@ -1,13 +1,16 @@ |
|||||||
|
|
||||||
var pkg = require('../package'); |
var pkg = require('../package'); |
||||||
|
|
||||||
function controller( req, res ){ |
function controller( req, res, next ){ |
||||||
|
|
||||||
|
// stats
|
||||||
res.json({ |
res.json({ |
||||||
name: pkg.name, |
name: pkg.name, |
||||||
version: { |
version: { |
||||||
number: pkg.version |
number: pkg.version |
||||||
} |
} |
||||||
}); |
}); |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
module.exports = controller; |
module.exports = controller; |
@ -0,0 +1,35 @@ |
|||||||
|
|
||||||
|
var query = require('../query/suggest'), |
||||||
|
backend = require('../src/backend'); |
||||||
|
|
||||||
|
function controller( req, res, next ){ |
||||||
|
|
||||||
|
// backend command
|
||||||
|
var cmd = { |
||||||
|
index: 'pelias', |
||||||
|
body: query( req.clean ) |
||||||
|
}; |
||||||
|
|
||||||
|
// query backend
|
||||||
|
backend().client.suggest( cmd, function( err, data ){ |
||||||
|
|
||||||
|
var docs = []; |
||||||
|
|
||||||
|
// 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 || []; |
||||||
|
} |
||||||
|
|
||||||
|
// respond
|
||||||
|
return res.status(200).json({ |
||||||
|
date: new Date().getTime(), |
||||||
|
body: docs |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
module.exports = controller; |
@ -0,0 +1,65 @@ |
|||||||
|
# invalid path |
||||||
|
|
||||||
|
*Generated: Fri Sep 12 2014 19:14:09 GMT+0100 (BST)* |
||||||
|
## Request |
||||||
|
```javascript |
||||||
|
{ |
||||||
|
"protocol": "http:", |
||||||
|
"host": "localhost", |
||||||
|
"method": "GET", |
||||||
|
"port": 3100, |
||||||
|
"path": "/notexist", |
||||||
|
"headers": { |
||||||
|
"User-Agent": "Ciao/Client 1.0" |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Response |
||||||
|
```javascript |
||||||
|
Status: 404 |
||||||
|
{ |
||||||
|
"x-powered-by": "pelias", |
||||||
|
"charset": "utf8", |
||||||
|
"access-control-allow-origin": "*", |
||||||
|
"access-control-allow-methods": "GET", |
||||||
|
"access-control-allow-headers": "X-Requested-With,content-type", |
||||||
|
"access-control-allow-credentials": "true", |
||||||
|
"cache-control": "public,max-age=300", |
||||||
|
"content-type": "application/json; charset=utf-8", |
||||||
|
"content-length": "35", |
||||||
|
"etag": "W/\"23-dfdfa185\"", |
||||||
|
"date": "Fri, 12 Sep 2014 18:14:09 GMT", |
||||||
|
"connection": "close" |
||||||
|
} |
||||||
|
``` |
||||||
|
```javascript |
||||||
|
{ |
||||||
|
"error": "not found: invalid path" |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Tests |
||||||
|
|
||||||
|
### ✓ content-type header correctly set |
||||||
|
```javascript |
||||||
|
response.should.have.header 'Content-Type','application/json; charset=utf-8' |
||||||
|
``` |
||||||
|
|
||||||
|
### ✓ should respond in json with server info |
||||||
|
```javascript |
||||||
|
should.exist json |
||||||
|
should.exist json.error |
||||||
|
json.error.should.equal 'not found: invalid path' |
||||||
|
``` |
||||||
|
|
||||||
|
### ✓ cache-control header correctly set |
||||||
|
```javascript |
||||||
|
response.should.have.header 'Cache-Control','public,max-age=300' |
||||||
|
``` |
||||||
|
|
||||||
|
### ✓ not found |
||||||
|
```javascript |
||||||
|
response.statusCode.should.equal 404 |
||||||
|
``` |
||||||
|
|
@ -0,0 +1,54 @@ |
|||||||
|
# cross-origin resource sharing |
||||||
|
|
||||||
|
*Generated: Fri Sep 12 2014 19:14:09 GMT+0100 (BST)* |
||||||
|
## Request |
||||||
|
```javascript |
||||||
|
{ |
||||||
|
"protocol": "http:", |
||||||
|
"host": "localhost", |
||||||
|
"method": "GET", |
||||||
|
"port": 3100, |
||||||
|
"path": "/", |
||||||
|
"headers": { |
||||||
|
"User-Agent": "Ciao/Client 1.0" |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Response |
||||||
|
```javascript |
||||||
|
Status: 200 |
||||||
|
{ |
||||||
|
"x-powered-by": "pelias", |
||||||
|
"charset": "utf8", |
||||||
|
"access-control-allow-origin": "*", |
||||||
|
"access-control-allow-methods": "GET", |
||||||
|
"access-control-allow-headers": "X-Requested-With,content-type", |
||||||
|
"access-control-allow-credentials": "true", |
||||||
|
"cache-control": "public,max-age=60", |
||||||
|
"content-type": "application/json; charset=utf-8", |
||||||
|
"content-length": "50", |
||||||
|
"etag": "W/\"32-85536434\"", |
||||||
|
"date": "Fri, 12 Sep 2014 18:14:09 GMT", |
||||||
|
"connection": "close" |
||||||
|
} |
||||||
|
``` |
||||||
|
```javascript |
||||||
|
{ |
||||||
|
"name": "pelias-api", |
||||||
|
"version": { |
||||||
|
"number": "0.0.0" |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Tests |
||||||
|
|
||||||
|
### ✓ access control headers correctly set |
||||||
|
```javascript |
||||||
|
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' |
||||||
|
``` |
||||||
|
|
@ -0,0 +1,78 @@ |
|||||||
|
# api root |
||||||
|
|
||||||
|
*Generated: Fri Sep 12 2014 19:14:09 GMT+0100 (BST)* |
||||||
|
## Request |
||||||
|
```javascript |
||||||
|
{ |
||||||
|
"protocol": "http:", |
||||||
|
"host": "localhost", |
||||||
|
"method": "GET", |
||||||
|
"port": 3100, |
||||||
|
"path": "/", |
||||||
|
"headers": { |
||||||
|
"User-Agent": "Ciao/Client 1.0" |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Response |
||||||
|
```javascript |
||||||
|
Status: 200 |
||||||
|
{ |
||||||
|
"x-powered-by": "pelias", |
||||||
|
"charset": "utf8", |
||||||
|
"access-control-allow-origin": "*", |
||||||
|
"access-control-allow-methods": "GET", |
||||||
|
"access-control-allow-headers": "X-Requested-With,content-type", |
||||||
|
"access-control-allow-credentials": "true", |
||||||
|
"cache-control": "public,max-age=60", |
||||||
|
"content-type": "application/json; charset=utf-8", |
||||||
|
"content-length": "50", |
||||||
|
"etag": "W/\"32-85536434\"", |
||||||
|
"date": "Fri, 12 Sep 2014 18:14:09 GMT", |
||||||
|
"connection": "close" |
||||||
|
} |
||||||
|
``` |
||||||
|
```javascript |
||||||
|
{ |
||||||
|
"name": "pelias-api", |
||||||
|
"version": { |
||||||
|
"number": "0.0.0" |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Tests |
||||||
|
|
||||||
|
### ✓ endpoint available |
||||||
|
```javascript |
||||||
|
response.statusCode.should.equal 200 |
||||||
|
``` |
||||||
|
|
||||||
|
### ✓ vanity header correctly set |
||||||
|
```javascript |
||||||
|
response.should.have.header 'X-Powered-By','pelias' |
||||||
|
``` |
||||||
|
|
||||||
|
### ✓ cache-control header correctly set |
||||||
|
```javascript |
||||||
|
response.should.have.header 'Cache-Control','public,max-age=60' |
||||||
|
``` |
||||||
|
|
||||||
|
### ✓ should respond in json with server info |
||||||
|
```javascript |
||||||
|
should.exist json |
||||||
|
should.exist json.name |
||||||
|
should.exist json.version |
||||||
|
``` |
||||||
|
|
||||||
|
### ✓ content-type header correctly set |
||||||
|
```javascript |
||||||
|
response.should.have.header 'Content-Type','application/json; charset=utf-8' |
||||||
|
``` |
||||||
|
|
||||||
|
### ✓ charset header correctly set |
||||||
|
```javascript |
||||||
|
response.should.have.header 'Charset','utf8' |
||||||
|
``` |
||||||
|
|
@ -0,0 +1,52 @@ |
|||||||
|
# jsonp |
||||||
|
|
||||||
|
*Generated: Fri Sep 12 2014 19:14:09 GMT+0100 (BST)* |
||||||
|
## Request |
||||||
|
```javascript |
||||||
|
{ |
||||||
|
"protocol": "http:", |
||||||
|
"host": "localhost", |
||||||
|
"method": "GET", |
||||||
|
"port": 3100, |
||||||
|
"path": "/?callback=test", |
||||||
|
"headers": { |
||||||
|
"User-Agent": "Ciao/Client 1.0" |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Response |
||||||
|
```javascript |
||||||
|
Status: 200 |
||||||
|
{ |
||||||
|
"x-powered-by": "pelias", |
||||||
|
"charset": "utf8", |
||||||
|
"access-control-allow-origin": "*", |
||||||
|
"access-control-allow-methods": "GET", |
||||||
|
"access-control-allow-headers": "X-Requested-With,content-type", |
||||||
|
"access-control-allow-credentials": "true", |
||||||
|
"cache-control": "public,max-age=60", |
||||||
|
"content-type": "application/javascript; charset=utf-8", |
||||||
|
"content-length": "57", |
||||||
|
"etag": "W/\"39-b8a2aba1\"", |
||||||
|
"date": "Fri, 12 Sep 2014 18:14:09 GMT", |
||||||
|
"connection": "close" |
||||||
|
} |
||||||
|
``` |
||||||
|
```html |
||||||
|
test({"name":"pelias-api","version":{"number":"0.0.0"}}); |
||||||
|
``` |
||||||
|
|
||||||
|
## Tests |
||||||
|
|
||||||
|
### ✓ content-type header correctly set |
||||||
|
```javascript |
||||||
|
response.should.have.header 'Content-Type','application/javascript; charset=utf-8' |
||||||
|
``` |
||||||
|
|
||||||
|
### ✓ should respond with jsonp |
||||||
|
```javascript |
||||||
|
should.exist response.body |
||||||
|
response.body.substr(0,5).should.equal 'test('; |
||||||
|
``` |
||||||
|
|
@ -1,11 +0,0 @@ |
|||||||
|
|
||||||
var express = require('express'); |
|
||||||
var app = express(); |
|
||||||
|
|
||||||
// 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; |
|
@ -1,7 +1,58 @@ |
|||||||
|
|
||||||
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
|
// api root
|
||||||
app.get( '/', require('./controller/index') ); |
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 ); |
app.listen( process.env.PORT || 3100 ); |
@ -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; |
@ -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; |
@ -0,0 +1,12 @@ |
|||||||
|
|
||||||
|
// querable indeces
|
||||||
|
|
||||||
|
module.exports = [ |
||||||
|
'geoname', |
||||||
|
'osmnode', |
||||||
|
'osmway', |
||||||
|
'admin0', |
||||||
|
'admin1', |
||||||
|
'admin2', |
||||||
|
'neighborhood' |
||||||
|
]; |
@ -0,0 +1,31 @@ |
|||||||
|
|
||||||
|
var logger = require('../src/logger'); |
||||||
|
|
||||||
|
// Build pelias suggest query
|
||||||
|
function generate( params ){ |
||||||
|
|
||||||
|
var cmd = { |
||||||
|
'pelias' : { |
||||||
|
'text' : params.input, |
||||||
|
'completion' : { |
||||||
|
'size' : params.size, |
||||||
|
'field' : 'suggest', |
||||||
|
'context': { |
||||||
|
'dataset': params.layers, |
||||||
|
'location': { |
||||||
|
'value': [ params.lon, params.lat ], |
||||||
|
|
||||||
|
// // commented out until they fix the precision bug in ES 1.3.3
|
||||||
|
'precision': 2 // params.zoom > 9 ? 3 : (params.zoom > 7 ? 2 : 1)
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
logger.log( 'cmd', JSON.stringify( cmd, null, 2 ) ); |
||||||
|
return cmd; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
module.exports = generate; |
@ -0,0 +1,81 @@ |
|||||||
|
|
||||||
|
var logger = require('../src/logger'), |
||||||
|
indeces = require('../query/indeces'); |
||||||
|
|
||||||
|
// validate inputs, convert types and apply defaults
|
||||||
|
function sanitize( params, cb ){ |
||||||
|
|
||||||
|
var clean = {}; |
||||||
|
|
||||||
|
// ensure the input params are a valid object
|
||||||
|
if( Object.prototype.toString.call( params ) !== '[object Object]' ){ |
||||||
|
params = {}; |
||||||
|
} |
||||||
|
|
||||||
|
// input text
|
||||||
|
if('string' !== typeof params.input || !params.input.length){ |
||||||
|
return cb( 'invalid param \'input\': text length, must be >0' ); |
||||||
|
} |
||||||
|
clean.input = params.input; |
||||||
|
|
||||||
|
// total results
|
||||||
|
var size = parseInt( params.size, 10 ); |
||||||
|
if( !isNaN( size ) ){ |
||||||
|
clean.size = Math.min( size, 40 ); // max
|
||||||
|
} else { |
||||||
|
clean.size = 10; // default
|
||||||
|
} |
||||||
|
|
||||||
|
// which layers to query
|
||||||
|
if('string' === typeof params.layers && params.layers.length){ |
||||||
|
var layers = params.layers.split(',').map( function( layer ){ |
||||||
|
return layer.toLowerCase(); // lowercase inputs
|
||||||
|
}); |
||||||
|
for( var x=0; x<layers.length; x++ ){ |
||||||
|
if( -1 === indeces.indexOf( layers[x] ) ){ |
||||||
|
return cb( 'invalid param \'layer\': must be one or more of ' + layers.join(',') ); |
||||||
|
} |
||||||
|
} |
||||||
|
clean.layers = layers; |
||||||
|
} |
||||||
|
else { |
||||||
|
clean.layers = indeces; // default (all layers)
|
||||||
|
} |
||||||
|
|
||||||
|
// lat
|
||||||
|
var lat = parseFloat( params.lat, 10 ); |
||||||
|
if( isNaN( lat ) || lat < 0 || lat > 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 param \'lon\': must be >-180 and <180' ); |
||||||
|
} |
||||||
|
clean.lon = lon; |
||||||
|
|
||||||
|
// zoom level
|
||||||
|
var zoom = parseInt( params.zoom, 10 ); |
||||||
|
if( !isNaN( zoom ) ){ |
||||||
|
clean.zoom = Math.min( zoom, 18 ); // max
|
||||||
|
} else { |
||||||
|
clean.zoom = 10; // default
|
||||||
|
} |
||||||
|
|
||||||
|
return cb( undefined, clean ); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// middleware
|
||||||
|
module.exports = function( req, res, next ){ |
||||||
|
sanitize( req.query, function( err, clean ){ |
||||||
|
if( err ){ |
||||||
|
res.status(400); // 400 Bad Request
|
||||||
|
return next(err); |
||||||
|
} |
||||||
|
req.clean = clean; |
||||||
|
next(); |
||||||
|
}); |
||||||
|
}; |
@ -0,0 +1,21 @@ |
|||||||
|
|
||||||
|
var Backend = require('geopipes-elasticsearch-backend'), |
||||||
|
backends = {}, |
||||||
|
client; |
||||||
|
|
||||||
|
// set env specific client
|
||||||
|
if( process.env.NODE_ENV === 'test' ){ |
||||||
|
client = require('./pelias-mockclient'); |
||||||
|
} else { |
||||||
|
client = require('pelias-esclient')(); |
||||||
|
} |
||||||
|
|
||||||
|
function getBackend( index, type ){ |
||||||
|
var key = ( index + ':' + type ); |
||||||
|
if( !backends[key] ){ |
||||||
|
backends[key] = new Backend( client, index, type ); |
||||||
|
} |
||||||
|
return backends[key]; |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = getBackend; |
@ -0,0 +1,5 @@ |
|||||||
|
module.exports = { |
||||||
|
log: console.log.bind( console ), |
||||||
|
warn: console.warn.bind( console ), |
||||||
|
error: console.error.bind( console ) |
||||||
|
}; |
@ -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' |
@ -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' |
@ -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('; |
@ -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 |
Loading…
Reference in new issue