Implement Model.nestRemoting

Explicitly enable another level of nesting/accessing relations; limited
to a depth of 2 levels.
This commit is contained in:
Fabien Franzen 2014-08-04 18:27:50 +02:00
parent 0590eaf278
commit 9be8d11431
2 changed files with 232 additions and 2 deletions

View File

@ -4,6 +4,8 @@
var registry = require('../registry');
var assert = require('assert');
var SharedClass = require('strong-remoting').SharedClass;
var extend = require('util')._extend;
var stringUtils = require('underscore.string');
/**
* The base class for **all models**.
@ -203,7 +205,7 @@ Model.setup = function () {
}
}
});
return ModelCtor;
};
@ -469,7 +471,106 @@ Model.scopeRemoting = function(relationName, relation, define) {
http: {verb: 'delete', path: '/' + relationName},
description: 'Deletes all ' + relationName + ' of this model.'
});
}
};
Model.nestRemoting = function(relationName, options) {
var regExp = /^__([^_]+)__([^_]+)$/;
options = options || {};
var relation = this.relations[relationName];
if (relation && relation.modelTo && relation.modelTo.sharedClass) {
var self = this;
var sharedClass = this.sharedClass;
var sharedToClass = relation.modelTo.sharedClass;
var toModelName = relation.modelTo.modelName;
var pathName = options.pathName || relation.options.path || relationName;
var paramName = options.paramName || 'nk';
var http = [].concat(sharedToClass.http || [])[0];
if (relation.multiple) {
var httpPath = pathName + '/:' + paramName;
var acceptArgs = [
{
arg: paramName, type: 'any', http: { source: 'path' },
description: 'Foreign key for ' + relation.name,
required: true
}
];
} else {
var httpPath = pathName;
var acceptArgs = [];
}
sharedToClass.methods().forEach(function(method) {
var matches;
if (!method.isStatic && (matches = method.name.match(regExp))) {
var prefix = relation.multiple ? '__findById__' : '__get__';
var getterName = options.getterName || (prefix + relationName);
var getterFn = relation.modelFrom.prototype[getterName];
if (typeof getterFn !== 'function') {
throw new Error('Invalid remote method: `' + getterName + '`');
}
var nestedFn = relation.modelTo.prototype[method.name];
if (typeof nestedFn !== 'function') {
throw new Error('Invalid remote method: `' + method.name + '`');
}
var opts = {};
var methodName = '__' + matches[1] + '__' + relationName + '__' + matches[2];
opts.accepts = acceptArgs.concat(method.accepts || []);
opts.returns = [].concat(method.returns || []);
opts.description = method.description;
opts.rest = extend({}, method.rest || {});
opts.http = [];
var routes = [].concat(method.http || []);
routes.forEach(function(route) {
if (route.path) {
var copy = extend({}, route);
copy.path = httpPath + route.path;
opts.http.push(copy);
}
});
if (relation.multiple) {
sharedClass.defineMethod(methodName, opts, function(fkId) {
var args = Array.prototype.slice.call(arguments, 1);
var last = args[args.length - 1];
var cb = typeof last === 'function' ? last : null;
this[getterName](fkId, function(err, inst) {
if (err && cb) return cb(err);
if (inst instanceof relation.modelTo) {
nestedFn.apply(inst, args);
} else if (cb) {
cb(err, null);
}
});
}, method.isStatic);
} else {
sharedClass.defineMethod(methodName, opts, function() {
var args = Array.prototype.slice.call(arguments);
var last = args[args.length - 1];
var cb = typeof last === 'function' ? last : null;
this[getterName](function(err, inst) {
if (err && cb) return cb(err);
if (inst instanceof relation.modelTo) {
nestedFn.apply(inst, args);
} else if (cb) {
cb(err, null);
}
});
}, method.isStatic);
}
}
});
} else {
throw new Error('Relation `' + relationName + '` does not exist for model `' + this.modelName + '`');
}
};
// setup the initial model
Model.setup();

View File

@ -926,4 +926,133 @@ describe('relations - integration', function () {
});
describe('nested relations', function() {
before(function defineProductAndCategoryModels() {
var Book = app.model(
'Book',
{ properties: { name: 'string' }, dataSource: 'db',
plural: 'books' }
);
var Page = app.model(
'Page',
{ properties: { name: 'string' }, dataSource: 'db',
plural: 'pages' }
);
var Image = app.model(
'Image',
{ properties: { name: 'string' }, dataSource: 'db',
plural: 'images' }
);
var Note = app.model(
'Note',
{ properties: { text: 'string' }, dataSource: 'db',
plural: 'notes' }
);
Book.hasMany(Page);
Page.hasMany(Note);
Image.belongsTo(Book);
Book.nestRemoting('pages');
Image.nestRemoting('book');
expect(Book.prototype['__findById__pages__notes']).to.be.a.function;
expect(Image.prototype['__findById__book__pages']).to.be.a.function;
});
before(function createBook(done) {
var test = this;
app.models.Book.create({ name: 'Book 1' },
function(err, book) {
if (err) return done(err);
test.book = book;
book.pages.create({ name: 'Page 1' },
function(err, page) {
if (err) return done(err);
test.page = page;
page.notes.create({ text: 'Page Note 1' },
function(err, note) {
test.note = note;
done();
});
});
});
});
before(function createCover(done) {
var test = this;
app.models.Image.create({ name: 'Cover 1', book: test.book },
function(err, image) {
if (err) return done(err);
test.image = image;
done();
});
});
it('has regular relationship routes', function(done) {
var test = this;
this.get('/api/books/' + test.book.id + '/pages')
.expect(200, function(err, res) {
expect(res.body).to.be.an.array;
expect(res.body).to.have.length(1);
expect(res.body[0].name).to.equal('Page 1');
done();
});
});
it('has a basic error handler', function(done) {
var test = this;
this.get('/api/books/unknown/pages')
.expect(404, function(err, res) {
expect(res.body.error).to.be.an.object;
var expected = 'could not find a model with id unknown';
expect(res.body.error.message).to.equal(expected);
done();
});
});
it('enables nested relationship routes - belongsTo find', function(done) {
var test = this;
this.get('/api/images/' + test.image.id + '/book/pages')
.end(function(err, res) {
expect(res.body).to.be.an.array;
expect(res.body).to.have.length(1);
expect(res.body[0].name).to.equal('Page 1');
done();
});
});
it('enables nested relationship routes - belongsTo findById', function(done) {
var test = this;
this.get('/api/images/' + test.image.id + '/book/pages/' + test.page.id)
.end(function(err, res) {
expect(res.body).to.be.an.object;
expect(res.body.name).to.equal('Page 1');
done();
});
});
it('enables nested relationship routes - hasMany find', function(done) {
var test = this;
this.get('/api/books/' + test.book.id + '/pages/' + test.page.id + '/notes')
.expect(200, function(err, res) {
expect(res.body).to.be.an.array;
expect(res.body).to.have.length(1);
expect(res.body[0].text).to.equal('Page Note 1');
done();
});
});
it('enables nested relationship routes - hasMany findById', function(done) {
var test = this;
this.get('/api/books/' + test.book.id + '/pages/' + test.page.id + '/notes/' + test.note.id)
.expect(200, function(err, res) {
expect(res.body).to.be.an.object;
expect(res.body.text).to.equal('Page Note 1');
done();
});
});
});
});