data-shaper
Advanced tools
Comparing version 0.1.4 to 0.1.5
@@ -44,6 +44,70 @@ 'use strict'; | ||
/** | ||
* Get reverse reference data. Reverse references are denoted by | ||
* parentheses containing the field to use when looking up and the | ||
* field from the current object to get the value for. | ||
* | ||
* Example: employeeDetails(employeeId=id) | ||
* a^ b^ c^ ^d | ||
* | ||
* a) collection that has the data we want | ||
* b) field in the collection to query on | ||
* c) either a single or a double equation sign: | ||
* = meaning it's a one-to-many reference (will return array of ids) | ||
* == meaning it's a direct reverse reference (will return single id) | ||
* d) field in the current object being referred from the collection | ||
* | ||
* @param {string} reference | ||
* @return {object} Reference data | ||
*/ | ||
function getReverseReferenceData(reference) { | ||
var data = reference.match(/^([A-z0-9-_]+)\(([A-z0-9-_]+)(={1,2})([A-z0-9-_]+)\)$/); | ||
if (data === null) { | ||
return data; | ||
} | ||
return { | ||
collection: data[1], | ||
referring: data[2], | ||
referred: data[4], | ||
oneToMany: data[3] === '=' | ||
}; | ||
} | ||
/** | ||
* Returns whether the given reference is a one-to-many relation or not | ||
* | ||
* @param {object|string} reference | ||
* @return {boolean} | ||
*/ | ||
function isOneToMany(reference) { | ||
if (reference.reference) { | ||
reference = reference.reference; | ||
} | ||
var refData = getReverseReferenceData(reference); | ||
return refData !== null && refData.oneToMany; | ||
} | ||
/** | ||
* Given a substring, will return a function that when called with a string determines | ||
* if that string contains the substring. | ||
* | ||
* @param {string} substr Substring to match | ||
* @return {function} | ||
*/ | ||
function startsWith(substr) { | ||
return function(str) { | ||
return str.indexOf(substr) === 0; | ||
}; | ||
} | ||
module.exports = { | ||
splitReference: splitReference, | ||
getPartOfReference: getPartOfReference, | ||
hashFetchDataCall: hashFetchDataCall | ||
hashFetchDataCall: hashFetchDataCall, | ||
getReverseReferenceData: getReverseReferenceData, | ||
isOneToMany: isOneToMany, | ||
startsWith: startsWith | ||
}; |
'use strict'; | ||
var async = require('async'); | ||
var merge = require('lodash.merge'); | ||
var helpers = require('./helpers'); | ||
var splitReference = helpers.splitReference; | ||
var startsWith = helpers.startsWith; | ||
function createFragmentHandler(options, shape, id, cb) { | ||
return function handleFragment(fetchErr, fragmentData) { | ||
if (fetchErr) { | ||
return cb(fetchErr); | ||
} | ||
// We got null back from fetchData. Create data for fragment | ||
// with null value and pass it to the callback | ||
if (fragmentData === null) { | ||
var nullFragmentData = {}; | ||
nullFragmentData[shape.collectionName + '::' + id] = null; | ||
return cb(null, { | ||
data: nullFragmentData, | ||
shape: shape, | ||
id: id | ||
}); | ||
} | ||
// Shape the data for the fragment | ||
options.shapeData(fragmentData, shape, options, function(err, shapedFragmentData) { | ||
if (err) { | ||
return cb(err); | ||
} | ||
cb(err, { | ||
data: shapedFragmentData, | ||
shape: shape, | ||
id: id | ||
}); | ||
}); | ||
}; | ||
} | ||
/** | ||
@@ -20,3 +58,2 @@ * Resolves a property specified using a shape fragment | ||
var fetchData = options.fetchData; | ||
var shapeData = options.shapeData; | ||
@@ -31,28 +68,12 @@ var reference = fragment.reference; | ||
// Resolve the value for the reference | ||
resolveValue(data, reference, options, function(resolveErr, id) { | ||
resolveValue(data, reference, options, function(resolveErr, value) { | ||
if (resolveErr) { | ||
return callback(resolveErr); | ||
callback(resolveErr); | ||
return; | ||
} | ||
// Fetch the data for the shape | ||
fetchData(id, propertyName, function(fetchErr, fragmentData) { | ||
if (fetchErr) { | ||
return callback(fetchErr); | ||
} | ||
// We got null back from fetchData. Create data for fragment | ||
// with null value and pass it to the callback | ||
if (fragmentData === null) { | ||
var nullFragmentData = {}; | ||
nullFragmentData[shape.collectionName + '::' + id] = null; | ||
return callback(null, { | ||
data: nullFragmentData, | ||
shape: shape, | ||
id: id | ||
}); | ||
} | ||
// Shape the data for the fragment | ||
shapeData(fragmentData, shape, options, function(err, shapedFragmentData) { | ||
if (typeof value === 'object') { | ||
async.map(Object.keys(value), function(id, cb) { | ||
createFragmentHandler(options, shape, id, cb)(null, value[id]); | ||
}, function(err, res) { | ||
if (err) { | ||
@@ -62,9 +83,20 @@ return callback(err); | ||
callback(err, { | ||
data: shapedFragmentData, | ||
var fragmentData = merge.apply(null, res.map(function(item) { | ||
return item.data; | ||
})); | ||
var entityFilter = startsWith(fragment.shape.collectionName + '::'); | ||
callback(null, { | ||
data: fragmentData, | ||
shape: shape, | ||
id: id | ||
id: Object.keys(fragmentData).filter(entityFilter).map(function(key) { | ||
return fragmentData[key] && fragmentData[key].id; | ||
}).filter(Boolean) | ||
}); | ||
}); | ||
}); | ||
return; | ||
} | ||
// Fetch the data for the shape | ||
fetchData(value, propertyName, createFragmentHandler(options, shape, value, callback)); | ||
}); | ||
@@ -71,0 +103,0 @@ } |
'use strict'; | ||
var async = require('async'); | ||
var unique = require('lodash.uniq'); | ||
var getPartOfReference = require('./helpers').getPartOfReference; | ||
var getReverseReferenceData = require('./helpers').getReverseReferenceData; | ||
@@ -25,2 +29,11 @@ /** | ||
// We got a null reference, meaning that the resolving of a reverse | ||
// reference is done | ||
if (reference === null) { | ||
process.nextTick(function() { | ||
callback(null, sourceData); | ||
}); | ||
return; | ||
} | ||
// Get position of first dot in the reference | ||
@@ -37,4 +50,8 @@ var dotPosition = reference.indexOf('.'); | ||
// Check if we're dealing with a reverse reference here | ||
var property = getPartOfReference(reference, 0); | ||
var reverseRefData = getReverseReferenceData(property); | ||
// We're looking for a value we have in the sourceData object | ||
if (dotPosition < 0) { | ||
if (dotPosition < 0 && !reverseRefData) { | ||
process.nextTick(function() { | ||
@@ -46,5 +63,10 @@ callback(null, sourceData[reference]); | ||
var property = getPartOfReference(reference, 0); | ||
var value = sourceData[property]; | ||
fetchData(sourceData[property], property, function(err, data) { | ||
if (reverseRefData) { | ||
value = sourceData[reverseRefData.referred]; | ||
property = reverseRefData.collection + '::' + reverseRefData.referring; | ||
} | ||
fetchData(value, property, function(err, data) { | ||
if (err) { | ||
@@ -55,6 +77,25 @@ callback(err); | ||
var keys = Object.keys(data); | ||
var childRef = (dotPosition < 0 ? null : reference.substr(dotPosition + 1)); | ||
// Check if the first object in data is an object, if it is we | ||
// got a list of objects back.. | ||
if (typeof data[keys[0]] === 'object') { | ||
async.map(Object.keys(data), function(id, cb) { | ||
options.resolveValue(data[id], childRef, options, cb); | ||
}, function(childResolveErr, childValues) { | ||
if (childResolveErr) { | ||
return callback(childResolveErr); | ||
} | ||
// Filter out duplicates | ||
callback(null, unique(childValues)); | ||
}); | ||
return; | ||
} | ||
// Resolve next level | ||
options.resolveValue( | ||
data, | ||
reference.substr(dotPosition + 1), | ||
childRef, | ||
options, | ||
@@ -61,0 +102,0 @@ callback |
@@ -5,2 +5,3 @@ 'use strict'; | ||
var merge = require('lodash.merge'); | ||
var isOneToMany = require('./helpers').isOneToMany; | ||
@@ -47,4 +48,10 @@ /** | ||
shapedObject[property] = {}; | ||
shapedObject[property][res.shape.collectionName] = res.id; | ||
var refId = res.id; | ||
if (Array.isArray(refId) && !isOneToMany(reference)) { | ||
refId = res.id[0]; | ||
} | ||
shapedObject[property][res.shape.collectionName] = refId; | ||
cb(); | ||
@@ -51,0 +58,0 @@ }); |
{ | ||
"name": "data-shaper", | ||
"version": "0.1.4", | ||
"version": "0.1.5", | ||
"description": "Utility for building meaningful data shapes from normalized, related data", | ||
@@ -30,8 +30,10 @@ "main": "src/index.js", | ||
"gulp-mocha": "^2.1.3", | ||
"mocha": "^2.2.5" | ||
"mocha": "^2.2.5", | ||
"pluralize": "^1.1.2" | ||
}, | ||
"dependencies": { | ||
"async": "^1.4.0", | ||
"lodash.merge": "^3.3.2" | ||
"lodash.merge": "^3.3.2", | ||
"lodash.uniq": "^3.2.2" | ||
} | ||
} |
@@ -72,2 +72,55 @@ # Data shaper | ||
## Reverse references | ||
In some cases data is referred to using coupling tables in order to make it possible to represent one-to-many or many-to-many relationships. The data shaper lets you express these relations using a reverse reference lookup. | ||
It looks like this; `collectionName(collectionField=referencedField)` and when the resolver finds a reference like this it does the following: | ||
1. Extracts the data from the reference: collection name, referring and referenced field | ||
2. Find the value to use when looking up data in the collection by fetching the value of `referencedField` from the object where the reference is | ||
3. Pass the collection, referring field and referenced value to the `fetchData` function | ||
This will result in an array of all matches being returned in the shape. | ||
## Single, direct, reverse reference | ||
In some cases, you might have a single, direct reverse reference. For instance, assume you have the following collection structure (imagine the `catBreedId` can only contain unique values): | ||
### catBreeds | ||
| id | name | description | | ||
|----|----------------------|--------------------------| | ||
| 1 | Norwegian Forest Cat | the most awesome of cats | | ||
| 2 | Scottish fold | the cutest of cats | | ||
### translations | ||
| id | catBreedId | translatedName | | ||
|----|------------|-----------------| | ||
| 11 | 1 | Norsk skogkatt | | ||
| 12 | 2 | Skotsk kattepus | | ||
Should you want to retrieve the translation for a given `catBreed`, you can do so by specifying a reverse, direct reference. It uses the same syntax as defined above, except it uses a double equation sign (`==`). For instance, the shape could be represented as: | ||
```js | ||
{ | ||
collectionName: 'catBreeds', | ||
shape: { | ||
id: 'id', | ||
name: 'name', | ||
translated: { | ||
reference: 'translations(catBreedId==id)', | ||
shape: { | ||
collectionName: 'translations', | ||
shape: { | ||
id: 'id', | ||
translatedName: 'translatedName' | ||
} | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
The difference here is that the shape will return a single ID for the reference, in the same way as regular references are shaped. | ||
## Fetching data | ||
@@ -80,5 +133,32 @@ In order for the data shaper to be able to resolve data you need to name your foreign keys in a way so that you're able to know what to query. The resolver pass the id and reference to the `fetchData` function you provide to the data-shaper. You will then have to use the reference to determine where the data is to be fetched from, get the data and return it. | ||
function fetchData(id, reference, callback) { | ||
/** | ||
* Function fetching and returning data. | ||
* | ||
* If reference contains a simple string the data can usually be fetched by primary key, | ||
* and if the value is a reverse reference the collection name and field can be parsed from the | ||
* reference and the value used to filter the result set. In both cases a full object with all | ||
* relevant data for the collection is expected. | ||
* | ||
* @param {int} Value to use when looking up data | ||
* @param {string} reference One of two types; someOtherId (foreign key) or collection::fieldName (reverse reference) | ||
* @param {function} callback | ||
*/ | ||
function fetchData(value, reference, callback) { | ||
if (reference.indexOf('::') > -1) { | ||
var splitReference = reference.split('::'); | ||
db(tableName) | ||
.where(splitReference, '=', value) | ||
.then(function(res) { | ||
callback(null, res) | ||
}) | ||
.catch(callback); | ||
return; | ||
} | ||
// Remove Id suffix from foreign key name to get collection name | ||
var tableName = reference.replace(/Id$/, ''); | ||
// Fetch the data | ||
db(tableName).fetch(id, callback); | ||
@@ -85,0 +165,0 @@ } |
@@ -6,3 +6,5 @@ 'use strict'; | ||
var dataShaper = require('../'); | ||
var fetchData = require('./mock/fetch-data'); | ||
var data = require('./mock/data'); | ||
var mockError = require('./mock/error'); | ||
var fetchData = require('./mock/fetch-data')(data); | ||
var fetchDataCounter = require('./mock/fetch-data-counter'); | ||
@@ -20,3 +22,3 @@ | ||
var defaultOptions = { fetchData: fetchData() }; | ||
var defaultOptions = { fetchData: fetchData }; | ||
@@ -28,3 +30,3 @@ describe('Data shaper', function() { | ||
id: 'id', | ||
name: 'name', | ||
name: 'firstName', | ||
zip: 'zipId', | ||
@@ -37,8 +39,5 @@ postal: 'zipId.postal', | ||
it('can shape array of data objects', function(done) { | ||
var data = [ | ||
{ id: 1, firstName: 'Fred', lastName: 'Flintstone', age: 36 }, | ||
{ id: 2, firstName: 'Barney', lastName: 'Rubble', age: 32 } | ||
]; | ||
var shapeData = [data.persons['1'], data.persons['2']]; | ||
dataShaper(data, simpleShape, defaultOptions, function(err, res) { | ||
dataShaper(shapeData, simpleShape, defaultOptions, function(err, res) { | ||
assert(!err); | ||
@@ -56,2 +55,59 @@ assert.deepEqual(res, { | ||
it('can shape object with reverse reference', function(done) { | ||
var shape = { | ||
collectionName: 'persons', | ||
shape: { | ||
id: 'id', | ||
name: 'firstName', | ||
addresses: { | ||
reference: 'addresses(personId=id)', | ||
shape: { | ||
collectionName: 'addresses', | ||
shape: { | ||
id: 'id', | ||
address: 'address', | ||
zip: 'zipId', | ||
country: 'zipId.countryId.name' | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
dataShaper( | ||
data.persons['1'], | ||
shape, | ||
defaultOptions, | ||
function(err, res) { | ||
assert(!err); | ||
assert.deepEqual(res, { | ||
addresses: { | ||
'1': { | ||
id: 1, | ||
address: 'Alphabet st. 1', | ||
zip: 1234, | ||
country: 'Norway' | ||
}, | ||
'2': { | ||
id: 2, | ||
address: 'Number rd. 2', | ||
zip: 1234, | ||
country: 'Norway' | ||
} | ||
}, | ||
persons: { | ||
'1': { | ||
id: 1, | ||
name: 'Fred', | ||
addresses: { addresses: [1, 2] } | ||
} | ||
} | ||
}); | ||
done(); | ||
} | ||
); | ||
}); | ||
it('returns an empty object if no data is given', function(done) { | ||
@@ -100,3 +156,3 @@ dataShaper([], {}, defaultOptions, function(err, res) { | ||
dataShaper( | ||
{ id: 1, firstName: 'Fred', lastName: 'Flintstone', age: 36 }, | ||
data.persons['1'], | ||
simpleShape, | ||
@@ -118,7 +174,3 @@ defaultOptions, | ||
function resolveValue(data, reference, options, callback) { | ||
process.nextTick(function() { | ||
callback(errorText); | ||
}); | ||
} | ||
var resolveFailure = mockError(errorText); | ||
@@ -128,3 +180,3 @@ dataShaper( | ||
simpleShape, | ||
merge({}, defaultOptions, { resolveValue: resolveValue }), | ||
merge({}, defaultOptions, { resolveValue: resolveFailure }), | ||
function(err) { | ||
@@ -131,0 +183,0 @@ assert.equal(err, errorText); |
@@ -46,2 +46,69 @@ 'use strict'; | ||
}); | ||
describe('#getReverseReferenceData', function() { | ||
it('extracts information from reverse references', function(done) { | ||
assert.deepEqual( | ||
helpers.getReverseReferenceData('foo(bar=id)'), | ||
{ collection: 'foo', referring: 'bar', referred: 'id', oneToMany: true } | ||
); | ||
assert.deepEqual( | ||
helpers.getReverseReferenceData('fooCollection(myField=someOtherField)'), | ||
{ collection: 'fooCollection', referring: 'myField', referred: 'someOtherField', oneToMany: true } | ||
); | ||
assert.deepEqual( | ||
helpers.getReverseReferenceData('foo-collection(my-field=some-field)'), | ||
{ collection: 'foo-collection', referring: 'my-field', referred: 'some-field', oneToMany: true } | ||
); | ||
assert.deepEqual( | ||
helpers.getReverseReferenceData('foo-collection(my-field==some-field)'), | ||
{ collection: 'foo-collection', referring: 'my-field', referred: 'some-field', oneToMany: false } | ||
); | ||
assert.deepEqual( | ||
helpers.getReverseReferenceData('foo_collection(my_field=some_field)'), | ||
{ collection: 'foo_collection', referring: 'my_field', referred: 'some_field', oneToMany: true } | ||
); | ||
done(); | ||
}); | ||
it('returns null if reverse reference is not valid', function(done) { | ||
var invalidRefs = [ | ||
'.foo(bar=id).', '.foo(bar=id)', 'foo(bar=id).', | ||
'sfd(bar)', 'sfd(bar:id)', 'sfd(bar=id', | ||
'sfd(bar)', 'sfd(bar))', 'sfd' | ||
]; | ||
for (var i in invalidRefs) { | ||
var ref = invalidRefs[i]; | ||
assert.equal(helpers.getReverseReferenceData(ref), null); | ||
} | ||
done(); | ||
}); | ||
}); | ||
describe('#isOneToMany', function() { | ||
it('returns correct value for reference definitions', function() { | ||
assert.equal(helpers.isOneToMany('foo(bar=id)'), true); | ||
assert.equal(helpers.isOneToMany('fooCollection(myField=someOtherField)'), true); | ||
assert.equal(helpers.isOneToMany('foo-collection(my-field=some-field)'), true); | ||
assert.equal(helpers.isOneToMany('foo-collection(my-field==some-field)'), false); | ||
assert.equal(helpers.isOneToMany('foo_collection(my_field=some_field)'), true); | ||
}); | ||
it('supports nested object reference', function() { | ||
assert.equal(helpers.isOneToMany({ | ||
reference: 'foo-collection(my-field=some-field)' | ||
}), true); | ||
assert.equal(helpers.isOneToMany({ | ||
reference: 'foo-collection(my-field==some-field)' | ||
}), false); | ||
}); | ||
}); | ||
}); |
'use strict'; | ||
var pluralize = require('pluralize'); | ||
module.exports = function(data) { | ||
data = (typeof data === 'undefined') ? {} : data; | ||
return function fetchData(id, reference, callback) { | ||
return function fetchData(value, reference, callback) { | ||
var splitRef = reference.split('::'); | ||
var collectionKey = splitRef[0]; | ||
var filterProperty = splitRef[1]; | ||
// If data is null, return null. For testing purposes | ||
if (data === null) { | ||
process.nextTick(function() { | ||
callback(null, null); | ||
}); | ||
return; | ||
} | ||
// Regular foreign key reference | ||
if (!filterProperty) { | ||
var collection = pluralize(collectionKey.replace(/Id$/, '')); | ||
process.nextTick(function() { | ||
callback(null, data[collection][value]); | ||
}); | ||
return; | ||
} | ||
// Slightly more magic reverse reference | ||
var response = {}; | ||
for (var key in data[collectionKey]) { | ||
if (data[collectionKey][key][filterProperty] === value) { | ||
response[key] = data[collectionKey][key]; | ||
} | ||
} | ||
process.nextTick(function() { | ||
callback(null, data); | ||
callback(null, response); | ||
}); | ||
}; | ||
}; |
@@ -15,3 +15,3 @@ 'use strict'; | ||
var personData = { id: 1, firstName: 'Fred', companyId: 2 }; | ||
var companyData = { id: 2, name: 'VG' }; | ||
var companyData = { companies: { '2': { id: 2, name: 'VG' } } }; | ||
@@ -18,0 +18,0 @@ var companyShape = { |
@@ -9,14 +9,17 @@ 'use strict'; | ||
var mockError = require('./mock/error'); | ||
var data = require('./mock/data'); | ||
var fetchData = require('./mock/fetch-data')(data); | ||
var defaultOptions = { | ||
resolveValue: resolveValue | ||
resolveValue: resolveValue, | ||
fetchData: fetchData | ||
}; | ||
describe('Resolve value', function() { | ||
var data = { id: 1, lastName: 'Flintstone', companyId: 2}; | ||
var personData = data.persons['1']; | ||
it('takes local values off the data object', function(done) { | ||
resolveValue(data, 'lastName', defaultOptions, function(err, res) { | ||
resolveValue(personData, 'lastName', defaultOptions, function(err, res) { | ||
assert(!err); | ||
assert.equal(res, data.lastName); | ||
assert.equal(res, personData.lastName); | ||
done(); | ||
@@ -27,23 +30,9 @@ }); | ||
it('resolves dot notated references', function(done) { | ||
var customData = { | ||
companyId: { '2': { id: 2, name: 'VG', municipalId: 1 } }, | ||
municipalId: { '1': { id: 1, name: 'Oslo', countryId: 4 }}, | ||
countryId: { '4': { id: 4, name: 'Norway' }} | ||
}; | ||
// Data fetcher that responds to id and reference params and returns | ||
// mock data for a few different collections | ||
function customFetchData(id, reference, callback) { | ||
process.nextTick(function() { | ||
callback(null, customData[reference][String(id)]); | ||
}); | ||
} | ||
resolveValue( | ||
data, | ||
data.persons['1'], | ||
'companyId.municipalId.countryId.name', | ||
merge({}, defaultOptions, { fetchData: customFetchData }), | ||
defaultOptions, | ||
function(err, value) { | ||
assert(!err); | ||
assert.equal(value, customData.countryId['4'].name); | ||
assert.equal(value, data.countries['1'].name); | ||
done(); | ||
@@ -75,2 +64,15 @@ } | ||
}); | ||
it('can resolve a one-to-many-relation', function(done) { | ||
resolveValue( | ||
data.persons['1'], | ||
'phoneNumbers(employeeId=id).phoneTypeId.name', | ||
defaultOptions, | ||
function(err, res) { | ||
assert(!err); | ||
assert.deepEqual(res, ['Mobile', 'Landline']); | ||
done(); | ||
} | ||
); | ||
}); | ||
}); |
'use strict'; | ||
var assert = require('assert'); | ||
var merge = require('lodash.merge'); | ||
var shapeData = require('../lib/shape-data'); | ||
var resolveValue = require('../lib/resolve-value'); | ||
var resolveFragment = require('../lib/resolve-fragment'); | ||
var data = require('./mock/data'); | ||
var fetchData = require('./mock/fetch-data')(data); | ||
var options = { | ||
fetchData: function(id, ref, callback) { | ||
process.nextTick(function() { | ||
callback(null, { | ||
id: 2, | ||
name: 'VG' | ||
}); | ||
}); | ||
}, | ||
fetchData: fetchData, | ||
resolveValue: resolveValue, | ||
@@ -44,3 +41,3 @@ resolveFragment: resolveFragment, | ||
shapeData( | ||
{ id: 1, firstName: 'Fred', companyId: 2 }, | ||
data.persons['1'], | ||
shape, | ||
@@ -64,3 +61,3 @@ options, | ||
merge({}, options, { | ||
resolveFragment: function(data, ref, opts, callback) { | ||
resolveFragment: function(sourceData, ref, opts, callback) { | ||
process.nextTick(function() { | ||
@@ -67,0 +64,0 @@ callback('Strange error'); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
47676
22
1084
220
3
7
+ Addedlodash.uniq@^3.2.2
+ Addedlodash._basecallback@3.3.1(transitive)
+ Addedlodash._baseindexof@3.1.0(transitive)
+ Addedlodash._baseisequal@3.0.7(transitive)
+ Addedlodash._baseuniq@3.0.3(transitive)
+ Addedlodash._cacheindexof@3.0.2(transitive)
+ Addedlodash._createcache@3.1.2(transitive)
+ Addedlodash.pairs@3.0.1(transitive)
+ Addedlodash.uniq@3.2.2(transitive)