From e6e68fa536722d68b2e9e1eaa87a7e3c30431437 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Tue, 3 Jul 2018 14:24:53 +0200 Subject: [PATCH] feat(aliases): allow aliases for address fields --- Document.js | 48 ++++++++++++++- README.md | 1 + test/document/address.js | 112 +++++++++++++++++++++++++++++----- test/document/toESDocument.js | 57 +++++++++++++++++ 4 files changed, 200 insertions(+), 18 deletions(-) diff --git a/Document.js b/Document.js index 030ff2a..a4237d5 100644 --- a/Document.js +++ b/Document.js @@ -370,16 +370,58 @@ Document.prototype.clearAllParents = function() { // address Document.prototype.setAddress = function( prop, value ){ + validate.type('string', value); + validate.truthy(value); validate.property(addressFields, prop); + + if( Array.isArray( this.address_parts[ prop ] ) ){ + this.address_parts[ prop ][ 0 ] = value; + } else { + this.address_parts[ prop ] = value; + } + + return this; +}; + +Document.prototype.setAddressAlias = function( prop, value ){ + validate.type('string', value); validate.truthy(value); + validate.property(addressFields, prop); + + // is this the first time setting this prop? ensure it's an array + if( !this.hasAddress( prop ) ){ + this.address_parts[ prop ] = []; + } + + // is casting required to convert a scalar field to an array? + else if( 'string' === typeof this.address_parts[ prop ] ){ + var stringValue = this.address_parts[ prop ]; + this.address_parts[ prop ] = [ stringValue ]; + } + + // is the array empty? ie. no prior call to setAddress() + // in this case we will also set element 0 (the element used for display) + if( !this.address_parts[ prop ].length ){ + this.setAddress( prop, value ); + } + + // set the alias as the second, third, fourth, etc value in the array + this.address_parts[ prop ].push( value ); - this.address_parts[ prop ] = value; return this; }; Document.prototype.getAddress = function( prop ){ - return this.address_parts[ prop ]; + return Array.isArray( this.address_parts[ prop ] ) ? + this.address_parts[ prop ][ 0 ] : + this.address_parts[ prop ]; +}; + +Document.prototype.getAddressAliases = function( prop ){ + return Array.isArray( this.address_parts[ prop ] ) ? + this.address_parts[ prop ].slice( 1 ) : + []; }; Document.prototype.hasAddress = function( prop ){ @@ -387,7 +429,7 @@ Document.prototype.hasAddress = function( prop ){ }; Document.prototype.delAddress = function( prop ){ - if( this.hasName( prop ) ){ + if( this.hasAddress( prop ) ){ delete this.address_parts[ prop ]; return true; } diff --git a/README.md b/README.md index 886a0d7..dda49fd 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ var poi = new Document( 'geoname', 'venue', 1003 ) .addParent( 'neighbourhood', 'Shoreditch', '2002' ) .setAddress( 'number', '10' ) .setAddress( 'street', 'pelias place' ) + .setAddressAlias( 'street', 'pelias pl' ) .addCategory( 'foo' ) .addCategory( 'bar' ) .removeCategory( 'foo' ) diff --git a/test/document/address.js b/test/document/address.js index 68f8ed1..197e893 100644 --- a/test/document/address.js +++ b/test/document/address.js @@ -20,21 +20,92 @@ module.exports.tests.setAddress = function(test) { t.equal(doc.address_parts.zip, 'bar', 'setter works'); t.end(); }); - test('setAddress - validate', function(t) { - var doc = new Document('mysource','mylayer','myid'); - t.throws( doc.setAddress.bind(doc, 1), null, 'invalid type' ); - t.throws( doc.setAddress.bind(doc, ''), null, 'invalid length' ); - t.throws( doc.setAddress.bind(doc, 'foo', 1), null, 'invalid property' ); - t.throws( doc.setAddress.bind(doc, '4', 2), null, 'invalid property' ); - t.throws( doc.setAddress.bind(doc, 'zip', 2), null, 'invalid property' ); - t.throws( doc.setAddress.bind(doc, 'unit', 2), null, 'invalid property' ); - t.throws( doc.setAddress.bind(doc, 'street', true), null, 'invalid property' ); - t.throws( doc.setAddress.bind(doc, 'street', null), null, 'invalid property' ); - t.throws( doc.setAddress.bind(doc, 'street', '\n'), null, 'invalid property' ); - t.equal(doc.address_parts.street, undefined, 'property unchanged'); - t.doesNotThrow( doc.setAddress.bind(doc, 'zip', 'foo'), null, 'invalid property' ); - t.doesNotThrow( doc.setAddress.bind(doc, 'unit', 'foo'), null, 'invalid property' ); - t.doesNotThrow( doc.setAddress.bind(doc, 'street', '1'), null, 'invalid property' ); + test('setAddress - validate key', function(t) { + var doc = new Document('mysource','mylayer','myid'); + t.throws( doc.setAddress.bind(doc,1), null, 'invalid type' ); + t.throws( doc.setAddress.bind(doc,''), null, 'invalid length' ); + t.throws( doc.setAddress.bind(doc,' '), null, 'invalid length' ); + t.throws( doc.setAddress.bind(doc,null), null, 'invalid length' ); + t.equal(doc.getAddress('test'), undefined, 'property not set'); + t.end(); + }); + test('setAddress - validate key in property list', function(t) { + var doc = new Document('mysource','mylayer','myid'); + t.throws( doc.setAddress.bind(doc, 'invalid', 'foo'), null, 'invalid property' ); + t.doesNotThrow( doc.setAddress.bind(doc, 'name', 'foo'), null, 'valid property' ); + t.doesNotThrow( doc.setAddress.bind(doc, 'number', 'foo'), null, 'valid property' ); + t.doesNotThrow( doc.setAddress.bind(doc, 'unit', 'foo'), null, 'valid property' ); + t.doesNotThrow( doc.setAddress.bind(doc, 'street', '1'), null, 'valid property' ); + t.doesNotThrow( doc.setAddress.bind(doc, 'zip', 'foo'), null, 'valid property' ); + t.end(); + }); + test('setAddress - validate val', function(t) { + var doc = new Document('mysource','mylayer','myid'); + t.throws( doc.setAddress.bind(doc,'zip',1), null, 'invalid value' ); + t.throws( doc.setAddress.bind(doc,'zip',''), null, 'invalid value' ); + t.throws( doc.setAddress.bind(doc,'zip',' '), null, 'invalid value' ); + t.throws( doc.setAddress.bind(doc,'zip',null), null, 'invalid value' ); + t.throws( doc.setAddress.bind(doc,'zip','\t'), null, 'invalid value' ); + t.equal(doc.getAddress('test'), undefined, 'property not set'); + t.end(); + }); +}; + +module.exports.tests.getAddressAliases = function(test) { + test('getAddressAliases', function(t) { + var doc = new Document('mysource','mylayer','myid'); + t.deepEqual(doc.getAddressAliases('zip'), [], 'getter works'); + doc.address_parts = { 'zip': 'bar' }; + t.deepEqual(doc.getAddressAliases('zip'), [], 'getter works'); + doc.address_parts = { 'zip': ['bar'] }; + t.deepEqual(doc.getAddressAliases('zip'), [], 'getter works'); + doc.address_parts = { 'zip': ['bar','baz','boo'] }; + t.deepEqual(doc.getAddressAliases('zip'), ['baz','boo'], 'getter works'); + t.end(); + }); +}; + +module.exports.tests.setAddressAlias = function(test) { + test('setAddressAlias - no prior call to setAddress', function(t) { + var doc = new Document('mysource','mylayer','myid'); + t.equal(doc.setAddressAlias('zip','bar'), doc, 'chainable'); + t.equal(doc.setAddressAlias('zip','baz'), doc, 'chainable'); + t.equal(doc.address_parts.zip[0], 'bar', 'setter works'); + t.equal(doc.address_parts.zip[1], 'bar', 'setter works'); + t.equal(doc.address_parts.zip[2], 'baz', 'setter works'); + t.equal(doc.getAddress('zip'), 'bar', 'name set'); + t.deepEqual(doc.getAddressAliases('zip'), ['bar','baz'], 'aliases set'); + t.end(); + }); + test('setAddressAlias', function(t) { + var doc = new Document('mysource','mylayer','myid'); + t.equal(doc.setAddress('zip','bar'), doc, 'chainable'); + t.equal(doc.setAddressAlias('zip','baz'), doc, 'chainable'); + t.equal(doc.setAddressAlias('zip','boo'), doc, 'chainable'); + t.equal(doc.address_parts.zip[0], 'bar', 'setter works'); + t.equal(doc.address_parts.zip[1], 'baz', 'setter works'); + t.equal(doc.address_parts.zip[2], 'boo', 'setter works'); + t.equal(doc.getAddress('zip'), 'bar', 'name set'); + t.deepEqual(doc.getAddressAliases('zip'), ['baz','boo'], 'aliases set'); + t.end(); + }); + test('setAddressAlias - validate key', function(t) { + var doc = new Document('mysource','mylayer','myid'); + t.throws( doc.setAddressAlias.bind(doc,1), null, 'invalid type' ); + t.throws( doc.setAddressAlias.bind(doc,''), null, 'invalid length' ); + t.throws( doc.setAddressAlias.bind(doc,' '), null, 'invalid length' ); + t.throws( doc.setAddressAlias.bind(doc,null), null, 'invalid length' ); + t.deepEqual(doc.getAddressAliases('test'), [], 'property not set'); + t.end(); + }); + test('setAddressAlias - validate val', function(t) { + var doc = new Document('mysource','mylayer','myid'); + t.throws( doc.setAddressAlias.bind(doc,'zip',1), null, 'invalid value' ); + t.throws( doc.setAddressAlias.bind(doc,'zip',''), null, 'invalid value' ); + t.throws( doc.setAddressAlias.bind(doc,'zip',' '), null, 'invalid value' ); + t.throws( doc.setAddressAlias.bind(doc,'zip',null), null, 'invalid value' ); + t.throws( doc.setAddressAlias.bind(doc,'zip','\t'), null, 'invalid value' ); + t.deepEqual(doc.getAddressAliases('test'), [], 'property not set'); t.end(); }); }; @@ -49,6 +120,17 @@ module.exports.tests.hasAddress = function(test) { }); }; +module.exports.tests.delAddress = function(test) { + test('delAddress', function(t) { + var doc = new Document('mysource','mylayer','myid'); + t.equal(doc.delAddress('zip'), false, 'deller works'); + doc.address_parts.zip = 'bar'; + t.equal(doc.delAddress('zip'), true, 'deller works'); + t.equal(doc.address_parts.zip, undefined, 'deller works'); + t.end(); + }); +}; + module.exports.all = function (tape, common) { function test(name, testFunction) { diff --git a/test/document/toESDocument.js b/test/document/toESDocument.js index 00e4f0c..78b44f8 100644 --- a/test/document/toESDocument.js +++ b/test/document/toESDocument.js @@ -132,6 +132,63 @@ module.exports.tests.toESDocument = function(test) { }); + test('toESDocumentWithAddressAliases', function(t) { + var Document = proxyquire('../../Document', { 'pelias-config': fakeConfig }); + + var doc = new Document('mysource','mylayer','myid'); + doc.setAddress('name', 'address name'); + doc.setAddress('number', 'address number'); + doc.setAddressAlias('street', 'astreet'); + doc.setAddress('street', 'address street'); + doc.setAddress('zip', 'address zip'); + doc.setAddressAlias('zip', 'azip'); + doc.setAddress('unit', 'address unit'); + + var esDoc = doc.toESDocument(); + + var expected = { + _index: 'pelias', + _type: 'mylayer', + _id: 'myid', + data: { + source: 'mysource', + layer: 'mylayer', + source_id: 'myid', + address_parts: { + name: 'address name', + number: 'address number', + street: ['address street','astreet'], + zip: ['address zip','azip'], + unit: 'address unit' + }, + name: {}, + phrase: {} + } + }; + + t.deepEqual(esDoc, expected, 'creates correct elasticsearch document'); + t.end(); + }); + + test('unset properties should not output in toESDocument', (t) => { + const Document = proxyquire('../../Document', { 'pelias-config': fakeConfig }); + + const esDoc = new Document('mysource','mylayer','myid').toESDocument(); + + // test that empty arrays/object are stripped from the doc before sending it + // downstream to elasticsearch. + t.false(esDoc.data.hasOwnProperty('address_parts'), 'does not include empty top-level maps'); + t.false(esDoc.data.hasOwnProperty('category'), 'does not include empty top-level arrays'); + t.false(esDoc.data.hasOwnProperty('parent'), 'does not include empty parent arrays'); + t.false(esDoc.data.hasOwnProperty('bounding_box'), 'should not include bounding_box'); + t.false(esDoc.data.hasOwnProperty('center_point'), 'should not include center'); + t.false(esDoc.data.hasOwnProperty('population'), ' should not include population'); + t.false(esDoc.data.hasOwnProperty('popularity'), ' should not include popularity'); + t.false(esDoc.data.hasOwnProperty('polygon'), ' should not include polygon'); + t.end(); + + }); + }; module.exports.tests.toESDocumentWithCustomConfig = function(test) {