Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cascading PUTs #73

Merged
merged 1 commit into from
Jun 17, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 73 additions & 12 deletions lib/services/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ var _ = require('lodash'),
is = require('../assert-is'),
path = require('path'),
config = require('config'),
glob = require('glob');
glob = require('glob'),
referenceProperty = '_ref';

/**
* Takes a ref, and returns the component name within it.
Expand Down Expand Up @@ -56,11 +57,11 @@ function get(ref, locals) {
*/
function putLatest(ref, data) {
data = JSON.stringify(data);
return db.batch([
return [
{ type: 'put', key: ref, value: data },
{ type: 'put', key: ref + '@latest', value: data },
{ type: 'put', key: ref + '@' + uid(), value: data}
]);
];
}

/**
Expand All @@ -71,10 +72,10 @@ function putLatest(ref, data) {
*/
function putPublished(ref, data) {
data = JSON.stringify(data);
return db.batch([
return [
{ type: 'put', key: ref + '@published', value: data },
{ type: 'put', key: ref + '@' + uid(), value: data}
]);
];
}

/**
Expand All @@ -86,9 +87,9 @@ function putPublished(ref, data) {
*/
function putTag(ref, data, tag) {
data = JSON.stringify(data);
return db.batch([
return [
{ type: 'put', key: ref + '@' + tag, value: data }
]);
];
}

/**
Expand Down Expand Up @@ -123,16 +124,76 @@ function putDefaultBehavior(ref, data) {
* @param data
*/
function put(ref, data) {
var promise,
var result,
componentModule = files.getComponentModule(getName(ref));

if (componentModule && _.isFunction(componentModule.put)) {
promise = componentModule.put(ref, data);
result = componentModule.put(ref, data);
} else {
promise = putDefaultBehavior(ref, data);
result = putDefaultBehavior(ref, data);
}

return is.promise(promise, ref).return(data);
return result;
}

/**
* Clear all of an object properties (in place), not a new object
*
* No need to return anything since it's in-place
*
* @param {object} obj
*/
function clearOwnProperties(obj) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just obj = {};?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it needs to be a particular object, specifically the one linked to by the original object. That's what it means by "in-place". Creating a new object might as well be {}, like you say.

In this case, there is a root object with many objects within it, and we're clearing those deep objects located somewhere deep within that root object. We don't know where these deep objects are -- they could even be within several layers of other objects within the root object.

_.forOwn(obj, function (value, key) {
delete obj[key];
});
}

/**
* True if this is a reference object that also has real data in it.
*
* Used to determine if that data should be preserved or not.
*
* @param {object} obj
* @returns {boolean}
*/
function isReferencedAndReal(obj) {
return _.isString(obj[referenceProperty]) && _.size(obj) > 1;
}

/**
* @param {string} ref
* @param {object} data
* @returns {Promise.object}
*/
function cascadingPut(ref, data) {
//search for _ref with _size greater than 1
var ops = [],
deepObjects = _.listDeepObjects(data, isReferencedAndReal);

//reverse; children should be before parents
_.each(deepObjects.reverse(), function (obj) {
var ref = obj[referenceProperty];

// since children are before parents, no one will see data below them
ops.push({key: ref, value: _.omit(obj, referenceProperty)});

// omit cloned 1 level deep and we clear what omit cloned from obj
// so the op gets the first level of data, but it's removed from the main obj
clearOwnProperties(obj);
obj[referenceProperty] = ref;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cruzanmo Specifically, note this comment.

It's also the reason why ordering is important for the _.omit cloning of the first level of the object, which is why the reverse is there.

Algorithms are fun! :D

});

//add the cleaned root object at the end
ops.push({key: ref, value: data});

return bluebird.map(ops, function (op) {
//run each through the normal put, which may or may not hit custom component logic
return put(op.key, op.value);
}).then(function (ops) {
//flatten to list of final batch ops
return db.batch(_.filter(_.flattenDeep(ops), _.identity));
}).return(data); //return root object if successful
}

