Browse Source

Merge branch 'master' of github.com:pelias/api into precision

pull/42/head
Peter Johnson 10 years ago
parent
commit
a52bec60b7
  1. 7
      app.js
  2. 41
      controller/doc.js
  3. 17
      controller/search.js
  4. 101
      controller/suggest.js
  5. 40
      controller/suggest_nearby.js
  6. 14
      docs/404.md
  7. 4
      docs/cors.md
  8. 97
      docs/doc/msuccess.md
  9. 80
      docs/doc/success.md
  10. 36
      docs/index.md
  11. 14
      docs/jsonp.md
  12. 31
      docs/reverse/success.md
  13. 183
      docs/search/success.md
  14. 164
      docs/suggest/success.md
  15. 223
      docs/suggest/success_nearby.md
  16. 3
      package.json
  17. 2
      query/reverse.js
  18. 4
      query/suggest.js
  19. 73
      sanitiser/_id.js
  20. 25
      sanitiser/_layers.js
  21. 6
      sanitiser/_size.js
  22. 23
      sanitiser/doc.js
  23. 10
      sanitiser/reverse.js
  24. 43
      service/mget.js
  25. 30
      service/search.js
  26. 28
      service/suggest.js
  27. 16
      test/ciao/doc/msuccess.coffee
  28. 16
      test/ciao/doc/success.coffee
  29. 16
      test/ciao/suggest/success_nearby.coffee
  30. 92
      test/unit/controller/doc.js
  31. 13
      test/unit/controller/suggest.js
  32. 91
      test/unit/controller/suggest_nearby.js
  33. 37
      test/unit/mock/backend.js
  34. 10
      test/unit/query/reverse.js
  35. 9
      test/unit/query/suggest.js
  36. 8
      test/unit/run.js
  37. 159
      test/unit/sanitiser/doc.js
  38. 97
      test/unit/sanitiser/suggest.js
  39. 82
      test/unit/service/mget.js
  40. 82
      test/unit/service/search.js
  41. 79
      test/unit/service/suggest.js

7
app.js

@ -11,6 +11,7 @@ app.use( require('./middleware/jsonp') );
/** ----------------------- sanitisers ----------------------- **/
var sanitisers = {};
sanitisers.doc = require('./sanitiser/doc');
sanitisers.suggest = require('./sanitiser/suggest');
sanitisers.search = sanitisers.suggest;
sanitisers.reverse = require('./sanitiser/reverse');
@ -19,7 +20,9 @@ sanitisers.reverse = require('./sanitiser/reverse');
var controllers = {};
controllers.index = require('./controller/index');
controllers.doc = require('./controller/doc');
controllers.suggest = require('./controller/suggest');
controllers.suggest_nearby = require('./controller/suggest_nearby');
controllers.search = require('./controller/search');
/** ----------------------- routes ----------------------- **/
@ -27,8 +30,12 @@ controllers.search = require('./controller/search');
// api root
app.get( '/', controllers.index() );
// doc API
app.get( '/doc', sanitisers.doc.middleware, controllers.doc() );
// suggest API
app.get( '/suggest', sanitisers.suggest.middleware, controllers.suggest() );
app.get( '/suggest/nearby', sanitisers.suggest.middleware, controllers.suggest_nearby() );
// search API
app.get( '/search', sanitisers.search.middleware, controllers.search() );

41
controller/doc.js

@ -0,0 +1,41 @@
var service = { mget: require('../service/mget') };
var geojsonify = require('../helper/geojsonify').search;
function setup( backend ){
// allow overriding of dependencies
backend = backend || require('../src/backend');
function controller( req, res, next ){
var query = req.clean.ids.map( function(id) {
return {
_index: 'pelias',
_type: id.type,
_id: id.id
};
});
service.mget( backend, query, function( err, docs ){
// error handler
if( err ){ return next( err ); }
// convert docs to geojson
var geojson = geojsonify( docs );
// response envelope
geojson.date = new Date().getTime();
// respond
return res.status(200).json( geojson );
});
}
return controller;
}
module.exports = setup;

17
controller/search.js

