data-shaper
Advanced tools
Comparing version 0.1.5 to 1.0.0
'use strict'; | ||
var merge = require('lodash.merge'); | ||
/** | ||
@@ -10,5 +12,7 @@ * Split reference into an array | ||
function splitReference(reference) { | ||
return reference.split('.').filter(function(element) { | ||
return element.length > 0; | ||
}); | ||
if (reference === '') { | ||
return []; | ||
} | ||
return reference.match(/([^\(]+\([^\)]+\)|[^\.]+)/g); | ||
} | ||
@@ -37,10 +41,20 @@ | ||
* | ||
* @param {int} id | ||
* @param {object|int} value | ||
* @param {string} reference | ||
* @return {string} hash of function call | ||
*/ | ||
function hashFetchDataCall(id, reference) { | ||
return reference + '::' + id; | ||
function hashFetchDataCall(value, reference) { | ||
if (typeof value !== 'object') { | ||
return reference + '::' + value; | ||
} | ||
return reference + '::' + JSON.stringify(value); | ||
} | ||
/*eslint-disable */ | ||
// Regex used for validating the format of the reference | ||
// Match: someCollection(property=value,...) | ||
var refRegex = /^([A-z0-9-_]+)\(((?:[A-z0-9-_]+={1,2}[A-z0-9-._]+|"[^"]+")(?:(?:,\s*(?:[A-z0-9-_]+=(?:[A-z0-9-._]+|"[^"]+")))*))\)$/; | ||
/*eslint-enable */ | ||
/** | ||
@@ -51,3 +65,8 @@ * Get reverse reference data. Reverse references are denoted by | ||
* | ||
* Example: employeeDetails(employeeId=id) | ||
* Multiple field=value filters is supported, but only the first one can | ||
* use a ==. Three types of values are supported; quoted string (a value) | ||
* string without quotes (reference to a field in data) and a number (integer | ||
* or float value with dot as decimal separator). | ||
* | ||
* Example: employeeDetails(employeeId=id,otherField=123) | ||
* a^ b^ c^ ^d | ||
@@ -66,14 +85,43 @@ * | ||
function getReverseReferenceData(reference) { | ||
var data = reference.match(/^([A-z0-9-_]+)\(([A-z0-9-_]+)(={1,2})([A-z0-9-_]+)\)$/); | ||
var match = reference.match(refRegex); | ||
if (data === null) { | ||
return data; | ||
// Return null if the reference is malformed in some way | ||
if (match === null) { | ||
return match; | ||
} | ||
return { | ||
collection: data[1], | ||
referring: data[2], | ||
referred: data[4], | ||
oneToMany: data[3] === '=' | ||
var response = { | ||
collection: match[1], | ||
references: {}, | ||
filters: {}, | ||
oneToMany: reference.indexOf('==') < 0 | ||
}; | ||
// We have a match, split the filter into the different parts | ||
// var collection = match[1]; | ||
var refs = /([A-z0-9-_]+)(={1,2})([A-z0-9-_]+|"[^"]+")/g; | ||
// Iterate over the matched parts of the reference to find the properties | ||
var ref; | ||
while ((ref = refs.exec(match[2]))) { | ||
var value = ref[3]; | ||
var property = ref[1]; | ||
// Numeric value, add to filter | ||
if (value.match(/^\d+(?:\.\d+)?$/)) { | ||
response.filters[property] = parseFloat(value); | ||
continue; | ||
} | ||
// String value, add to filter | ||
if (value.match(/^".*"$/)) { | ||
response.filters[property] = value.substr(1, value.length - 2); | ||
continue; | ||
} | ||
// Reference to a data property value, add to references | ||
response.references[property] = value; | ||
} | ||
return response; | ||
} | ||
@@ -98,3 +146,3 @@ | ||
* Given a substring, will return a function that when called with a string determines | ||
* if that string contains the substring. | ||
* if that string starts with the substring. | ||
* | ||
@@ -110,9 +158,28 @@ * @param {string} substr Substring to match | ||
/** | ||
* Merge data+references and filters to build a query for fetchData | ||
* | ||
* @param {object} data | ||
* @param {object} references | ||
* @param {object} filters | ||
* @return {object} | ||
*/ | ||
function buildQuery(data, references, filters) { | ||
var query = {}; | ||
for (var field in references) { | ||
query[field] = data[references[field]]; | ||
} | ||
return merge(query, filters); | ||
} | ||
module.exports = { | ||
splitReference: splitReference, | ||
getPartOfReference: getPartOfReference, | ||
getReverseReferenceData: getReverseReferenceData, | ||
hashFetchDataCall: hashFetchDataCall, | ||
getReverseReferenceData: getReverseReferenceData, | ||
isOneToMany: isOneToMany, | ||
buildQuery: buildQuery, | ||
splitReference: splitReference, | ||
startsWith: startsWith | ||
}; |
@@ -6,5 +6,10 @@ 'use strict'; | ||
var getPartOfReference = require('./helpers').getPartOfReference; | ||
var getReverseReferenceData = require('./helpers').getReverseReferenceData; | ||
var helpers = require('./helpers'); | ||
var splitReference = helpers.splitReference; | ||
var getPartOfReference = helpers.getPartOfReference; | ||
var getReverseReferenceData = helpers.getReverseReferenceData; | ||
var buildQuery = helpers.buildQuery; | ||
var isOneToMany = helpers.isOneToMany; | ||
/** | ||
@@ -40,3 +45,4 @@ * Resolves property values based on a relation reference | ||
// Get position of first dot in the reference | ||
var dotPosition = reference.indexOf('.'); | ||
var refParts = splitReference(reference); | ||
var refPartsLength = refParts.length; | ||
@@ -56,3 +62,3 @@ // We got null as data, not possible to continue resolving | ||
// We're looking for a value we have in the sourceData object | ||
if (dotPosition < 0 && !reverseRefData) { | ||
if (refPartsLength === 1 && !reverseRefData) { | ||
process.nextTick(function() { | ||
@@ -66,5 +72,6 @@ callback(null, sourceData[reference]); | ||
// Reverse reference – prepare fetchData query | ||
if (reverseRefData) { | ||
value = sourceData[reverseRefData.referred]; | ||
property = reverseRefData.collection + '::' + reverseRefData.referring; | ||
value = buildQuery(sourceData, reverseRefData.references, reverseRefData.filters); | ||
property = reverseRefData.collection; | ||
} | ||
@@ -79,3 +86,3 @@ | ||
var keys = Object.keys(data); | ||
var childRef = (dotPosition < 0 ? null : reference.substr(dotPosition + 1)); | ||
var childRef = (refPartsLength === 1 ? null : refParts.slice(1).join('.')); | ||
@@ -85,13 +92,19 @@ // Check if the first object in data is an object, if it is we | ||
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); | ||
} | ||
// This is a one-to-one relation, we'll change the data structure | ||
if (isOneToMany(refParts[0])) { | ||
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; | ||
// Filter out duplicates | ||
callback(null, unique(childValues)); | ||
}); | ||
return; | ||
} | ||
// We have a one-to-one reference | ||
data = data[keys[0]]; | ||
} | ||
@@ -98,0 +111,0 @@ |
{ | ||
"name": "data-shaper", | ||
"version": "0.1.5", | ||
"version": "1.0.0", | ||
"description": "Utility for building meaningful data shapes from normalized, related data", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
@@ -125,2 +125,5 @@ # Data shaper | ||
## Complex reverse references | ||
Sometimes there is a need for filtering the data, for instance if the reverse lookup returns translations for multiple languages and you only need the norwegian translation. Filtering of data can be done by specifying multiple field-value pairs; ```translations(catBreedId=id, language="no-NB")```. | ||
## Fetching data | ||
@@ -136,17 +139,21 @@ 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. | ||
* | ||
* 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. | ||
* If value is not an object the reference is a foreign key and the data can usually be fetched | ||
* by primary key on the referred table. If however the value is an object, we're doing a reverse | ||
* lookup and the value contains the data to filter by. | ||
* | ||
* @param {int} Value to use when looking up data | ||
* @param {string} reference One of two types; someOtherId (foreign key) or collection::fieldName (reverse reference) | ||
* The value looks like this when doing revers referencing: { fieldA: 'valueA', fieldB: 123 } | ||
* | ||
* In both cases a full object with all relevant data for the collection is expected. | ||
* | ||
* @param {object|int} Value to use when looking up data | ||
* @param {string} reference One of two types; someOtherId (foreign key) or collection (for reverse reference) | ||
* @param {function} callback | ||
*/ | ||
function fetchData(value, reference, callback) { | ||
if (reference.indexOf('::') > -1) { | ||
var splitReference = reference.split('::'); | ||
if (typeof value === 'object') { | ||
var collection = reference; | ||
var query = value; | ||
db(tableName) | ||
.where(splitReference, '=', value) | ||
db(collection) | ||
.where(query) | ||
.then(function(res) { | ||
@@ -161,6 +168,6 @@ callback(null, res) | ||
// Remove Id suffix from foreign key name to get collection name | ||
var tableName = reference.replace(/Id$/, ''); | ||
var collection = reference.replace(/Id$/, ''); | ||
// Fetch the data | ||
db(tableName).fetch(id, callback); | ||
db(collection).fetch(id, callback); | ||
} | ||
@@ -167,0 +174,0 @@ ``` |
@@ -108,2 +108,33 @@ 'use strict'; | ||
it('can shape object with reverse reference and filter', function(done) { | ||
var shape = { | ||
collectionName: 'persons', | ||
shape: { | ||
id: 'id', | ||
name: 'firstName', | ||
addressId: 'addresses(personId==id, address="Alphabet st. 1").id' | ||
} | ||
}; | ||
dataShaper( | ||
data.persons['1'], | ||
shape, | ||
defaultOptions, | ||
function(err, res) { | ||
assert(!err); | ||
assert.deepEqual(res, { | ||
persons: { | ||
'1': { | ||
id: 1, | ||
name: 'Fred', | ||
addressId: 1 | ||
} | ||
} | ||
}); | ||
done(); | ||
} | ||
); | ||
}); | ||
it('returns an empty object if no data is given', function(done) { | ||
@@ -110,0 +141,0 @@ dataShaper([], {}, defaultOptions, function(err, res) { |
@@ -9,5 +9,10 @@ 'use strict'; | ||
it('splits dot notated reference into parts', function(done) { | ||
var parts = helpers.splitReference('some.related.property'); | ||
var parts = helpers.splitReference('addresses(personId==id,address="Alphabet st. 1").zip.name'); | ||
assert.deepEqual(parts, ['some', 'related', 'property']); | ||
assert.deepEqual(parts, [ | ||
'addresses(personId==id,address="Alphabet st. 1")', | ||
'zip', | ||
'name' | ||
]); | ||
done(); | ||
@@ -52,3 +57,8 @@ }); | ||
helpers.getReverseReferenceData('foo(bar=id)'), | ||
{ collection: 'foo', referring: 'bar', referred: 'id', oneToMany: true } | ||
{ | ||
collection: 'foo', | ||
references: { bar: 'id' }, | ||
filters: {}, | ||
oneToMany: true | ||
} | ||
); | ||
@@ -58,3 +68,8 @@ | ||
helpers.getReverseReferenceData('fooCollection(myField=someOtherField)'), | ||
{ collection: 'fooCollection', referring: 'myField', referred: 'someOtherField', oneToMany: true } | ||
{ | ||
collection: 'fooCollection', | ||
references: { myField: 'someOtherField' }, | ||
filters: {}, | ||
oneToMany: true | ||
} | ||
); | ||
@@ -64,3 +79,8 @@ | ||
helpers.getReverseReferenceData('foo-collection(my-field=some-field)'), | ||
{ collection: 'foo-collection', referring: 'my-field', referred: 'some-field', oneToMany: true } | ||
{ | ||
collection: 'foo-collection', | ||
references: { 'my-field': 'some-field' }, | ||
filters: {}, | ||
oneToMany: true | ||
} | ||
); | ||
@@ -70,3 +90,8 @@ | ||
helpers.getReverseReferenceData('foo-collection(my-field==some-field)'), | ||
{ collection: 'foo-collection', referring: 'my-field', referred: 'some-field', oneToMany: false } | ||
{ | ||
collection: 'foo-collection', | ||
references: { 'my-field': 'some-field' }, | ||
filters: {}, | ||
oneToMany: false | ||
} | ||
); | ||
@@ -76,5 +101,20 @@ | ||
helpers.getReverseReferenceData('foo_collection(my_field=some_field)'), | ||
{ collection: 'foo_collection', referring: 'my_field', referred: 'some_field', oneToMany: true } | ||
{ | ||
collection: 'foo_collection', | ||
references: { 'my_field': 'some_field' }, | ||
filters: {}, | ||
oneToMany: true | ||
} | ||
); | ||
assert.deepEqual( | ||
helpers.getReverseReferenceData('foo_collection(my_field=1st-player, something="value", foo=123.435)'), | ||
{ | ||
collection: 'foo_collection', | ||
references: { 'my_field': '1st-player' }, | ||
filters: { foo: 123, something: 'value' }, | ||
oneToMany: true | ||
} | ||
); | ||
done(); | ||
@@ -87,3 +127,4 @@ }); | ||
'sfd(bar)', 'sfd(bar:id)', 'sfd(bar=id', | ||
'sfd(bar)', 'sfd(bar))', 'sfd' | ||
'sfd(bar)', 'sfd(bar))', 'sfd', 'sfd()', | ||
'sfd(foo=bar, bar==foo)' | ||
]; | ||
@@ -120,2 +161,13 @@ | ||
}); | ||
describe('#buildQuery', function() { | ||
it('builds query from data, references and filter', function() { | ||
var data = { id: 1, firstName: 'Kristoffer', age: 26 }; | ||
var references = { personId: 'id' }; | ||
var filters = { firstName: 'Kristoffer', age: 26 }; | ||
var query = helpers.buildQuery(data, references, filters); | ||
assert.deepEqual(query, { age: 26, firstName: 'Kristoffer', personId: 1 }); | ||
}); | ||
}); | ||
}); |
@@ -23,12 +23,4 @@ 'use strict'; | ||
var companies = { | ||
'2': { | ||
id: 2, | ||
name: 'VG', | ||
municipalId: 1 | ||
}, | ||
'3': { | ||
id: 3, | ||
name: 'VaffelNinja', | ||
municipalId: 1 | ||
} | ||
'2': { id: 2, name: 'VG', municipalId: 1 }, | ||
'3': { id: 3, name: 'VaffelNinja', municipalId: 1 } | ||
}; | ||
@@ -38,3 +30,4 @@ | ||
'1': { | ||
id: 1, personId: 1, | ||
id: 1, | ||
personId: 1, | ||
address: 'Alphabet st. 1', | ||
@@ -41,0 +34,0 @@ zipId: 1234, |
@@ -8,7 +8,35 @@ 'use strict'; | ||
function fetchReverse(refData, collection, callback) { | ||
var response = {}; | ||
for (var id in data[collection]) { | ||
var item = data[collection][id]; | ||
var match = true; | ||
for (var property in refData) { | ||
if (item[property] !== refData[property]) { | ||
match = false; | ||
break; | ||
} | ||
} | ||
if (match) { | ||
response[id] = item; | ||
} | ||
} | ||
process.nextTick(function() { | ||
callback(null, response); | ||
}); | ||
} | ||
function fetch(value, reference, callback) { | ||
var collection = pluralize(reference.replace(/Id$/, '')); | ||
process.nextTick(function() { | ||
callback(null, data[collection][value]); | ||
}); | ||
} | ||
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 | ||
@@ -22,24 +50,11 @@ if (data === null) { | ||
// Regular foreign key reference | ||
if (!filterProperty) { | ||
var collection = pluralize(collectionKey.replace(/Id$/, '')); | ||
process.nextTick(function() { | ||
callback(null, data[collection][value]); | ||
}); | ||
// Referese reference | ||
if (typeof value === 'object') { | ||
fetchReverse(value, reference, callback); | ||
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, response); | ||
}); | ||
// Regular reference | ||
fetch(value, reference, callback); | ||
}; | ||
}; |
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
Network access
Supply chain riskThis module accesses the network.
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
53261
1229
0
227
1