/**
Expand Down Expand Up @@ -202,7 +263,7 @@ function getTemplate(ref) {

//outsiders can act on components too
module.exports.get = get;
module.exports.put = put;
module.exports.put = cascadingPut; //special: could lead to multiple put operations
module.exports.del = del;

//maybe server.js people want to reach in and reuse these?
Expand Down
106 changes: 78 additions & 28 deletions lib/services/components.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe(_.startCase(filename), function () {
});

afterEach(function () {
sandbox.verifyAndRestore();
sandbox.restore();
});

describe('getName', function () {
Expand Down Expand Up @@ -93,39 +93,24 @@ describe(_.startCase(filename), function () {
describe('putTag', function () {
var fn = lib[this.title];

it('puts to tag', function (done) {
sandbox.mock(db).expects('batch').withArgs().once().returns(bluebird.resolve());
fn('/components/whatever', {}, 'special').done(function () {
done();
}, function (err) {
done(err);
});
it('puts to tag', function () {
fn('/components/whatever', {}, 'special');
});
});

describe('putPublished', function () {
var fn = lib[this.title];

it('puts to published', function (done) {
sandbox.mock(db).expects('batch').withArgs().once().returns(bluebird.resolve());
fn('/components/whatever', {}).done(function () {
done();
}, function (err) {
done(err);
});
it('puts to published', function () {
fn('/components/whatever', {});
});
});

describe('putLatest', function () {
var fn = lib[this.title];

it('puts to latest', function (done) {
sandbox.mock(db).expects('batch').withArgs().once().returns(bluebird.resolve());
fn('/components/whatever', {}).done(function () {
done();
}, function (err) {
done(err);
});
it('puts to latest', function () {
fn('/components/whatever', {});
});
});

Expand All @@ -136,17 +121,13 @@ describe(_.startCase(filename), function () {

});

it('deletes', function (done) {
it('deletes', function () {
var mockDb = sandbox.mock(db),
mockFiles = sandbox.mock(files);
mockDb.expects('get').withArgs().once().returns(bluebird.resolve('{}'));
mockDb.expects('del').withArgs().once().returns(bluebird.resolve());
mockFiles.expects('getComponentModule').withArgs('whatever').twice().returns(null);
fn('/components/whatever').done(function () {
done();
}, function (err) {
done(err);
});
return fn('/components/whatever');
});
});

Expand All @@ -156,6 +137,75 @@ describe(_.startCase(filename), function () {
it('throws error if component module does not return promise', function () {

});

it('puts', function () {
var ref = 'a',
data = {};

sandbox.stub(files, 'getComponentModule');
sandbox.stub(db, 'batch');

return fn(ref, data).then(function () {
expect(db.batch.getCall(0).args[0]).to.deep.contain.members([{ key: 'a', type: 'put', value: '{}' }]);
});
});

it('returns original object if successful', function () {
var ref = 'a',
data = {};

sandbox.stub(files, 'getComponentModule');
sandbox.stub(db, 'batch');

return fn(ref, data).then(function (result) {
expect(result).to.deep.equal({});
});
});

it('cascades', function () {
var ref = 'a',
data = {a: 'b', c: {_ref:'d', e: 'f'}};

sandbox.stub(files, 'getComponentModule');
sandbox.stub(db, 'batch');

return fn(ref, data).then(function () {
var ops = db.batch.getCall(0).args[0];
expect(ops).to.deep.contain.members([
{ key: 'd', type: 'put', value: JSON.stringify({e: 'f'}) },
{ key: 'a', type: 'put', value: JSON.stringify({a: 'b', c: { _ref: 'd'}}) }
]);
});
});

it('cascades with component modules', function () {
var ref = 'a',
data = {a: 'b', c: {_ref:'d', e: 'f'}},
rootModuleData = {type: 'put', key: 'g', value: JSON.stringify({h: 'i'})},
deepModuleData = {type: 'put', key: 'j', value: JSON.stringify({k: 'l'})},
putSpy = sinon.stub();

sandbox.stub(files, 'getComponentModule').returns({put: putSpy});
sandbox.stub(db, 'batch');
putSpy.withArgs('a', sinon.match.object).returns([rootModuleData]);
putSpy.withArgs('d', sinon.match.object).returns([deepModuleData]);

return fn(ref, data).then(function () {
expect(db.batch.getCall(0).args[0]).to.deep.contain.members([rootModuleData, deepModuleData]);
});
});

it('returns basic root object if successful even if cascading', function () {
var ref = 'a',
data = {a: 'b', c: {_ref:'d', e: 'f'}};

sandbox.stub(files, 'getComponentModule');
sandbox.stub(db, 'batch');

return fn(ref, data).then(function (result) {
expect(result).to.deep.equal({ a: 'b', c: { _ref: 'd' } });
});
});
});

describe('get', function () {
Expand Down
43 changes: 30 additions & 13 deletions test/api/components/put.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ describe(endpointName, function () {
acceptsHtml = apiAccepts.acceptsHtml(_.camelCase(filename)),
updatesOther = apiAccepts.updatesOther(_.camelCase(filename)),
createsNewVersion = apiAccepts.createsNewVersion(_.camelCase(filename)),
data = { name: 'Manny', species: 'cat' };
cascades = apiAccepts.cascades(_.camelCase(filename)),
data = { name: 'Manny', species: 'cat' },
cascadingTarget = '/components/validDeep',
cascadingData = {a: 'b', c: {_ref: cascadingTarget, d: 'e'}},
cascadingReturnData = {a: 'b', c: {_ref: cascadingTarget}},
cascadingDeepData = {d: 'e'};

beforeEach(function () {
sandbox = sinon.sandbox.create();
Expand Down Expand Up @@ -44,10 +49,13 @@ describe(endpointName, function () {
acceptsJsonBody(path, {name: 'invalid'}, {}, 404, { code: 404, message: 'Not Found' });
acceptsJsonBody(path, {name: 'valid'}, data, 200, data);
acceptsJsonBody(path, {name: 'missing'}, data, 200, data);
acceptsJsonBody(path, {name: 'valid'}, cascadingData, 200, cascadingReturnData);

acceptsHtml(path, {name: 'invalid'}, 404);
acceptsHtml(path, {name: 'valid'}, 406);
acceptsHtml(path, {name: 'missing'}, 406);

cascades(path, {name: 'valid'}, cascadingData, cascadingTarget, cascadingDeepData);
});

describe('/components/:name/schema', function () {
Expand All @@ -65,17 +73,20 @@ describe(endpointName, function () {
describe('/components/:name@:version', function () {
var path = this.title;

acceptsJson(path, {name: 'invalid', version: 'abc'}, 404, { code: 404, message: 'Not Found' });
acceptsJson(path, {name: 'valid', version: 'abc'}, 200, {});
acceptsJson(path, {name: 'missing', version: 'abc'}, 200, {});
acceptsJson(path, {name: 'invalid', version: 'def'}, 404, { code: 404, message: 'Not Found' });
acceptsJson(path, {name: 'valid', version: 'def'}, 200, {});
acceptsJson(path, {name: 'missing', version: 'def'}, 200, {});

acceptsJsonBody(path, {name: 'invalid', version: 'def'}, {}, 404, { code: 404, message: 'Not Found' });
acceptsJsonBody(path, {name: 'valid', version: 'def'}, data, 200, data);
acceptsJsonBody(path, {name: 'missing', version: 'def'}, data, 200, data);
acceptsJsonBody(path, {name: 'valid', version: 'def'}, cascadingData, 200, cascadingReturnData);

acceptsHtml(path, {name: 'invalid', version: 'def'}, 404);
acceptsHtml(path, {name: 'valid', version: 'def'}, 406);
acceptsHtml(path, {name: 'missing', version: 'def'}, 406);

acceptsHtml(path, {name: 'invalid', version: 'ghi'}, 404);
acceptsHtml(path, {name: 'valid', version: 'ghi'}, 406);
acceptsHtml(path, {name: 'missing', version: 'ghi'}, 406);
cascades(path, {name: 'valid', version: 'def'}, cascadingData, cascadingTarget, cascadingDeepData);
});

describe('/components/:name/instances', function () {
Expand Down Expand Up @@ -104,11 +115,14 @@ describe(endpointName, function () {
acceptsJsonBody(path, {name: 'invalid', id: 'valid'}, {}, 404, { code: 404, message: 'Not Found' });
acceptsJsonBody(path, {name: 'valid', id: 'valid'}, data, 200, data);
acceptsJsonBody(path, {name: 'missing', id: 'missing'}, data, 200, data);
acceptsJsonBody(path, {name: 'valid'}, cascadingData, 200, cascadingReturnData);

acceptsHtml(path, {name: 'invalid', id: 'valid'}, 404);
acceptsHtml(path, {name: 'valid', id: 'valid'}, 406);
acceptsHtml(path, {name: 'valid', id: 'missing'}, 406);

cascades(path, {name: 'valid', id: 'valid'}, cascadingData, cascadingTarget, cascadingDeepData);

updatesOther(path, path + '@latest', {name: 'valid', id: 'newId'}, data);
updatesOther(path + '@latest', path, {name: 'valid', id: 'newId'}, data);

Expand All @@ -120,17 +134,20 @@ describe(endpointName, function () {
describe('/components/:name/instances/:id@:version', function () {
var path = this.title;

acceptsJson(path, {name: 'invalid', version: 'abc', id: 'valid'}, 404, { code: 404, message: 'Not Found' });
acceptsJson(path, {name: 'valid', version: 'abc', id: 'valid'}, 200, {});
acceptsJson(path, {name: 'valid', version: 'abc', id: 'missing'}, 200, {});
acceptsJson(path, {name: 'invalid', version: 'def', id: 'valid'}, 404, { code: 404, message: 'Not Found' });
acceptsJson(path, {name: 'valid', version: 'def', id: 'valid'}, 200, {});
acceptsJson(path, {name: 'valid', version: 'def', id: 'missing'}, 200, {});

acceptsJsonBody(path, {name: 'invalid', version: 'def', id: 'valid'}, {}, 404, { code: 404, message: 'Not Found' });
acceptsJsonBody(path, {name: 'valid', version: 'def', id: 'valid'}, data, 200, data);
acceptsJsonBody(path, {name: 'missing', version: 'def', id: 'missing'}, data, 200, data);
acceptsJsonBody(path, {name: 'valid', version: 'def', id: 'valid'}, cascadingData, 200, cascadingReturnData);

acceptsHtml(path, {name: 'invalid', version: 'def', id: 'valid'}, 404);
acceptsHtml(path, {name: 'valid', version: 'def', id: 'valid'}, 406);
acceptsHtml(path, {name: 'valid', version: 'def', id: 'missing'}, 406);

acceptsHtml(path, {name: 'invalid', version: 'ghi', id: 'valid'}, 404);
acceptsHtml(path, {name: 'valid', version: 'ghi', id: 'valid'}, 406);
acceptsHtml(path, {name: 'valid', version: 'ghi', id: 'missing'}, 406);
cascades(path, {name: 'valid', version: 'def', id: 'valid'}, cascadingData, cascadingTarget, cascadingDeepData);
});
});
});
Loading