@ -1,4 +1,5 @@
var service = { search: require('../service/search') };
var geojsonify = require('../helper/geojsonify').search;
function setup( backend, query ){
@ -15,20 +16,16 @@ function setup( backend, query ){
body: query( req.clean )
};
// query backend
backend().client.search( cmd, function( err, data ){
if (req.clean.layers) {
cmd.type = req.clean.layers;
}
var docs = [];
// query backend
service.search( backend, cmd, function( err, docs ){
// handle backend errors
// error handler
if( err ){ return next( err ); }
if( data && data.hits && data.hits.total && Array.isArray(data.hits.hits)){
docs = data.hits.hits.map( function( hit ){
return hit._source;
});
}
// convert docs to geojson
var geojson = geojsonify( docs );

101
controller/suggest.js

@ -1,5 +1,7 @@
var service = { suggest: require('../service/suggest') };
var geojsonify = require('../helper/geojsonify').suggest;
var async = require('async');
function setup( backend, query ){
@ -9,35 +11,110 @@ function setup( backend, query ){
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 SIZE = req.clean.size || 10;
var docs = [];
var query_backend = function(cmd, callback) {
// query backend
service.suggest( backend, cmd, function( err, docs ){
// error handler
if( err ){ return next( err ); }
// handle backend errors
if( err ){ return next( err ); }
callback(null, docs);
});
};
// map response to a valid FeatureCollection
if( data && Array.isArray( data.pelias ) && data.pelias.length ){
docs = data['pelias'][0].options || [];
}
var dedup = function(combined) {
var unique_ids = [];
return combined.filter(function(item, pos) {
if (unique_ids.indexOf(item.payload.id) == -1) {
unique_ids.push(item.payload.id);
return true;
}
return false;
});
};
var respond = function(data) {
// convert docs to geojson
var geojson = geojsonify( docs );
var geojson = geojsonify( data );
// response envelope
geojson.date = new Date().getTime();
// respond
return res.status(200).json( geojson );
});
};
if (req.clean.input) {
var async_query;
// admin only
req.admin = {};
for (k in req.clean) { req.admin[k] = req.clean[k] }
req.admin.layers = ['admin0','admin1','admin2'];
if (req.clean.input.length < 4 && isNaN(parseInt(req.clean.input, 10))) {
async_query = {
admin_3p: function(callback){
cmd.body = query( req.admin, 3 );
query_backend(cmd, callback);
},
admin_1p: function(callback){
cmd.body = query( req.admin, 1 );
query_backend(cmd, callback);
},
all_3p: function(callback) {
cmd.body = query( req.clean, 3 );
query_backend(cmd, callback);
}
}
} else {
async_query = {
all_5p: function(callback){
cmd.body = query( req.clean, 5);
query_backend(cmd, callback);
},
all_3p: function(callback){
cmd.body = query( req.clean, 3);
query_backend(cmd, callback);
},
all_1p: function(callback){
cmd.body = query( req.clean, 1 );
query_backend(cmd, callback);
},
admin_1p: function(callback){
cmd.body = query( req.admin );
query_backend(cmd, callback);
}
}
}
async.parallel(async_query, function(err, results) {
// results is equal to: {a: docs, b: docs, c: docs}
var splice_length = parseInt((SIZE / Object.keys(results).length), 10);
var results_keys = Object.keys(async_query);
var combined = [];
results_keys.forEach(function(key){
combined = combined.concat(results[key].splice(0,splice_length));
});
combined = dedup(combined);
respond(combined);
});
} else {
query_backend(cmd, function(err, results) {
respond(results);
});
}
}
return controller;

40
controller/suggest_nearby.js

@ -0,0 +1,40 @@
var service = { suggest: require('../service/suggest') };
var geojsonify = require('../helper/geojsonify').suggest;
function setup( backend, query ){
// allow overriding of dependencies
backend = backend || require('../src/backend');
query = query || require('../query/suggest');
function controller( req, res, next ){
// backend command
var cmd = {
index: 'pelias',
body: query( req.clean )
};
// query backend
service.suggest( backend, cmd, function( err, docs ){
// error handler
if( err ){ return next( err ); }
// convert docs to geojson
var geojson = geojsonify( docs );
// response envelope
geojson.date = new Date().getTime();
// respond
return res.status(200).json( geojson );
});
}
return controller;
}
module.exports = setup;

14
docs/404.md

@ -1,6 +1,6 @@
# invalid path
*Generated: Thu Sep 25 2014 19:25:20 GMT+0100 (BST)*
*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)*
## Request
```javascript
{
@ -27,7 +27,7 @@ Status: 404
"content-type": "application/json; charset=utf-8",
"content-length": "35",
"etag": "W/\"23-dfdfa185\"",
"date": "Thu, 25 Sep 2014 18:25:20 GMT",
"date": "Thu, 06 Nov 2014 16:44:19 GMT",
"connection": "close"
}
```
@ -44,6 +44,11 @@ Status: 404
response.statusCode.should.equal 404
```
### âś“ content-type header correctly set
```javascript
response.should.have.header 'Content-Type','application/json; charset=utf-8'
```
### âś“ cache-control header correctly set
```javascript
response.should.have.header 'Cache-Control','public,max-age=300'
@ -56,8 +61,3 @@ should.exist json.error
json.error.should.equal 'not found: invalid path'
```
### âś“ content-type header correctly set
```javascript
response.should.have.header 'Content-Type','application/json; charset=utf-8'
```

4
docs/cors.md

@ -1,6 +1,6 @@
# cross-origin resource sharing
*Generated: Thu Sep 25 2014 19:25:20 GMT+0100 (BST)*
*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)*
## Request
```javascript
{
@ -27,7 +27,7 @@ Status: 200
"content-type": "application/json; charset=utf-8",
"content-length": "50",
"etag": "W/\"32-85536434\"",
"date": "Thu, 25 Sep 2014 18:25:20 GMT",
"date": "Thu, 06 Nov 2014 16:44:19 GMT",
"connection": "close"
}
```

97
docs/doc/msuccess.md

@ -0,0 +1,97 @@
# valid doc query
*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)*
## Request
```javascript
{
"protocol": "http:",
"host": "localhost",
"method": "GET",
"port": 3100,
"path": "/doc?id=geoname:4221195&id=geoname:4193595"
}
```
## Response
```javascript
Status: 200
{
"x-powered-by": "mapzen",
"charset": "utf8",
"cache-control": "public,max-age=60",
"server": "Pelias/0.0.0",
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET",
"access-control-allow-headers": "X-Requested-With,content-type",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"content-length": "555",
"etag": "W/\"22b-dd736629\"",
"date": "Thu, 06 Nov 2014 16:44:19 GMT",
"connection": "close"
}
```
```javascript
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.9207,
34.36094
]
},
"properties": {
"name": "Sanders Grove Cemetery",
"admin0": "United States",
"admin1": "Georgia",
"admin2": "Hart County",
"text": "Sanders Grove Cemetery, Hart County, Georgia"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-83.94213,
33.32262
]
},
"properties": {
"name": "Etheredge Cemetery",
"admin0": "United States",
"admin1": "Georgia",
"admin2": "Butts County",
"text": "Etheredge Cemetery, Butts County, Georgia"
}
}
],
"date": 1415292259726
}
```
## Tests
### âś“ 200 ok
```javascript
response.statusCode.should.equal 200
```
### âś“ valid geojson
```javascript
json.type.should.equal 'FeatureCollection'
json.features.should.be.instanceof Array
```
### âś“ valid response
```javascript
now = new Date().getTime()
should.exist json
should.not.exist json.error
json.date.should.be.within now-5000, now+5000
```

80
docs/doc/success.md

@ -0,0 +1,80 @@
# valid doc query
*Generated: Thu Nov 06 2014 11:44:20 GMT-0500 (EST)*
## Request
```javascript
{
"protocol": "http:",
"host": "localhost",
"method": "GET",
"port": 3100,
"path": "/doc?id=geoname:4221195"
}
```
## Response
```javascript
Status: 200
{
"x-powered-by": "mapzen",
"charset": "utf8",
"cache-control": "public,max-age=60",
"server": "Pelias/0.0.0",
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET",
"access-control-allow-headers": "X-Requested-With,content-type",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"content-length": "311",
"etag": "W/\"137-1644173e\"",
"date": "Thu, 06 Nov 2014 16:44:20 GMT",
"connection": "close"
}
```
```javascript
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.9207,
34.36094
]
},
"properties": {
"name": "Sanders Grove Cemetery",
"admin0": "United States",
"admin1": "Georgia",
"admin2": "Hart County",
"text": "Sanders Grove Cemetery, Hart County, Georgia"
}
}
],
"date": 1415292260057
}
```
## Tests
### âś“ valid geojson
```javascript
json.type.should.equal 'FeatureCollection'
json.features.should.be.instanceof Array
```
### âś“ valid response
```javascript
now = new Date().getTime()
should.exist json
should.not.exist json.error
json.date.should.be.within now-5000, now+5000
```
### âś“ 200 ok
```javascript
response.statusCode.should.equal 200
```

36
docs/index.md

@ -1,6 +1,6 @@
# api root
*Generated: Thu Sep 25 2014 19:25:20 GMT+0100 (BST)*
*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)*
## Request
```javascript
{
@ -27,7 +27,7 @@ Status: 200
"content-type": "application/json; charset=utf-8",
"content-length": "50",
"etag": "W/\"32-85536434\"",
"date": "Thu, 25 Sep 2014 18:25:20 GMT",
"date": "Thu, 06 Nov 2014 16:44:19 GMT",
"connection": "close"
}
```
@ -42,25 +42,20 @@ Status: 200
## Tests
### âś“ vanity header correctly set
```javascript
response.should.have.header 'X-Powered-By','mapzen'
```
### âś“ server header correctly set
### âś“ charset header correctly set
```javascript
response.should.have.header 'Server'
response.headers.server.should.match /Pelias\/\d{1,2}\.\d{1,2}\.\d{1,2}/
response.should.have.header 'Charset','utf8'
```
### âś“ content-type header correctly set
### âś“ endpoint available
```javascript
response.should.have.header 'Content-Type','application/json; charset=utf-8'
response.statusCode.should.equal 200
```
### âś“ cache-control header correctly set
### âś“ server header correctly set
```javascript
response.should.have.header 'Cache-Control','public,max-age=60'
response.should.have.header 'Server'
response.headers.server.should.match /Pelias\/\d{1,2}\.\d{1,2}\.\d{1,2}/
```
### âś“ should respond in json with server info
@ -70,13 +65,18 @@ should.exist json.name
should.exist json.version
```
### âś“ endpoint available
### âś“ vanity header correctly set
```javascript
response.statusCode.should.equal 200
response.should.have.header 'X-Powered-By','mapzen'
```
### âś“ charset header correctly set
### âś“ content-type header correctly set
```javascript
response.should.have.header 'Charset','utf8'
response.should.have.header 'Content-Type','application/json; charset=utf-8'
```
### âś“ cache-control header correctly set
```javascript
response.should.have.header 'Cache-Control','public,max-age=60'
```

14
docs/jsonp.md

@ -1,6 +1,6 @@
# jsonp
*Generated: Thu Sep 25 2014 19:25:20 GMT+0100 (BST)*
*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)*
## Request
```javascript
{
@ -27,7 +27,7 @@ Status: 200
"content-type": "application/javascript; charset=utf-8",
"content-length": "57",
"etag": "W/\"39-b8a2aba1\"",
"date": "Thu, 25 Sep 2014 18:25:20 GMT",
"date": "Thu, 06 Nov 2014 16:44:19 GMT",
"connection": "close"
}
```
@ -37,14 +37,14 @@ test({"name":"pelias-api","version":{"number":"0.0.0"}});
## Tests
### âś“ should respond with jsonp
### âś“ content-type header correctly set
```javascript
should.exist response.body
response.body.substr(0,5).should.equal 'test(';
response.should.have.header 'Content-Type','application/javascript; charset=utf-8'
```
### âś“ content-type header correctly set
### âś“ should respond with jsonp
```javascript
response.should.have.header 'Content-Type','application/javascript; charset=utf-8'
should.exist response.body
response.body.substr(0,5).should.equal 'test(';
```

