Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

data-shaper

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

data-shaper - npm Package Compare versions

Comparing version 0.1.5 to 1.0.0

103

lib/helpers.js
'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
};

47

lib/resolve-value.js

@@ -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);
};
};
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc