Merge pull request #435 from fabien/feature/deep-remoting
Implement Model.nestRemoting
This commit is contained in:
commit
a9e77e0e23
|
@ -270,6 +270,7 @@ app.remoteObjects = function () {
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Get a handler of the specified type from the handler cache.
|
* Get a handler of the specified type from the handler cache.
|
||||||
|
* @triggers `mounted` events on shared class constructors (models)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
app.handler = function (type) {
|
app.handler = function (type) {
|
||||||
|
@ -280,6 +281,11 @@ app.handler = function (type) {
|
||||||
|
|
||||||
var remotes = this.remotes();
|
var remotes = this.remotes();
|
||||||
var handler = this._handlers[type] = remotes.handler(type);
|
var handler = this._handlers[type] = remotes.handler(type);
|
||||||
|
|
||||||
|
remotes.classes().forEach(function(sharedClass) {
|
||||||
|
sharedClass.ctor.emit('mounted', app, sharedClass, remotes);
|
||||||
|
});
|
||||||
|
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
var registry = require('../registry');
|
var registry = require('../registry');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var SharedClass = require('strong-remoting').SharedClass;
|
var SharedClass = require('strong-remoting').SharedClass;
|
||||||
|
var extend = require('util')._extend;
|
||||||
|
var stringUtils = require('underscore.string');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base class for **all models**.
|
* The base class for **all models**.
|
||||||
|
@ -205,7 +207,7 @@ Model.setup = function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return ModelCtor;
|
return ModelCtor;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -487,7 +489,151 @@ Model.scopeRemoting = function(relationName, relation, define) {
|
||||||
http: {verb: 'delete', path: '/' + pathName},
|
http: {verb: 'delete', path: '/' + pathName},
|
||||||
description: 'Deletes all ' + relationName + ' of this model.'
|
description: 'Deletes all ' + relationName + ' of this model.'
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Model.nestRemoting = function(relationName, options, cb) {
|
||||||
|
if (typeof options === 'function' && !cb) {
|
||||||
|
cb = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
var regExp = /^__([^_]+)__([^_]+)$/;
|
||||||
|
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 = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// A method should return the method name to use, if it is to be
|
||||||
|
// included as a nested method - a falsy return value will skip.
|
||||||
|
var filter = cb || options.filterMethod || function(method, relation) {
|
||||||
|
var matches = method.name.match(regExp);
|
||||||
|
if (matches) {
|
||||||
|
return '__' + matches[1] + '__' + relation.name + '__' + matches[2];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sharedToClass.methods().forEach(function(method) {
|
||||||
|
var methodName;
|
||||||
|
if (!method.isStatic && (methodName = filter(method, relation))) {
|
||||||
|
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 = {};
|
||||||
|
|
||||||
|
opts.accepts = acceptArgs.concat(method.accepts || []);
|
||||||
|
opts.returns = [].concat(method.returns || []);
|
||||||
|
opts.description = method.description;
|
||||||
|
opts.rest = extend({}, method.rest || {});
|
||||||
|
opts.rest.delegateTo = method.name;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.hooks === false) return; // don't inherit before/after hooks
|
||||||
|
|
||||||
|
self.once('mounted', function(app, sc, remotes) {
|
||||||
|
var listenerTree = extend({}, remotes.listenerTree || {});
|
||||||
|
listenerTree.before = listenerTree.before || {};
|
||||||
|
listenerTree.after = listenerTree.after || {};
|
||||||
|
|
||||||
|
var beforeListeners = remotes.listenerTree.before[toModelName] || {};
|
||||||
|
var afterListeners = remotes.listenerTree.after[toModelName] || {};
|
||||||
|
|
||||||
|
sharedClass.methods().forEach(function(method) {
|
||||||
|
var delegateTo = method.rest && method.rest.delegateTo;
|
||||||
|
if (delegateTo) {
|
||||||
|
var before = method.isStatic ? beforeListeners : beforeListeners['prototype'];
|
||||||
|
var after = method.isStatic ? afterListeners : afterListeners['prototype'];
|
||||||
|
var m = method.isStatic ? method.name : 'prototype.' + method.name;
|
||||||
|
if (before[delegateTo]) {
|
||||||
|
self.beforeRemote(m, function(ctx, result, next) {
|
||||||
|
before[delegateTo]._listeners.call(null, ctx, next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (after[delegateTo]) {
|
||||||
|
self.afterRemote(m, function(ctx, result, next) {
|
||||||
|
after[delegateTo]._listeners.call(null, ctx, next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error('Relation `' + relationName + '` does not exist for model `' + this.modelName + '`');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// setup the initial model
|
// setup the initial model
|
||||||
Model.setup();
|
Model.setup();
|
||||||
|
|
|
@ -947,4 +947,158 @@ 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;
|
||||||
|
|
||||||
|
Page.beforeRemote('prototype.__findById__notes', function(ctx, result, next) {
|
||||||
|
ctx.res.set('x-before', 'before');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
Page.afterRemote('prototype.__findById__notes', function(ctx, result, next) {
|
||||||
|
ctx.res.set('x-after', 'after');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
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 - pages', 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 regular relationship routes - notes', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.get('/api/pages/' + test.page.id + '/notes/' + test.note.id)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.headers['x-before']).to.equal('before');
|
||||||
|
expect(res.headers['x-after']).to.equal('after');
|
||||||
|
expect(res.body).to.be.an.object;
|
||||||
|
expect(res.body.text).to.equal('Page Note 1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a basic error handler', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.get('/api/books/unknown/pages/' + test.page.id + '/notes')
|
||||||
|
.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.headers['x-before']).to.equal('before');
|
||||||
|
expect(res.headers['x-after']).to.equal('after');
|
||||||
|
expect(res.body).to.be.an.object;
|
||||||
|
expect(res.body.text).to.equal('Page Note 1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue