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'); |
||||
|
||||
function controller( req, res ){ |
||||
function controller( req, res, next ){ |
||||
|
||||
// stats
|
||||
res.json({ |
||||
name: pkg.name, |
||||
version: { |
||||
number: pkg.version |
||||
} |
||||
}); |
||||
|
||||
} |
||||
|
||||
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
|
||||
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 ); |
@ -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