Browse Source

Merge pull request #2 from pelias/dev

merge development branch
pull/4/head
Peter Johnson @insertcoffee 10 years ago
parent
commit
03e74ae541
  1. 1
      .gitignore
  2. 5
      controller/index.js
  3. 35
      controller/suggest.js
  4. 65
      docs/404.md
  5. 54
      docs/cors.md
  6. 78
      docs/index.md
  7. 52
      docs/jsonp.md
  8. 141
      docs/suggest/success.md
  9. 11
      express.js
  10. 53
      index.js
  11. 8
      middleware/404.js
  12. 9
      middleware/500.js
  13. 7
      package.json
  14. 12
      query/indeces.js
  15. 31
      query/suggest.js
  16. 81
      sanitiser/suggest.js
  17. 21
      src/backend.js
  18. 5
      src/logger.js
  19. 17
      test/ciao/404.coffee
  20. 9
      test/ciao/cors.coffee
  21. 6
      test/ciao/index.coffee
  22. 10
      test/ciao/jsonp.coffee
  23. 15
      test/ciao/suggest/success.coffee

1
.gitignore vendored

@ -1 +1,2 @@
node_modules
*.log

5
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;

35
controller/suggest.js

@ -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;

65
docs/404.md

@ -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
```

54
docs/cors.md

@ -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'
```

78
docs/index.md

@ -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'
```

52
docs/jsonp.md

@ -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(';
```

141
docs/suggest/success.md

@ -0,0 +1,141 @@
# valid suggest query
*Generated: Fri Sep 12 2014 19:14:09 GMT+0100 (BST)*
## Request
```javascript
{
"protocol": "http:",
"host": "localhost",
"method": "GET",
"port": 3100,
"path": "/suggest?input=a&lat=0&lon=0",
"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": "1248",
"etag": "W/\"jtfnMCXDw5frK6L5eD1thg==\"",
"date": "Fri, 12 Sep 2014 18:14:09 GMT",
"connection": "close"
}
```
```javascript
{
"date": 1410545649156,
"body": [
{
"text": "ACRELÂNDIA, Brazil",
"score": 1,
"payload": {
"id": "admin2/708:adm2:br:bra:acrel__ndia",
"geo": "-66.908143,-9.954353"
}
},
{
"text": "ALTA FLORESTA, Brazil",
"score": 1,
"payload": {
"id": "admin2/2986:adm2:br:bra:alta_floresta",
"geo": "-56.404593,-10.042071"
}
},
{
"text": "ALTO ALEGRE, Brazil",
"score": 1,
"payload": {
"id": "admin2/4611:adm2:br:bra:alto_alegre",
"geo": "-62.627879,3.103540"
}
},
{
"text": "ALTO PARAÍSO, Brazil",
"score": 1,
"payload": {
"id": "admin2/4584:adm2:br:bra:alto_para__so",
"geo": "-63.418743,-9.697774"
}
},
{
"text": "ALVARÃES, Brazil",
"score": 1,
"payload": {
"id": "admin2/832:adm2:br:bra:alvar__es",
"geo": "-65.296384,-3.674615"
}
},
{
"text": "AMAJARI, Brazil",
"score": 1,
"payload": {
"id": "admin2/4610:adm2:br:bra:amajari",
"geo": "-62.710104,3.724864"
}
},
{
"text": "AMAZONAS, Brazil",
"score": 1,
"payload": {
"id": "admin1/3232:adm1:br:bra:amazonas",
"geo": "-64.949558,-3.785708"
}
},
{
"text": "ANAMÃ, Brazil",
"score": 1,
"payload": {
"id": "admin2/834:adm2:br:bra:anam__",
"geo": "-61.683670,-3.473836"
}
},
{
"text": "ANORI, Brazil",
"score": 1,
"payload": {
"id": "admin2/835:adm2:br:bra:anori",
"geo": "-62.182138,-4.154809"
}
},
{
"text": "APIACÁS, Brazil",
"score": 1,
"payload": {
"id": "admin2/2992:adm2:br:bra:apiac__s",
"geo": "-57.803447,-8.583036"
}
}
]
}
```
## Tests
### ✓ 200 ok
```javascript
response.statusCode.should.equal 200
```
### ✓ valid response
```javascript
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
```

11
express.js

@ -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;

53
index.js

@ -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 );

8
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;

9
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;

7
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",
@ -30,7 +31,9 @@
"elasticsearch": ">=1.2.1"
},
"dependencies": {
"express": "^4.8.8"
"express": "^4.8.8",
"geopipes-elasticsearch-backend": "0.0.7",
"pelias-esclient": "0.0.25"
},
"devDependencies": {
"ciao": "^0.3.4",

12
query/indeces.js

@ -0,0 +1,12 @@
// querable indeces
module.exports = [
'geoname',
'osmnode',
'osmway',
'admin0',
'admin1',
'admin2',
'neighborhood'
];

31
query/suggest.js

@ -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;

81
sanitiser/suggest.js

@ -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();
});
};

21
src/backend.js

@ -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;

5
src/logger.js

@ -0,0 +1,5 @@
module.exports = {
log: console.log.bind( console ),
warn: console.warn.bind( console ),
error: console.error.bind( console )
};

17
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'

9
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'

6
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

10
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(';

15
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
Loading…
Cancel
Save