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.4 to 0.1.5

tests/mock/data.js

66

lib/helpers.js

@@ -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
};

88

lib/resolve-fragment.js
'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');

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