diff --git a/README.md b/README.md index 0991c143..c3287227 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ The API recognizes the following properties under the top-level `api` key in you |`accessLog`|*no*||name of the format to use for access logs; may be any one of the [predefined values](https://github.com/expressjs/morgan#predefined-formats) in the `morgan` package. Defaults to `"common"`; if set to `false`, or an otherwise falsy value, disables access-logging entirely.| |`services`|*no*||service definitions for [point-in-polygon](https://github.com/pelias/pip-service), [libpostal](https://github.com/whosonfirst/go-whosonfirst-libpostal), [placeholder](https://github.com/pelias/placeholder), and [interpolation](https://github.com/pelias/interpolation) services. If missing (which is not recommended), the services will not be called.| |`defaultParameters.focus.point.lon`
`defaultParameters.focus.point.lat`|no | |default coordinates for focus point +|`targets.layers_by_source`
`targets.source_aliases`
`targets.layer_aliases`|no | |custom values for which `sources` and `layers` the API accepts ([more info](https://github.com/pelias/api/pull/1131)). A good starting configuration file includes this section (fill in the service and Elasticsearch hosts as needed): diff --git a/helper/TypeMapping.js b/helper/TypeMapping.js new file mode 100644 index 00000000..97d87a26 --- /dev/null +++ b/helper/TypeMapping.js @@ -0,0 +1,146 @@ +const _ = require('lodash'); +const elasticsearch = require('elasticsearch'); + +var TypeMapping = function(){ + + // A list of all sources + this.sources = []; + + // A list of alternate names for sources, mostly used to save typing + this.source_aliases = {}; + + // A list of all layers + this.layers = []; + + /* + * A list of all layers in each source. This is used for convenience elswhere + * and to determine when a combination of source and layer parameters is + * not going to match any records and will return no results. + */ + this.layers_by_source = {}; + + /* + * A list of layer aliases that can be used to support specific use cases + * (like coarse geocoding) * or work around the fact that different sources + * may have layers that mean the same thing but have a different name + */ + this.layer_aliases = {}; + + /* + * An object that contains all sources or aliases. The key is the source or alias, + * the value is either that source, or the canonical name for that alias if it's an alias. + */ + this.source_mapping = {}; + + /* + * An object that has a key for each possible layer or alias, + * and returns either that layer, or gall the layers in the alias + */ + this.layer_mapping = {}; +}; + +TypeMapping.addStandardTargetsToAliases = function(standard, aliases) { + var combined = _.extend({}, aliases); + standard.forEach(function(target) { + if (combined[target] === undefined) { + combined[target] = [target]; + } + }); + + return combined; +}; + +// source alias setter +TypeMapping.prototype.setSourceAliases = function( aliases ){ + this.source_aliases = aliases; +}; + +// layers-by-source alias setter +TypeMapping.prototype.setLayersBySource = function( lbs ){ + this.layers_by_source = lbs; +}; + +// layer alias setter +TypeMapping.prototype.setLayerAliases = function( aliases ){ + this.layer_aliases = aliases; +}; + +// generate mappings after setters have been run +TypeMapping.prototype.generateMappings = function(){ + this.sources = Object.keys( this.layers_by_source ); + this.source_mapping = TypeMapping.addStandardTargetsToAliases(this.sources, this.source_aliases); + this.layers = _.uniq(Object.keys(this.layers_by_source).reduce(function(acc, key) { + return acc.concat(this.layers_by_source[key]); + }.bind(this), [])); + this.layer_mapping = TypeMapping.addStandardTargetsToAliases(this.layers, this.layer_aliases); +}; + +// load values from targets block +TypeMapping.prototype.loadTargets = function( targetsBlock ){ + + if( !_.isObject(targetsBlock) ){ return; } + + // set values from targets block + this.setSourceAliases( targetsBlock.source_aliases || {} ); + this.setLayersBySource( targetsBlock.layers_by_source || {} ); + this.setLayerAliases( targetsBlock.layer_aliases || {} ); + + // generate the mappings + this.generateMappings(); +}; + +// load values from either pelias config file or from elasticsearch +TypeMapping.prototype.load = function( done ){ + + // load pelias config + const peliasConfigTargets = _.get( + require('pelias-config').generate(require('../schema')), + 'api.targets', {} + ); + + // load targets from config file + this.loadTargets( peliasConfigTargets ); + + // do not load values from elasticsearch + if( true !== peliasConfigTargets.auto_discover ){ + if( 'function' === typeof done ){ done(); } + return; + } + + if( 'function' === typeof done ){ done(); } + return; + + // load values from elasticsearch + + // create connection to elasticsearch + // const esclient = elasticsearch.Client(peliasConfig.esclient); + + // const query = { + // requestCache: true, + // preference: '_replica_first', + // timeout: '10s', + // body: { + // aggs: { + // sources: { + // terms: { + // field: 'source', + // size: 100 + // } + // }, + // layers: { + // terms: { + // field: 'layer', + // size: 100 + // } + // } + // }, + // size: 0 + // } + // }; + + // esclient.search( query, ( err, res ) => { + // console.error( err, res ); + // }); +}; + +module.exports = TypeMapping; diff --git a/helper/type_mapping.js b/helper/type_mapping.js index 41303583..987aa26a 100644 --- a/helper/type_mapping.js +++ b/helper/type_mapping.js @@ -1,86 +1,8 @@ -const _ = require('lodash'); +const TypeMapping = require('./TypeMapping'); -function addStandardTargetsToAliases(standard, aliases) { - var combined = _.extend({}, aliases); - standard.forEach(function(target) { - if (combined[target] === undefined) { - combined[target] = [target]; - } - }); +// instantiate a new type mapping +var tm = new TypeMapping(); +tm.load(); - return combined; -} - -/* - * Sources - */ - -// a list of all sources -var SOURCES = ['openstreetmap', 'openaddresses', 'geonames', 'whosonfirst']; - -/* - * A list of alternate names for sources, mostly used to save typing - */ -var SOURCE_ALIASES = { - 'osm': ['openstreetmap'], - 'oa': ['openaddresses'], - 'gn': ['geonames'], - 'wof': ['whosonfirst'] -}; - -/* - * Create an object that contains all sources or aliases. The key is the source or alias, - * the value is either that source, or the canonical name for that alias if it's an alias. - */ -var SOURCE_MAPPING = addStandardTargetsToAliases(SOURCES, SOURCE_ALIASES); - -/* - * Layers - */ - -/* - * A list of all layers in each source. This is used for convenience elswhere - * and to determine when a combination of source and layer parameters is - * not going to match any records and will return no results. - */ -var LAYERS_BY_SOURCE = { - openstreetmap: [ 'address', 'venue', 'street' ], - openaddresses: [ 'address' ], - geonames: [ 'country','macroregion', 'region', 'county','localadmin', - 'locality','borough', 'neighbourhood', 'venue' ], - whosonfirst: [ 'continent', 'empire', 'country', 'dependency', 'macroregion', 'region', - 'locality', 'localadmin', 'macrocounty', 'county', 'macrohood', 'borough', - 'neighbourhood', 'microhood', 'disputed', 'venue', 'postalcode', - 'continent', 'ocean', 'marinearea'] -}; - -/* - * A list of layer aliases that can be used to support specific use cases - * (like coarse geocoding) * or work around the fact that different sources - * may have layers that mean the same thing but have a different name - */ -var LAYER_ALIASES = { - 'coarse': [ 'continent', 'empire', 'country', 'dependency', 'macroregion', - 'region', 'locality', 'localadmin', 'macrocounty', 'county', 'macrohood', - 'borough', 'neighbourhood', 'microhood', 'disputed', 'postalcode', - 'continent', 'ocean', 'marinearea'] -}; - -// create a list of all layers by combining each entry from LAYERS_BY_SOURCE -var LAYERS = _.uniq(Object.keys(LAYERS_BY_SOURCE).reduce(function(acc, key) { - return acc.concat(LAYERS_BY_SOURCE[key]); -}, [])); - -/* - * Create the an object that has a key for each possible layer or alias, - * and returns either that layer, or all the layers in the alias - */ -var LAYER_MAPPING = addStandardTargetsToAliases(LAYERS, LAYER_ALIASES); - -module.exports = { - sources: SOURCES, - layers: LAYERS, - source_mapping: SOURCE_MAPPING, - layer_mapping: LAYER_MAPPING, - layers_by_source: LAYERS_BY_SOURCE -}; +// export singleton +module.exports = tm; \ No newline at end of file diff --git a/middleware/geocodeJSON.js b/middleware/geocodeJSON.js index 0ff2e303..60c74f68 100644 --- a/middleware/geocodeJSON.js +++ b/middleware/geocodeJSON.js @@ -47,10 +47,10 @@ function convertToGeocodeJSON(req, res, next, opts) { // OPTIONAL. Default: null. The attribution of the data. In case of multiple sources, // and then multiple attributions, can be an object with one key by source. // Can be a URI on the server, which outlines attribution details. - res.body.geocoding.attribution = url.format({ + res.body.geocoding.attribution = opts.config.attributionURL || url.format({ protocol: req.protocol, host: req.get('host'), - pathname: opts.basePath + 'attribution' + pathname: 'attribution' }); // OPTIONAL. Default: null. The query that has been issued to trigger the diff --git a/middleware/interpolate.js b/middleware/interpolate.js index 10c3f7e8..8507709a 100644 --- a/middleware/interpolate.js +++ b/middleware/interpolate.js @@ -2,6 +2,7 @@ const async = require('async'); const logger = require( 'pelias-logger' ).get( 'api' ); const source_mapping = require('../helper/type_mapping').source_mapping; const _ = require('lodash'); +const stable = require('stable'); /** example response from interpolation web service: @@ -116,7 +117,7 @@ function setup(service, should_execute) { // sort the results to ensure that addresses show up higher than street centroids if (_.has(res, 'data')) { - res.data.sort((a, b) => { + res.data = stable(res.data, (a, b) => { if (a.layer === 'address' && b.layer !== 'address') { return -1; } if (a.layer !== 'address' && b.layer === 'address') { return 1; } return 0; diff --git a/package.json b/package.json index 34af521a..2f361801 100644 --- a/package.json +++ b/package.json @@ -36,32 +36,33 @@ "node": ">=6.0.0" }, "dependencies": { + "@mapbox/geojson-extent": "^0.3.1", "addressit": "1.5.0", "async": "^2.0.0", "check-types": "^7.0.0", - "elasticsearch": "^14.2.1", + "elasticsearch": "^15.0.0", "elasticsearch-exceptions": "0.0.4", "express": "^4.8.8", "geojson": "^0.5.0", - "@mapbox/geojson-extent": "^0.3.1", "geolib": "^2.0.18", "iso-639-3": "^1.0.0", "iso3166-1": "^0.3.0", - "joi": "^12.0.0", + "joi": "^13.1.3", "locale": "^0.1.0", "lodash": "^4.17.4", "markdown": "0.5.0", "morgan": "^1.8.2", "pelias-categories": "1.2.0", - "pelias-config": "2.14.0", + "pelias-config": "3.0.2", "pelias-labels": "1.8.0", - "pelias-logger": "0.3.1", - "pelias-microservice-wrapper": "1.3.0", - "pelias-model": "5.3.2", + "pelias-logger": "0.4.2", + "pelias-microservice-wrapper": "1.4.0", + "pelias-model": "5.5.2", "pelias-query": "9.1.1", - "pelias-sorting": "1.1.0", + "pelias-sorting": "1.2.0", "predicates": "^2.0.0", - "retry": "^0.10.1", + "retry": "^0.12.0", + "stable": "^0.1.8", "stats-lite": "^2.0.4", "through2": "^2.0.3" }, @@ -71,7 +72,7 @@ "istanbul": "^0.4.2", "jshint": "^2.5.6", "nsp": "^3.0.0", - "pelias-mock-logger": "1.2.0", + "pelias-mock-logger": "1.3.0", "precommit-hook": "^3.0.0", "proxyquire": "^2.0.0", "semantic-release": "^15.1.4", @@ -88,6 +89,7 @@ "test" ], "release": { - "branch": "production" + "branch": "production", + "success": [] } } diff --git a/test/unit/helper/TypeMapping.js b/test/unit/helper/TypeMapping.js new file mode 100644 index 00000000..b4e45475 --- /dev/null +++ b/test/unit/helper/TypeMapping.js @@ -0,0 +1,208 @@ +const _ = require('lodash'); +const TypeMapping = require('../../../helper/TypeMapping'); + +module.exports.tests = {}; + +module.exports.tests.interface = function(test) { + test('valid interface', function(t) { + t.equal(typeof TypeMapping, 'function', 'TypeMapping is a function'); + + t.equal(typeof TypeMapping.addStandardTargetsToAliases, 'function', 'addStandardTargetsToAliases() is a function'); + t.equal(typeof TypeMapping.prototype.setSourceAliases, 'function', 'setSourceAliases() is a function'); + + t.equal(typeof TypeMapping.prototype.setLayersBySource, 'function', 'setLayersBySource() is a function'); + t.equal(typeof TypeMapping.prototype.setLayerAliases, 'function', 'setLayerAliases() is a function'); + + t.equal(typeof TypeMapping.prototype.generateMappings, 'function', 'generateMappings() is a function'); + + t.equal(typeof TypeMapping.prototype.loadTargets, 'function', 'loadTargets() is a function'); + t.equal(typeof TypeMapping.prototype.load, 'function', 'load() is a function'); + + t.end(); + }); +}; + +module.exports.tests.constructor = function(test) { + test('constructor', function(t) { + + var doc = new TypeMapping(); + + // initial values + t.deepEqual(doc.sources, [], 'initial value'); + t.deepEqual(doc.source_aliases, {}, 'initial value'); + t.deepEqual(doc.layers, [], 'initial value'); + t.deepEqual(doc.layers_by_source, {}, 'initial value'); + t.deepEqual(doc.layer_aliases, {}, 'initial value'); + t.deepEqual(doc.source_mapping, {}, 'initial value'); + t.deepEqual(doc.layer_mapping, {}, 'initial value'); + + t.end(); + }); +}; + +module.exports.tests.addStandardTargetsToAliases = function(test) { + test('static method addStandardTargetsToAliases', function(t) { + + var aliases = { test: ['test2'] }; + + t.deepEqual( + TypeMapping.addStandardTargetsToAliases([], aliases), + { test: ['test2'] } + ); + t.deepEqual(aliases, aliases, 'aliases object not mutated'); + + t.deepEqual( + TypeMapping.addStandardTargetsToAliases(['test'], aliases), + { test: ['test2'] }, + 'not modified' + ); + t.deepEqual(aliases, aliases, 'aliases object not mutated'); + + t.deepEqual( + TypeMapping.addStandardTargetsToAliases(['baz'], aliases), + { test: ['test2'], baz: ['baz'] } + ); + t.deepEqual(aliases, aliases, 'aliases object not mutated'); + + t.deepEqual( + TypeMapping.addStandardTargetsToAliases(['baz','boo'], aliases), + { test: ['test2'], baz: ['baz'], boo: ['boo'] } + ); + t.deepEqual(aliases, aliases, 'aliases object not mutated'); + + t.end(); + + }); +}; + +module.exports.tests.setSourceAliases = function(test) { + test('setter setSourceAliases', function(t) { + var tm = new TypeMapping(); + t.deepEqual(tm.source_aliases, {}); + tm.setSourceAliases({ foo: ['foo', 'bar'] }); + t.deepEqual(tm.source_aliases, { foo: ['foo', 'bar'] }); + t.end(); + }); +}; + +module.exports.tests.setLayersBySource = function(test) { + test('setter setLayersBySource', function(t) { + var tm = new TypeMapping(); + t.deepEqual(tm.layers_by_source, {}); + tm.setLayersBySource({ foo: ['foo', 'bar'] }); + t.deepEqual(tm.layers_by_source, { foo: ['foo', 'bar'] }); + t.end(); + }); +}; + +module.exports.tests.setLayerAliases = function(test) { + test('setter setLayerAliases', function(t) { + var tm = new TypeMapping(); + t.deepEqual(tm.layer_aliases, {}); + tm.setLayerAliases({ foo: ['foo', 'bar'] }); + t.deepEqual(tm.layer_aliases, { foo: ['foo', 'bar'] }); + t.end(); + }); +}; + +module.exports.tests.generateMappings = function(test) { + test('generateMappings - no-op', function(t) { + var tm = new TypeMapping(); + t.deepEqual(tm.sources, []); + t.deepEqual(tm.source_mapping, {}); + t.deepEqual(tm.layers, []); + t.deepEqual(tm.layer_mapping, {}); + tm.generateMappings(); + t.deepEqual(tm.sources, []); + t.deepEqual(tm.source_mapping, {}); + t.deepEqual(tm.layers, []); + t.deepEqual(tm.layer_mapping, {}); + t.end(); + }); + test('generateMappings - sources', function(t) { + var tm = new TypeMapping(); + tm.layers_by_source = { foo: ['foo'], faz: ['faz'] }; + tm.generateMappings(); + t.deepEqual(tm.sources, ['foo', 'faz']); + t.end(); + }); + test('generateMappings - source_mapping', function(t) { + var tm = new TypeMapping(); + tm.layers_by_source = { foo: ['foo'], faz: ['faz'] }; + tm.source_aliases = { foo: ['foo','f'], bar: ['bar', 'b'], baz: ['baz'] }; + tm.generateMappings(); + t.deepEqual(tm.source_mapping, { foo: ['foo', 'f'], bar: ['bar', 'b'], baz: ['baz'], faz: ['faz'] }); + t.end(); + }); + test('generateMappings - layers', function(t) { + var tm = new TypeMapping(); + tm.layers_by_source = { foo: ['foo'], faz: ['faz'] }; + tm.generateMappings(); + t.deepEqual(tm.layers, ['foo','faz']); + t.end(); + }); + test('generateMappings - layer_mapping', function(t) { + var tm = new TypeMapping(); + tm.layers_by_source = { foo: ['foo'], faz: ['faz'] }; + tm.layer_aliases = { foo: ['foo','f'], bar: ['bar', 'b'], baz: ['baz'] }; + tm.generateMappings(); + t.deepEqual(tm.layer_mapping, { foo: ['foo', 'f'], bar: ['bar', 'b'], baz: ['baz'], faz: ['faz'] }); + t.end(); + }); +}; + +module.exports.tests.loadTargets = function(test) { + test('loadTargets - undefined', function(t) { + var tm = new TypeMapping(); + tm.loadTargets(); + t.deepEqual(tm.sources, []); + t.deepEqual(tm.source_mapping, {}); + t.deepEqual(tm.layers, []); + t.deepEqual(tm.layer_mapping, {}); + t.end(); + }); + test('loadTargets', function(t) { + var tm = new TypeMapping(); + tm.loadTargets({ + source_aliases: { source1: ['s1', 's2'], source2: ['s3', 's4'] }, + layers_by_source: { source1: ['layer1', 'layer3'], source2: ['layer2'] }, + layer_aliases: { layer1: ['l1', 'l2'], layer2: ['l3', 'l4'] }, + }); + t.deepEqual(tm.sources, [ 'source1', 'source2' ]); + t.deepEqual(tm.source_mapping, { source1: [ 's1', 's2' ], source2: [ 's3', 's4' ] }); + t.deepEqual(tm.layers, [ 'layer1', 'layer3', 'layer2' ]); + t.deepEqual(tm.layer_mapping, { layer1: [ 'l1', 'l2' ], layer2: [ 'l3', 'l4' ], layer3: [ 'layer3' ] }); + t.end(); + }); +}; + +module.exports.tests.load = function(test) { + test('load from pelias config', function(t) { + var tm = new TypeMapping(); + tm.load(() => { + + // load pelias config + const expected = _.get( + require('pelias-config').generate(require('../../../schema')), + 'api.targets', {} + ); + + // values copied from config + t.deepEqual(tm.layers_by_source, expected.layers_by_source || {}); + t.deepEqual(tm.source_aliases, expected.source_aliases || {}); + t.deepEqual(tm.layer_aliases, expected.layer_aliases || {}); + t.end(); + }); + }); +}; + +module.exports.all = function (tape, common) { + + function test(name, testFunction) { + return tape('TypeMapping: ' + name, testFunction); + } + + for( var testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; diff --git a/test/unit/helper/type_mapping.js b/test/unit/helper/type_mapping.js index ec8cd379..25b06d0f 100644 --- a/test/unit/helper/type_mapping.js +++ b/test/unit/helper/type_mapping.js @@ -4,18 +4,31 @@ var type_mapping = require('../../../helper/type_mapping'); module.exports.tests = {}; module.exports.tests.interfaces = function(test, common) { - test('basic layer mapping', function(t) { - t.deepEquals(type_mapping.layer_mapping.venue, ['venue']); - t.deepEquals(type_mapping.layer_mapping.address, ['address']); + + test('complete sources', function(t) { + t.deepEquals(type_mapping.sources, [ 'openstreetmap', 'openaddresses', 'geonames', 'whosonfirst' ]); t.end(); }); - test('alias layer mapping', function(t) { - t.deepEquals(type_mapping.layer_mapping.coarse, - [ 'continent', 'empire', 'country', 'dependency', 'macroregion', - 'region', 'locality', 'localadmin', 'macrocounty', 'county', 'macrohood', - 'borough', 'neighbourhood', 'microhood', 'disputed', 'postalcode', - 'continent', 'ocean', 'marinearea']); + test('complete layers', function(t) { + t.deepEquals(type_mapping.layers, [ 'address', 'venue', 'street', 'country', 'macroregion', + 'region', 'county', 'localadmin', 'locality', 'borough', 'neighbourhood', 'continent', + 'empire', 'dependency', 'macrocounty', 'macrohood', 'microhood', 'disputed', + 'postalcode', 'ocean', 'marinearea' ]); + t.end(); + }); + + test('complete source mapping', function(t) { + t.deepEquals(type_mapping.source_mapping, { + osm: [ 'openstreetmap' ], + oa: [ 'openaddresses' ], + gn: [ 'geonames' ], + wof: [ 'whosonfirst' ], + openstreetmap: [ 'openstreetmap' ], + openaddresses: [ 'openaddresses' ], + geonames: [ 'geonames' ], + whosonfirst: [ 'whosonfirst' ] + }); t.end(); }); @@ -30,6 +43,66 @@ module.exports.tests.interfaces = function(test, common) { t.deepEquals(type_mapping.source_mapping.wof, ['whosonfirst']); t.end(); }); + + test('complete layer mapping', function(t) { + t.deepEquals(type_mapping.layer_mapping, { + coarse: [ 'continent', 'empire', 'country', 'dependency', 'macroregion', 'region', + 'locality', 'localadmin', 'macrocounty', 'county', 'macrohood', 'borough', + 'neighbourhood', 'microhood', 'disputed', 'postalcode', 'continent', 'ocean', 'marinearea' + ], + address: [ 'address' ], + venue: [ 'venue' ], + street: [ 'street' ], + country: [ 'country' ], + macroregion: [ 'macroregion' ], + region: [ 'region' ], + county: [ 'county' ], + localadmin: [ 'localadmin' ], + locality: [ 'locality' ], + borough: [ 'borough' ], + neighbourhood: [ 'neighbourhood' ], + continent: [ 'continent' ], + empire: [ 'empire' ], + dependency: [ 'dependency' ], + macrocounty: [ 'macrocounty' ], + macrohood: [ 'macrohood' ], + microhood: [ 'microhood' ], + disputed: [ 'disputed' ], + postalcode: [ 'postalcode' ], + ocean: [ 'ocean' ], + marinearea: [ 'marinearea' ] + }); + t.end(); + }); + + test('basic layer mapping', function(t) { + t.deepEquals(type_mapping.layer_mapping.venue, ['venue']); + t.deepEquals(type_mapping.layer_mapping.address, ['address']); + t.end(); + }); + + test('alias layer mapping', function(t) { + t.deepEquals(type_mapping.layer_mapping.coarse, + [ 'continent', 'empire', 'country', 'dependency', 'macroregion', + 'region', 'locality', 'localadmin', 'macrocounty', 'county', 'macrohood', + 'borough', 'neighbourhood', 'microhood', 'disputed', 'postalcode', + 'continent', 'ocean', 'marinearea']); + t.end(); + }); + + test('complete layers by source', function(t) { + t.deepEquals(type_mapping.layers_by_source, { + openstreetmap: [ 'address', 'venue', 'street' ], + openaddresses: [ 'address' ], + geonames: [ 'country', 'macroregion', 'region', 'county', 'localadmin', + 'locality', 'borough', 'neighbourhood', 'venue' ], + whosonfirst: [ 'continent', 'empire', 'country', 'dependency', 'macroregion', + 'region', 'locality', 'localadmin', 'macrocounty', 'county', 'macrohood', + 'borough', 'neighbourhood', 'microhood', 'disputed', 'venue', 'postalcode', + 'continent', 'ocean', 'marinearea' ] + }); + t.end(); + }); }; module.exports.all = function (tape, common) { diff --git a/test/unit/middleware/interpolate.js b/test/unit/middleware/interpolate.js index 3602592c..537c7444 100644 --- a/test/unit/middleware/interpolate.js +++ b/test/unit/middleware/interpolate.js @@ -277,6 +277,81 @@ module.exports.tests.success_conditions = (test, common) => { }); + test('results should be sorted first by address/non-address. previous ordering should otherwise be maintained via a stable sort', t => { + const service = (req, res, callback) => { + // results 5 and 7 will have interpolated results returned + // this is to ensure results are re-sorted to move the addresses first + if (res.id === 5 || res.id === 7) { + callback(null, { + properties: { + number: 17, + source: 'Source Abbr 1', + source_id: 'source 1 source id', + lat: 12.121212, + lon: 21.212121 + } + }); + } else { + // return empty results in most cases + callback(null, {}); + } + }; + + const logger = require('pelias-mock-logger')(); + + const controller = proxyquire('../../../middleware/interpolate', { + 'pelias-logger': logger + })(service, () => true); + + const req = { + clean: { + parsed_text: 'this is req.clean.parsed_text' + } + }; + const res = {}; + + // helper method to generate test results which default to streets + function generateTestStreets(id) { + return { + id: id+1, + layer: 'street', + name: { default: `name ${id+1}` }, + address_parts: {}, + source_id: 'original source_id' + }; + } + + // generate a set of street results of desired size + // NOTE: this set must be of 11 elements or greater + // Node.js uses stable insertion sort for arrays of 10 or fewer elements, + // but _unstable_ QuickSort for larger arrays + const resultCount = 11; + const sequence_array = Array.from(new Array(resultCount),(val,index)=>index); + res.data = sequence_array.map(generateTestStreets); + + controller(req, res, () => { + t.notOk(logger.hasErrorMessages(), 'there shouldn\'t be any error messages'); + + const results = res.data; + + t.equals(results.length, results.length, 'correct number of results should be returned'); + t.equals(results[0].layer, 'address', 'first result should be interpolated address'); + t.equals(results[1].layer, 'address', 'second result should be interpolated address'); + + // iterate through all remaining records, ensuring their ids are increasing, + // as was the case when the set of streets was originally generated + let previous_id; + for (let i = 2; i < results.length; i++) { + if (previous_id) { + t.ok(results[i].id > previous_id, `id ${results[i].id} should be higher than ${previous_id}, to ensure sort is stable`); + } + previous_id = results[i].id; + } + + t.end(); + }); + }); + test('service call returning error should not map in interpolated results for non-errors', t => { const service = (req, res, callback) => { if (res.id === 1) { diff --git a/test/unit/run.js b/test/unit/run.js index 52b6e519..404cd86b 100644 --- a/test/unit/run.js +++ b/test/unit/run.js @@ -36,6 +36,7 @@ var tests = [ require('./helper/geojsonify_place_details'), require('./helper/geojsonify'), require('./helper/logging'), + require('./helper/TypeMapping'), require('./helper/type_mapping'), require('./helper/stackTraceLine'), require('./middleware/access_log'),