31
docs/reverse/success.md

@ -1,6 +1,6 @@
# valid reverse query
*Generated: Thu Sep 25 2014 19:25:20 GMT+0100 (BST)*
*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)*
## Request
```javascript
{
@ -25,9 +25,9 @@ Status: 200
"access-control-allow-headers": "X-Requested-With,content-type",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"content-length": "263",
"etag": "W/\"107-75b55c25\"",
"date": "Thu, 25 Sep 2014 18:25:20 GMT",
"content-length": "282",
"etag": "W/\"11a-efcd00c9\"",
"date": "Thu, 06 Nov 2014 16:44:19 GMT",
"connection": "close"
}
```
@ -40,29 +40,25 @@ Status: 200
"geometry": {
"type": "Point",
"coordinates": [
-82.506198,
29.542519
-82.50622,
29.49136
]
},
"properties": {
"name": "Archer",
"name": "Adam",
"admin0": "United States",
"admin1": "*florida",
"text": "Archer, *florida, United States"
"admin1": "Florida",
"admin2": "Alachua County",
"text": "Adam, Alachua County, Florida"
}
}
],
"date": 1411669520735
"date": 1415292259729
}
```
## Tests
### âś“ 200 ok
```javascript
response.statusCode.should.equal 200
```
### âś“ valid response
```javascript
now = new Date().getTime()
@ -71,6 +67,11 @@ should.not.exist json.error
json.date.should.be.within now-5000, now+5000
```
### âś“ 200 ok
```javascript
response.statusCode.should.equal 200
```
### âś“ valid geojson
```javascript
json.type.should.equal 'FeatureCollection'

183
docs/search/success.md

@ -1,6 +1,6 @@
# valid search query
*Generated: Thu Sep 25 2014 19:25:21 GMT+0100 (BST)*
*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)*
## Request
```javascript
{
@ -25,9 +25,9 @@ Status: 200
"access-control-allow-headers": "X-Requested-With,content-type",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"content-length": "289",
"etag": "W/\"121-69343a38\"",
"date": "Thu, 25 Sep 2014 18:25:20 GMT",
"content-length": "2398",
"etag": "W/\"NldeHivz2maJ3rqa73a+2w==\"",
"date": "Thu, 06 Nov 2014 16:44:19 GMT",
"connection": "close"
}
```
@ -40,20 +40,173 @@ Status: 200
"geometry": {
"type": "Point",
"coordinates": [
-82.357442,
29.72089
-82.5052,
29.50312
]
},
"properties": {
"name": "Hidden Lake",
"name": "Blue Pete Lake",
"admin0": "United States",
"admin1": "Florida",
"admin2": "Alachua",
"text": "Hidden Lake, Alachua, United States"
"admin2": "Alachua County",
"text": "Blue Pete Lake, Alachua County, Florida"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.52097,
29.47185
]
},
"properties": {
"name": "Sawgrass Lake",
"admin0": "United States",
"admin1": "Florida",
"admin2": "Levy County",
"text": "Sawgrass Lake, Levy County, Florida"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.39141,
29.4468
]
},
"properties": {
"name": "Johnson Lake",
"admin0": "United States",
"admin1": "Florida",
"admin2": "Marion County",
"text": "Johnson Lake, Marion County, Florida"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.35435,
29.49526
]
},
"properties": {
"name": "Ledwith Lake",
"admin0": "United States",
"admin1": "Florida",
"admin2": "Alachua County",
"text": "Ledwith Lake, Alachua County, Florida"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.35316,
29.52469
]
},
"properties": {
"name": "Levy Lake",
"admin0": "United States",
"admin1": "Florida",
"admin2": "Alachua County",
"text": "Levy Lake, Alachua County, Florida"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.66311,
29.54036
]
},
"properties": {
"name": "Fox Lake",
"admin0": "United States",
"admin1": "Florida",
"admin2": "Levy County",
"text": "Fox Lake, Levy County, Florida"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.40502,
29.61705
]
},
"properties": {
"name": "Lake Kanapaha",
"admin0": "United States",
"admin1": "Florida",
"admin2": "Alachua County",
"text": "Lake Kanapaha, Alachua County, Florida"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.70856,
29.53293
]
},
"properties": {
"name": "Doorshutter Lake",
"admin0": "United States",
"admin1": "Florida",
"admin2": "Levy County",
"text": "Doorshutter Lake, Levy County, Florida"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.30215,
29.52978
]
},
"properties": {
"name": "Wauberg Lake",
"admin0": "United States",
"admin1": "Florida",
"admin2": "Alachua County",
"text": "Wauberg Lake, Alachua County, Florida"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.47914,
29.30795
]
},
"properties": {
"name": "Lake Stafford",
"admin0": "United States",
"admin1": "Florida",
"admin2": "Levy County",
"text": "Lake Stafford, Levy County, Florida"
}
}
],
"date": 1411669520989
"date": 1415292259730
}
```
@ -67,14 +220,14 @@ should.not.exist json.error
json.date.should.be.within now-5000, now+5000
```
### âś“ valid geojson
### âś“ 200 ok
```javascript
json.type.should.equal 'FeatureCollection'
json.features.should.be.instanceof Array
response.statusCode.should.equal 200
```
### âś“ 200 ok
### âś“ valid geojson
```javascript
response.statusCode.should.equal 200
json.type.should.equal 'FeatureCollection'
json.features.should.be.instanceof Array
```

164
docs/suggest/success.md

@ -1,6 +1,6 @@
# valid suggest query
*Generated: Thu Sep 25 2014 19:25:20 GMT+0100 (BST)*
*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)*
## Request
```javascript
{
@ -25,9 +25,9 @@ Status: 200
"access-control-allow-headers": "X-Requested-With,content-type",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"content-length": "1933",
"etag": "W/\"I89q+0HZNmXyHsTfLSP5Ww==\"",
"date": "Thu, 25 Sep 2014 18:25:20 GMT",
"content-length": "571",
"etag": "W/\"23b-5d6e3dd3\"",
"date": "Thu, 06 Nov 2014 16:44:19 GMT",
"connection": "close"
}
```
@ -40,47 +40,15 @@ Status: 200
"geometry": {
"type": "Point",
"coordinates": [
7.56019,
5.419786
-8.481618,
43.125692
]
},
"properties": {
"text": "Abia, Nigeria",
"score": 1,
"text": "A Coruña",
"score": 14,
"type": "admin1",
"id": "1775:adm1:ng:nga:abia"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-66.908143,
-9.954353
]
},
"properties": {
"text": "Acrelândia, Brazil",
"score": 1,
"type": "admin2",
"id": "708:adm2:br:bra:acrel_ndia"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-60.005461,
-3.099378
]
},
"properties": {
"text": "AdrianĂłpolis, Manaus, Brasil",
"score": 1,
"type": "neighborhood",
"id": "799:_:_:_:adrian_polis"
"id": "3374:adm1:es:esp:a_coru_a"
}
},
{
@ -88,95 +56,15 @@ Status: 200
"geometry": {
"type": "Point",
"coordinates": [
7.909644,
5.013733
7.56019,
5.419786
]
},
"properties": {
"text": "Akwa Ibom, Nigeria",
"score": 1,
"text": "Abia",
"score": 14,
"type": "admin1",
"id": "1776:adm1:ng:nga:akwa_ibom"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
9.691808,
4.050576
]
},
"properties": {
"text": "Akwa, Littoral, Cameroun",
"score": 1,
"type": "neighborhood",
"id": "1863:_:_:_:akwa"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-56.404593,
-10.042071
]
},
"properties": {
"text": "Alta Floresta, Brazil",
"score": 1,
"type": "admin2",
"id": "2986:adm2:br:bra:alta_floresta"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-62.627879,
3.10354
]
},
"properties": {
"text": "Alto Alegre, Brazil",
"score": 1,
"type": "admin2",
"id": "4611:adm2:br:bra:alto_alegre"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-63.418743,
-9.697774
]
},
"properties": {
"text": "Alto ParaĂ­so, Brazil",
"score": 1,
"type": "admin2",
"id": "4584:adm2:br:bra:alto_para_so"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-65.296384,
-3.674615
]
},
"properties": {
"text": "AlvarĂŁes, Brazil",
"score": 1,
"type": "admin2",
"id": "832:adm2:br:bra:alvar_es"
"id": "1775:adm1:ng:nga:abia"
}
},
{
@ -184,29 +72,24 @@ Status: 200
"geometry": {
"type": "Point",
"coordinates": [
-62.710104,
3.724864
33.772337,
2.826081
]
},
"properties": {
"text": "Amajari, Brazil",
"score": 1,
"type": "admin2",
"id": "4610:adm2:br:bra:amajari"
"text": "Abim",
"score": 14,
"type": "admin1",
"id": "2848:adm1:ug:uga:abim"
}
}
],
"date": 1411669520909
"date": 1415292259700
}
```
## Tests
### âś“ 200 ok
```javascript
response.statusCode.should.equal 200
```
### âś“ valid geojson
```javascript
json.type.should.equal 'FeatureCollection'
@ -221,3 +104,8 @@ should.not.exist json.error
json.date.should.be.within now-5000, now+5000
```
### âś“ 200 ok
```javascript
response.statusCode.should.equal 200
```

223
docs/suggest/success_nearby.md

@ -0,0 +1,223 @@
# valid suggest query
*Generated: Thu Nov 06 2014 11:44:20 GMT-0500 (EST)*
## Request
```javascript
{
"protocol": "http:",
"host": "localhost",
"method": "GET",
"port": 3100,
"path": "/suggest/nearby?input=a&lat=29.49136&lon=-82.50622"
}
```
## Response
```javascript
Status: 200
{
"x-powered-by": "mapzen",
"charset": "utf8",
"cache-control": "public,max-age=60",
"server": "Pelias/0.0.0",
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET",
"access-control-allow-headers": "X-Requested-With,content-type",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"content-length": "2034",
"etag": "W/\"Do9VJ5hCbynTxDjtm5fNlg==\"",
"date": "Thu, 06 Nov 2014 16:44:19 GMT",
"connection": "close"
}
```
```javascript
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.05231,
29.17998
]
},
"properties": {
"text": "Abiding Hope E V Lutheran Church, Marion County, Florida",
"score": 1,
"type": "geoname",
"id": "4145572"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.10231,
29.21942
]
},
"properties": {
"text": "Abundant Harvest Ministries, Marion County, Florida",
"score": 1,
"type": "geoname",
"id": "4145578"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.50622,
29.49136
]
},
"properties": {
"text": "Adam, Alachua County, Florida",
"score": 1,
"type": "geoname",
"id": "4145612"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.75374,
35.17789
]
},
"properties": {
"text": "Adams Branch, Transylvania County, North Carolina",
"score": 1,
"type": "geoname",
"id": "4452189"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.83012,
29.4783
]
},
"properties": {
"text": "Adamsville Cemetery, Levy County, Florida",
"score": 1,
"type": "geoname",
"id": "4145634"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.01511,
35.17289
]
},
"properties": {
"text": "Africa School (historical), Spartanburg County, South Carolina",
"score": 1,
"type": "geoname",
"id": "4569065"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.20426,
29.25192
]
},
"properties": {
"text": "Agape Baptist Church, Marion County, Florida",
"score": 1,
"type": "geoname",
"id": "4145673"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.14954,
29.19248
]
},
"properties": {
"text": "Agnew Cemetery, Marion County, Florida",
"score": 1,
"type": "geoname",
"id": "4145677"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.75429,
35.16928
]
},
"properties": {
"text": "Aiken Mountain, Transylvania County, North Carolina",
"score": 1,
"type": "geoname",
"id": "4452268"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
-82.15912,
29.47877
]
},
"properties": {
"text": "Alachua County Fire Rescue Station 31, Alachua County, Florida",
"score": 1,
"type": "geoname",
"id": "4152402"
}
}
],
"date": 1415292259785
}
```
## Tests
### âś“ 200 ok
```javascript
response.statusCode.should.equal 200
```
### âś“ valid response
```javascript
now = new Date().getTime()
should.exist json
should.not.exist json.error
json.date.should.be.within now-5000, now+5000
```
### âś“ valid geojson
```javascript
json.type.should.equal 'FeatureCollection'
json.features.should.be.instanceof Array
```

3
package.json

@ -36,7 +36,8 @@
"geojson": "^0.2.0",
"geopipes-elasticsearch-backend": "0.0.8",
"pelias-esclient": "0.0.25",
"toobusy": "^0.2.4"
"toobusy": "^0.2.4",
"async": "^0.9.0"
},
"devDependencies": {
"ciao": "^0.3.4",

2
query/reverse.js

@ -9,7 +9,7 @@ function generate( params ){
lon: params.lon
};
return queries.distance( centroid, { size: 1 } );
return queries.distance( centroid, { size: params.size || 1 } );
}
module.exports = generate;

4
query/suggest.js

@ -2,7 +2,7 @@
var logger = require('../src/logger');
// Build pelias suggest query
function generate( params ){
function generate( params, precision ){
var getPrecision = function(zoom) {
switch (true) {
@ -29,7 +29,7 @@ function generate( params ){
'dataset': params.layers,
'location': {
'value': [ params.lon, params.lat ],
'precision': getPrecision(params.zoom)
'precision': precision || getPrecision(params.zoom)
}
}
}

73
sanitiser/_id.js

@ -0,0 +1,73 @@
// validate inputs, convert types and apply defaults
// id generally looks like 'geoname:4163334' (type:id)
// so, both type and id are required fields.
function sanitize( req ){
req.clean = req.clean || {};
var params = req.query;
var indeces = require('../query/indeces');
var delim = ':';
// ensure params is a valid object
if( Object.prototype.toString.call( params ) !== '[object Object]' ){
params = {};
}
var errormessage = function(fieldname, message) {
return {
'error': true,
'message': message || ('invalid param \''+ fieldname + '\': text length, must be >0')
}
};
if(('string' === typeof params.id && !params.id.length) || params.id === undefined){
return errormessage('id');
}
if( params && params.id && params.id.length ){
req.clean.ids = [];
params.ids = Array.isArray(params.id) ? params.id : [params.id];
// de-dupe
params.ids = params.ids.filter(function(item, pos) {
return params.ids.indexOf(item) == pos;
});
for (var i=0; i<params.ids.length; i++) {
var thisparam = params.ids[i];
// basic format/ presence of ':'
if(thisparam.indexOf(delim) === -1) {
return errormessage(null, 'invalid: must be of the format type:id for ex: \'geoname:4163334\'');
}
var param_index = thisparam.indexOf(delim);
var type = thisparam.substring(0, param_index );
var id = thisparam.substring(param_index + 1);
// id text
if('string' !== typeof id || !id.length){
return errormessage(thisparam);
}
// type text
if('string' !== typeof type || !type.length){
return errormessage(thisparam);
}
// type text must be one of the indeces
if(indeces.indexOf(type) == -1){
return errormessage('type', type + ' is invalid. It must be one of these values - [' + indeces.join(", ") + ']');
}
req.clean.ids.push({
id: id,
type: type
});
}
}
return { 'error': false };
}
// export function
module.exports = sanitize;

25
sanitiser/_layers.js

@ -13,17 +13,38 @@ function sanitize( req ){
// which layers to query
if('string' === typeof params.layers && params.layers.length){
var alias_layers = ['poi', 'admin'];
var alias_indeces = indeces.concat(alias_layers);
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] ) ){
if( -1 === alias_indeces.indexOf( layers[x] ) ){
return {
'error': true,
'message': 'invalid param \'layer\': must be one or more of ' + indeces.join(',')
'message': 'invalid param \'layer\': must be one or more of ' + alias_indeces.join(',')
}
}
}
var expand_aliases = function(alias, layers, layer_indeces) {
var alias_index = layers.indexOf(alias);
if (alias_index !== -1 ) {
layers.splice(alias_index, 1);
layers = layers.concat(layer_indeces);
}
return layers;
}
layers = expand_aliases('poi', layers, ['geoname','osmnode','osmway']);
layers = expand_aliases('admin', layers, ['admin0','admin1','admin2','neighborhood']);
// de-dup
layers = layers.filter(function(item, pos) {
return layers.indexOf(item) == pos;
});
clean.layers = layers;
}
else {

6
sanitiser/_size.js

@ -1,9 +1,11 @@
// validate inputs, convert types and apply defaults
function sanitize( req ){
function sanitize( req, default_size){
var clean = req.clean || {};
var params= req.query;
var default_size = default_size || 10;
// ensure the input params are a valid object
if( Object.prototype.toString.call( params ) !== '[object Object]' ){
params = {};
@ -14,7 +16,7 @@ function sanitize( req ){
if( !isNaN( size ) ){
clean.size = Math.min( Math.max( size, 1 ), 40 ); // max
} else {
clean.size = 10; // default
clean.size = default_size; // default
}
req.clean = clean;

23
sanitiser/doc.js

@ -0,0 +1,23 @@
var logger = require('../src/logger'),
_sanitize = require('../sanitiser/_sanitize'),
sanitizers = {
id: require('../sanitiser/_id')
};
var sanitize = function(req, cb) { _sanitize(req, sanitizers, cb); }
// export sanitize for testing
module.exports.sanitize = sanitize;
// middleware
module.exports.middleware = function( req, res, next ){
sanitize( req, function( err, clean ){
if( err ){
res.status(400); // 400 Bad Request
return next(err);
}
req.clean = clean;
next();
});
};

10
sanitiser/reverse.js

@ -2,8 +2,12 @@
var logger = require('../src/logger'),
_sanitize = require('../sanitiser/_sanitize'),
sanitiser = {
latlonzoom: require('../sanitiser/_latlonzoom')
};
latlonzoom: require('../sanitiser/_latlonzoom'),
size: function( req ) {
var size = require('../sanitiser/_size');
return size(req, 1);
}
};
var sanitize = function(req, cb) { _sanitize(req, sanitiser, cb); }
@ -20,4 +24,4 @@ module.exports.middleware = function( req, res, next ){
req.clean = clean;
next();
});
};
};

43
service/mget.js

@ -0,0 +1,43 @@
/**
query must be an array of hashes, structured like so:
{
_index: 'myindex',
_type: 'mytype',
_id: 'myid'
}
**/
function service( backend, query, cb ){
// backend command
var cmd = {
body: {
docs: query
}
};
// query new backend
backend().client.mget( cmd, function( err, data ){
// handle backend errors
if( err ){ return cb( err ); }
// map returned documents
var docs = [];
if( data && Array.isArray(data.docs) ){
docs = data.docs.map( function( doc ){
return doc._source;
});
}
// fire callback
return cb( null, docs );
});
}
module.exports = service;

30
service/search.js

@ -0,0 +1,30 @@
/**
cmd can be any valid ES query command
**/
function service( backend, cmd, cb ){
// query new backend
backend().client.search( cmd, function( err, data ){
// handle backend errors
if( err ){ return cb( err ); }
// map returned documents
var docs = [];
if( data && data.hits && data.hits.total && Array.isArray(data.hits.hits)){
docs = data.hits.hits.map( function( hit ){
return hit._source;
});
}
// fire callback
return cb( null, docs );
});
}
module.exports = service;

28
service/suggest.js

@ -0,0 +1,28 @@
/**
cmd can be any valid ES suggest command
**/
function service( backend, cmd, cb ){
// query new backend
backend().client.suggest( cmd, function( err, data ){
// handle backend errors
if( err ){ return cb( err ); }
// map returned documents
var docs = [];
if( data && Array.isArray( data.pelias ) && data.pelias.length ){
docs = data['pelias'][0].options || [];
}
// fire callback
return cb( null, docs );
});
}
module.exports = service;

16
test/ciao/doc/msuccess.coffee

@ -0,0 +1,16 @@
#> valid doc query
path: '/doc?id=geoname:4221195&id=geoname:4193595'
#? 200 ok
response.statusCode.should.equal 200
#? valid response
now = new Date().getTime()
should.exist json
should.not.exist json.error
json.date.should.be.within now-5000, now+5000
#? valid geojson
json.type.should.equal 'FeatureCollection'
json.features.should.be.instanceof Array

16
test/ciao/doc/success.coffee

@ -0,0 +1,16 @@
#> valid doc query
path: '/doc?id=geoname:4221195'
#? 200 ok
response.statusCode.should.equal 200
#? valid response
now = new Date().getTime()
should.exist json
should.not.exist json.error
json.date.should.be.within now-5000, now+5000
#? valid geojson
json.type.should.equal 'FeatureCollection'
json.features.should.be.instanceof Array

16
test/ciao/suggest/success_nearby.coffee

@ -0,0 +1,16 @@
#> valid suggest query
path: '/suggest/nearby?input=a&lat=29.49136&lon=-82.50622'
#? 200 ok
response.statusCode.should.equal 200
#? valid response
now = new Date().getTime()
should.exist json
should.not.exist json.error
json.date.should.be.within now-5000, now+5000
#? valid geojson
json.type.should.equal 'FeatureCollection'
json.features.should.be.instanceof Array

92
test/unit/controller/doc.js

@ -0,0 +1,92 @@
var setup = require('../../../controller/doc'),
mockBackend = require('../mock/backend');
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) {
t.equal(typeof setup, 'function', 'setup is a function');
t.equal(typeof setup(), 'function', 'setup returns a controller');
t.end();
});
};
// functionally test controller (backend success)
module.exports.tests.functional_success = function(test, common) {
// expected geojson features for 'client/doc/ok/1' fixture
var expected = [{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [ -50.5, 100.1 ]
},
properties: {
name: 'test name1',
admin0: 'country1',
admin1: 'state1',
admin2: 'city1'
}
}, {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [ -51.5, 100.2 ]
},
properties: {
name: 'test name2',
admin0: 'country2',
admin1: 'state2',
admin2: 'city2'
}
}];
test('functional success', function(t) {
var backend = mockBackend( 'client/doc/ok/1', function( cmd ){
t.deepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'a' } ] } }, 'correct backend command');
});
var controller = setup( backend );
var res = {
status: function( code ){
t.equal(code, 200, 'status set');
return res;
},
json: function( json ){
t.equal(typeof json, 'object', 'returns json');
t.equal(typeof json.date, 'number', 'date set');
t.equal(json.type, 'FeatureCollection', 'valid geojson');
t.true(Array.isArray(json.features), 'features is array');
t.deepEqual(json.features, expected, 'values correctly mapped');
t.end();
}
};
controller( { clean: { ids: [ {'id' : 123, 'type': 'a' } ] } }, res );
});
};
// functionally test controller (backend failure)
module.exports.tests.functional_failure = function(test, common) {
test('functional failure', function(t) {
var backend = mockBackend( 'client/doc/fail/1', function( cmd ){
t.deepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'b' } ] } }, 'correct backend command');
});
var controller = setup( backend );
var next = function( message ){
t.equal(message,'a backend error occurred','error passed to errorHandler');
t.end();
};
controller( { clean: { ids: [ {'id' : 123, 'type': 'b' } ] } }, undefined, next );
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('GET /doc ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

13
test/unit/controller/suggest.js

@ -24,7 +24,7 @@ module.exports.tests.functional_success = function(test, common) {
coordinates: [ 101, -10.1 ]
},
properties: {
id: 'mockid',
id: 'mockid1',
type: 'mocktype',
value: 1
}
@ -35,7 +35,7 @@ module.exports.tests.functional_success = function(test, common) {
coordinates: [ 101, -10.1 ]
},
properties: {
id: 'mockid',
id: 'mockid2',
type: 'mocktype',
value: 2
}
@ -43,7 +43,12 @@ module.exports.tests.functional_success = function(test, common) {
test('functional success', function(t) {
var backend = mockBackend( 'client/suggest/ok/1', function( cmd ){
t.deepEqual(cmd, { body: { a: 'b' }, index: 'pelias' }, 'correct backend command');
if (cmd.body.layers) {
// layers are set exclusively for admin: test for admin-only layers
t.deepEqual(cmd, { body: { input: 'b', layers: [ 'admin0', 'admin1', 'admin2' ] }, index: 'pelias' }, 'correct backend command');
} else {
t.deepEqual(cmd, { body: { input: 'b' }, index: 'pelias' }, 'correct backend command');
}
});
var controller = setup( backend, mockQuery() );
var res = {
@ -60,7 +65,7 @@ module.exports.tests.functional_success = function(test, common) {
t.end();
}
};
controller( { clean: { a: 'b' } }, res );
controller( { clean: { input: 'b' } }, res );
});
};

91
test/unit/controller/suggest_nearby.js

@ -0,0 +1,91 @@
var setup = require('../../../controller/suggest'),
mockBackend = require('../mock/backend'),
mockQuery = require('../mock/query');
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) {
t.equal(typeof setup, 'function', 'setup is a function');
t.equal(typeof setup(), 'function', 'setup returns a controller');
t.end();
});
};
// functionally test controller (backend success)
module.exports.tests.functional_success = function(test, common) {
// expected geojson features for 'client/suggest/ok/1' fixture
var expected = [{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [ 101, -10.1 ]
},
properties: {
id: 'mockid1',
type: 'mocktype',
value: 1
}
}, {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [ 101, -10.1 ]
},
properties: {
id: 'mockid2',
type: 'mocktype',
value: 2
}
}];
test('functional success', function(t) {
var backend = mockBackend( 'client/suggest/ok/1', function( cmd ){
t.deepEqual(cmd, { body: { a: 'b' }, index: 'pelias' }, 'correct backend command');
});
var controller = setup( backend, mockQuery() );
var res = {
status: function( code ){
t.equal(code, 200, 'status set');
return res;
},
json: function( json ){
t.equal(typeof json, 'object', 'returns json');
t.equal(typeof json.date, 'number', 'date set');
t.equal(json.type, 'FeatureCollection', 'valid geojson');
t.true(Array.isArray(json.features), 'features is array');
t.deepEqual(json.features, expected, 'values correctly mapped');
t.end();
}
};
controller( { clean: { a: 'b' } }, res );
});
};
// functionally test controller (backend failure)
module.exports.tests.functional_failure = function(test, common) {
test('functional failure', function(t) {
var backend = mockBackend( 'client/suggest/fail/1', function( cmd ){
t.deepEqual(cmd, { body: { a: 'b' }, index: 'pelias' }, 'correct backend command');
});
var controller = setup( backend, mockQuery() );
var next = function( message ){
t.equal(message,'a backend error occurred','error passed to errorHandler');
t.end();
};
controller( { clean: { a: 'b' } }, undefined, next );
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('GET /suggest ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

37
test/unit/mock/backend.js

@ -1,12 +1,14 @@
var mockPayload = {
id: 'mocktype/mockid',
geo: '101,-10.1'
var mockPayload = function(id){
return {
id: 'mocktype/mockid'+id,
geo: '101,-10.1'
}
};
var responses = {};
responses['client/suggest/ok/1'] = function( cmd, cb ){
return cb( undefined, suggestEnvelope([ { value: 1, payload: mockPayload }, { value: 2, payload: mockPayload } ]) );
return cb( undefined, suggestEnvelope([ { value: 1, payload: mockPayload(1) }, { value: 2, payload: mockPayload(2) } ]) );
};
responses['client/suggest/fail/1'] = function( cmd, cb ){
return cb( 'a backend error occurred' );
@ -32,10 +34,33 @@ responses['client/search/fail/1'] = function( cmd, cb ){
return cb( 'a backend error occurred' );
};
responses['client/doc/ok/1'] = function( cmd, cb ){
return cb( undefined, docEnvelope([{
_source: {
value: 1,
center_point: { lat: 100.1, lon: -50.5 },
name: { default: 'test name1' },
admin0: 'country1', admin1: 'state1', admin2: 'city1'
}
}, {
_source: {
value: 2,
center_point: { lat: 100.2, lon: -51.5 },
name: { default: 'test name2' },
admin0: 'country2', admin1: 'state2', admin2: 'city2'
}
}]));
};
responses['client/doc/fail/1'] = responses['client/search/fail/1'];
function setup( key, cmdCb ){
function backend( a, b ){
return {
client: {
mget: function( cmd, cb ){
if( 'function' === typeof cmdCb ){ cmdCb( cmd ); }
return responses[key].apply( this, arguments );
},
suggest: function( cmd, cb ){
if( 'function' === typeof cmdCb ){ cmdCb( cmd ); }
return responses[key].apply( this, arguments );
@ -50,6 +75,10 @@ function setup( key, cmdCb ){
return backend;
}
function docEnvelope( options ){
return { docs: options };
}
function suggestEnvelope( options ){
return { pelias: [{ options: options }]};
}

10
test/unit/query/reverse.js

@ -58,6 +58,16 @@ module.exports.tests.query = function(test, common) {
};
t.deepEqual(query, expected, 'valid reverse query');
// test different sizes
var sizes = [1,2,10,undefined,null];
sizes.forEach( function(size) {
query = generate({
lat: 29.49136, lon: -82.50622, size: size
});
expected.size = size ? size : 1;
t.deepEqual(query, expected, 'valid reverse query for size: '+ size);
});
t.end();
});
};

9
test/unit/query/suggest.js

@ -63,9 +63,8 @@ module.exports.tests.precision = function(test, common) {
{zoom:null, precision:1},
{zoom:undefined, precision:1}
];
test_cases.forEach( function( test_case ){
test('valid precision where zoom = ' + test_case.zoom, function(t) {
test('valid precision', function(t) {
test_cases.forEach( function( test_case ){
var query = generate({
input: 'test', size: 10,
lat: 0, lon: 0, zoom:test_case.zoom,
@ -87,9 +86,9 @@ module.exports.tests.precision = function(test, common) {
}
}
};
t.deepEqual(query, expected, 'valid suggest query');
t.end();
t.deepEqual(query, expected, 'valid suggest query for zoom = ' + test_case.zoom);
});
t.end();
});
};

8
test/unit/run.js

@ -4,9 +4,15 @@ var common = {};
var tests = [
require('./controller/index'),
require('./controller/doc'),
require('./controller/suggest'),
require('./controller/suggest_nearby'),
require('./controller/search'),
require('./sanitiser/sanitise'),
require('./service/mget'),
require('./service/search'),
require('./service/suggest'),
require('./sanitiser/suggest'),
require('./sanitiser/doc'),
require('./query/indeces'),
require('./query/suggest'),
require('./query/search'),

159
test/unit/sanitiser/doc.js

@ -0,0 +1,159 @@
var doc = require('../../../sanitiser/doc'),
_sanitize = doc.sanitize,
middleware = doc.middleware,
indeces = require('../../../query/indeces'),
delimiter = ':',
defaultLengthError = function(input) { return 'invalid param \''+ input + '\': text length, must be >0' },
defaultFormatError = 'invalid: must be of the format type:id for ex: \'geoname:4163334\'',
defaultError = 'invalid param \'id\': text length, must be >0',
defaultMissingTypeError = function(input) {
var type = input.split(delimiter)[0];
return type + ' is invalid. It must be one of these values - [' + indeces.join(", ") + ']'},
defaultClean = { ids: [ { id: '123', type: 'geoname' } ] },
sanitize = function(query, cb) { _sanitize({'query':query}, cb); },
inputs = {
valid: [ 'geoname:1', 'osmnode:2', 'admin0:53', 'osmway:44', 'geoname:5' ],
invalid: [ ':', '', '::', 'geoname:', ':234', 'gibberish:23' ]
};
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('sanitize interface', function(t) {
t.equal(typeof sanitize, 'function', 'sanitize is a function');
t.equal(sanitize.length, 2, 'sanitize interface');
t.end();
});
test('middleware interface', function(t) {
t.equal(typeof middleware, 'function', 'middleware is a function');
t.equal(middleware.length, 3, 'sanitizee has a valid middleware');
t.end();
});
};
module.exports.tests.sanitize_id = function(test, common) {
test('invalid input', function(t) {
inputs.invalid.forEach( function( input ){
sanitize({ id: input }, function( err, clean ){
switch (err) {
case defaultError:
t.equal(err, defaultError, input + ' is invalid input'); break;
case defaultLengthError(input):
t.equal(err, defaultLengthError(input), input + ' is invalid (missing id/type)'); break;
case defaultFormatError:
t.equal(err, defaultFormatError, input + ' is invalid (invalid format)'); break;
case defaultMissingTypeError(input):
t.equal(err, defaultMissingTypeError(input), input + ' is an unknown type'); break;
default: break;
}
t.equal(clean, undefined, 'clean not set');
});
});
t.end();
});
test('valid input', function(t) {
inputs.valid.forEach( function( input ){
var input_parts = input.split(delimiter);
var expected = { ids: [ { id: input_parts[1], type: input_parts[0] } ] };
sanitize({ id: input }, function( err, clean ){
t.equal(err, undefined, 'no error (' + input + ')' );
t.deepEqual(clean, expected, 'clean set correctly (' + input + ')');
});
});
t.end();
});
};
module.exports.tests.sanitize_ids = function(test, common) {
test('invalid input', function(t) {
sanitize({ id: inputs.invalid }, function( err, clean ){
var input = inputs.invalid[0]; // since it breaks on the first invalid element
switch (err) {
case defaultError:
t.equal(err, defaultError, input + ' is invalid input'); break;
case defaultLengthError(input):
t.equal(err, defaultLengthError(input), input + ' is invalid (missing id/type)'); break;
case defaultFormatError:
t.equal(err, defaultFormatError, input + ' is invalid (invalid format)'); break;
case defaultMissingTypeError(input):
t.equal(err, defaultMissingTypeError(input), input + ' is an unknown type'); break;
default: break;
}
t.equal(clean, undefined, 'clean not set');
});
t.end();
});
test('valid input', function(t) {
var expected={};
expected.ids = [];
inputs.valid.forEach( function( input ){
var input_parts = input.split(delimiter);
expected.ids.push({ id: input_parts[1], type: input_parts[0] });
});
sanitize({ id: inputs.valid }, function( err, clean ){
t.equal(err, undefined, 'no error' );
t.deepEqual(clean, expected, 'clean set correctly');
});
t.end();
});
};
module.exports.tests.de_dupe = function(test, common) {
var expected = { ids: [ { id: '1', type: 'geoname' }, { id: '2', type: 'osmnode' } ] };
test('duplicate ids', function(t) {
sanitize( { id: ['geoname:1', 'osmnode:2', 'geoname:1'] }, function( err, clean ){
t.equal(err, undefined, 'no error' );
t.deepEqual(clean, expected, 'clean set correctly');
t.end();
});
});
};
module.exports.tests.invalid_params = function(test, common) {
test('invalid params', function(t) {
sanitize( undefined, function( err, clean ){
t.equal(err, defaultError, 'handle invalid params gracefully');
t.end();
});
});
};
module.exports.tests.middleware_failure = function(test, common) {
test('middleware failure', function(t) {
var res = { status: function( code ){
t.equal(code, 400, 'status set');
}};
var next = function( message ){
t.equal(message, defaultError);
t.end();
};
middleware( {}, res, next );
});
};
module.exports.tests.middleware_success = function(test, common) {
test('middleware success', function(t) {
var req = { query: { id: 'geoname' + delimiter + '123' }};
var next = function( message ){
t.equal(message, undefined, 'no error message set');
t.deepEqual(req.clean, defaultClean);
t.end();
};
middleware( req, undefined, next );
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('SANTIZE /doc ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

97
test/unit/sanitiser/sanitise.js → test/unit/sanitiser/suggest.js

@ -26,25 +26,25 @@ module.exports.tests.sanitize_input = function(test, common) {
invalid: [ '', 100, null, undefined, new Date() ],
valid: [ 'a', 'aa', 'aaaaaaaa' ]
};
inputs.invalid.forEach( function( input ){
test('invalid input', function(t) {
test('invalid input', function(t) {
inputs.invalid.forEach( function( input ){
sanitize({ input: input, lat: 0, lon: 0 }, function( err, clean ){
t.equal(err, 'invalid param \'input\': text length, must be >0', 'invalid input');
t.equal(err, 'invalid param \'input\': text length, must be >0', input + ' is an invalid input');
t.equal(clean, undefined, 'clean not set');
t.end();
});
});
t.end();
});
inputs.valid.forEach( function( input ){
test('valid input', function(t) {
test('valid input', function(t) {
inputs.valid.forEach( function( input ){
sanitize({ input: input, lat: 0, lon: 0 }, function( err, clean ){
var expected = JSON.parse(JSON.stringify( defaultClean ));
expected.input = input;
t.equal(err, undefined, 'no error');
t.deepEqual(clean, expected, 'clean set correctly');
t.end();
t.deepEqual(clean, expected, 'clean set correctly (' + input + ')');
});
});
t.end();
});
};
@ -53,25 +53,25 @@ module.exports.tests.sanitize_lat = function(test, common) {
invalid: [ -181, -120, -91, 91, 120, 181 ],
valid: [ 0, 45, 90, -0, '0', '45', '90' ]
};
lats.invalid.forEach( function( lat ){
test('invalid lat', function(t) {
test('invalid lat', function(t) {
lats.invalid.forEach( function( lat ){
sanitize({ input: 'test', lat: lat, lon: 0 }, function( err, clean ){
t.equal(err, 'invalid param \'lat\': must be >-90 and <90', 'invalid latitude');
t.equal(err, 'invalid param \'lat\': must be >-90 and <90', lat + ' is an invalid latitude');
t.equal(clean, undefined, 'clean not set');
t.end();
});
});
t.end();
});
lats.valid.forEach( function( lat ){
test('valid lat', function(t) {
test('valid lat', function(t) {
lats.valid.forEach( function( lat ){
sanitize({ input: 'test', lat: lat, lon: 0 }, function( err, clean ){
var expected = JSON.parse(JSON.stringify( defaultClean ));
expected.lat = parseFloat( lat );
t.equal(err, undefined, 'no error');
t.deepEqual(clean, expected, 'clean set correctly');
t.end();
t.deepEqual(clean, expected, 'clean set correctly (' + lat + ')');
});
});
t.end();
});
};
@ -80,25 +80,26 @@ module.exports.tests.sanitize_lon = function(test, common) {
invalid: [ -360, -181, 181, 360 ],
valid: [ -180, -1, -0, 0, 45, 90, '-180', '0', '180' ]
};
lons.invalid.forEach( function( lon ){
test('invalid lon', function(t) {
test('invalid lon', function(t) {
lons.invalid.forEach( function( lon ){
sanitize({ input: 'test', lat: 0, lon: lon }, function( err, clean ){
t.equal(err, 'invalid param \'lon\': must be >-180 and <180', 'invalid longitude');
t.equal(err, 'invalid param \'lon\': must be >-180 and <180', lon + ' is an invalid longitude');
t.equal(clean, undefined, 'clean not set');
t.end();
});
});
t.end();
});
lons.valid.forEach( function( lon ){
test('valid lon', function(t) {
test('valid lon', function(t) {
lons.valid.forEach( function( lon ){
sanitize({ input: 'test', lat: 0, lon: lon }, function( err, clean ){
var expected = JSON.parse(JSON.stringify( defaultClean ));
expected.lon = parseFloat( lon );
t.equal(err, undefined, 'no error');
t.deepEqual(clean, expected, 'clean set correctly');
t.end();
t.deepEqual(clean, expected, 'clean set correctly (' + lon + ')');
});
});
t.end();
});
};
@ -153,11 +154,55 @@ module.exports.tests.sanitize_layers = function(test, common) {
});
test('invalid layer', function(t) {
sanitize({ layers: 'test_layer', input: 'test', lat: 0, lon: 0 }, function( err, clean ){
var msg = 'invalid param \'layer\': must be one or more of geoname,osmnode,osmway,admin0,admin1,admin2,neighborhood';
var msg = 'invalid param \'layer\': must be one or more of geoname,osmnode,osmway,admin0,admin1,admin2,neighborhood,poi,admin';
t.equal(err, msg, 'invalid layer requested');
t.end();
});
});
test('poi (alias) layer', function(t) {
var poi_layers = ['geoname','osmnode','osmway'];
sanitize({ layers: 'poi', input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.deepEqual(clean.layers, poi_layers, 'poi layers set');
t.end();
});
});
test('admin (alias) layer', function(t) {
var admin_layers = ['admin0','admin1','admin2','neighborhood'];
sanitize({ layers: 'admin', input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.deepEqual(clean.layers, admin_layers, 'admin layers set');
t.end();
});
});
test('poi alias layer plus regular layers', function(t) {
var poi_layers = ['geoname','osmnode','osmway'];
var reg_layers = ['admin0', 'admin1'];
sanitize({ layers: 'poi,admin0,admin1', input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.deepEqual(clean.layers, reg_layers.concat(poi_layers), 'poi + regular layers');
t.end();
});
});
test('admin alias layer plus regular layers', function(t) {
var admin_layers = ['admin0','admin1','admin2','neighborhood'];
var reg_layers = ['geoname', 'osmway'];
sanitize({ layers: 'admin,geoname,osmway', input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.deepEqual(clean.layers, reg_layers.concat(admin_layers), 'admin + regular layers set');
t.end();
});
});
test('alias layer plus regular layers (no duplicates)', function(t) {
var poi_layers = ['geoname','osmnode','osmway'];
sanitize({ layers: 'poi,geoname,osmnode', input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.deepEqual(clean.layers, poi_layers, 'poi layers found (no duplicates)');
t.end();
});
});
test('multiple alias layers (no duplicates)', function(t) {
var alias_layers = ['geoname','osmnode','osmway','admin0','admin1','admin2','neighborhood'];
sanitize({ layers: 'poi,admin', input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.deepEqual(clean.layers, alias_layers, 'all layers found (no duplicates)');
t.end();
});
});
};
module.exports.tests.invalid_params = function(test, common) {
@ -197,7 +242,7 @@ module.exports.tests.middleware_success = function(test, common) {
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('SANTIZE /sanitise ' + name, testFunction);
return tape('SANTIZE /suggest ' + name, testFunction);
}
for( var testCase in module.exports.tests ){

82
test/unit/service/mget.js

@ -0,0 +1,82 @@
var setup = require('../../../service/mget'),
mockBackend = require('../mock/backend');
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) {
t.equal(typeof setup, 'function', 'setup is a function');
t.end();
});
};
// functionally test service
module.exports.tests.functional_success = function(test, common) {
var expected = [
{
value: 1,
center_point: { lat: 100.1, lon: -50.5 },
name: { default: 'test name1' },
admin0: 'country1', admin1: 'state1', admin2: 'city1'
},
{
value: 2,
center_point: { lat: 100.2, lon: -51.5 },
name: { default: 'test name2' },
admin0: 'country2', admin1: 'state2', admin2: 'city2'
}
];
test('valid query', function(t) {
var backend = mockBackend( 'client/doc/ok/1', function( cmd ){
t.deepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'a' } ] } }, 'correct backend command');
});
setup( backend, [ { _id: 123, _index: 'pelias', _type: 'a' } ], function(err, data) {
t.true(Array.isArray(data), 'returns an array');
data.forEach(function(d) {
t.true(typeof d === 'object', 'valid object');
});
t.deepEqual(data, expected, 'values correctly mapped')
t.end();
});
});
};
// functionally test service
module.exports.tests.functional_failure = function(test, common) {
test('invalid query', function(t) {
var invalid_queries = [
{ _id: 123, _index: 'pelias' },
{ _id: 123, _type: 'a' },
{ _index: 'pelias', _type: 'a' },
{ }
];
var backend = mockBackend( 'client/doc/fail/1', function( cmd ){
t.notDeepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'a' } ] } }, 'incorrect backend command');
});
invalid_queries.forEach(function(query) {
setup( backend, [ query ], function(err, data) {
t.equal(err, 'a backend error occurred','error passed to errorHandler');
t.equal(data, undefined, 'data is undefined');
});
});
t.end();
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('SERVICE /mget ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

82
test/unit/service/search.js

@ -0,0 +1,82 @@
var setup = require('../../../service/search'),
mockBackend = require('../mock/backend');
var example_valid_es_query = { body: { a: 'b' }, index: 'pelias' };
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) {
t.equal(typeof setup, 'function', 'setup is a function');
t.end();
});
};
// functionally test service
module.exports.tests.functional_success = function(test, common) {
var expected = [
{
value: 1,
center_point: { lat: 100.1, lon: -50.5 },
name: { default: 'test name1' },
admin0: 'country1', admin1: 'state1', admin2: 'city1'
},
{
value: 2,
center_point: { lat: 100.2, lon: -51.5 },
name: { default: 'test name2' },
admin0: 'country2', admin1: 'state2', admin2: 'city2'
}
];
test('valid ES query', function(t) {
var backend = mockBackend( 'client/search/ok/1', function( cmd ){
t.deepEqual(cmd, example_valid_es_query, 'no change to the command');
});
setup( backend, example_valid_es_query, function(err, data) {
t.true(Array.isArray(data), 'returns an array');
data.forEach(function(d) {
t.true(typeof d === 'object', 'valid object');
});
t.deepEqual(data, expected, 'values correctly mapped')
t.end();
});
});
};
// functionally test service
module.exports.tests.functional_failure = function(test, common) {
test('invalid ES query', function(t) {
var invalid_queries = [
{ },
{ foo: 'bar' }
];
var backend = mockBackend( 'client/search/fail/1', function( cmd ){
t.notDeepEqual(cmd, example_valid_es_query, 'incorrect backend command');
});
invalid_queries.forEach(function(query) {
setup( backend, [ query ], function(err, data) {
t.equal(err, 'a backend error occurred','error passed to errorHandler');
t.equal(data, undefined, 'data is undefined');
});
});
t.end();
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('SERVICE /search ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

79
test/unit/service/suggest.js

@ -0,0 +1,79 @@
var setup = require('../../../service/suggest'),
mockBackend = require('../mock/backend');
var example_valid_es_query = { body: { a: 'b' }, index: 'pelias' };
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) {
t.equal(typeof setup, 'function', 'setup is a function');
t.end();
});
};
// functionally test service
module.exports.tests.functional_success = function(test, common) {
var mockPayload = function(id){
return {
id: 'mocktype/mockid'+id,
geo: '101,-10.1'
}
};
var expected = [
{ value: 1, payload: mockPayload(1) },
{ value: 2, payload: mockPayload(2) }
];
test('valid ES query', function(t) {
var backend = mockBackend( 'client/suggest/ok/1', function( cmd ){
t.deepEqual(cmd, example_valid_es_query, 'no change to the command');
});
setup( backend, example_valid_es_query, function(err, data) {
t.true(Array.isArray(data), 'returns an array');
data.forEach(function(d) {
t.true(typeof d === 'object', 'valid object');
});
t.deepEqual(data, expected, 'values correctly mapped')
t.end();
});
});
};
// functionally test service
module.exports.tests.functional_failure = function(test, common) {
test('invalid ES query', function(t) {
var invalid_queries = [
{ },
{ foo: 'bar' }
];
var backend = mockBackend( 'client/suggest/fail/1', function( cmd ){
t.notDeepEqual(cmd, example_valid_es_query, 'incorrect backend command');
});
invalid_queries.forEach(function(query) {
setup( backend, [ query ], function(err, data) {
t.equal(err, 'a backend error occurred','error passed to errorHandler');
t.equal(data, undefined, 'data is undefined');
});
});
t.end();
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('SERVICE /suggest ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};
Loading…
Cancel
Save