- Use the RemoteObjects class to find remote objects instead of

creating a cache
 - Use the SharedClass class to build the remote connector
 - Change default base model from Model to DataModel
 - Fix DataModel errors not logging correct method names
 - Use the strong-remoting 1.4 resolver API to resolve dynamic remote
methods (relation api)
 - Remove use of fn object for storing remoting meta data
This commit is contained in:
Ritchie Martori 2014-05-19 15:56:26 -07:00
parent 558ea60da0
commit eec7bdd5f4
15 changed files with 174 additions and 93 deletions

35
CHANGES.md Normal file
View File

@ -0,0 +1,35 @@
# Breaking Changes
# 1.9
## Remote Method API
`loopback.remoteMethod()` is now deprecated.
Defining remote methods now should be done like this:
```js
// static
MyModel.greet = function(msg, cb) {
cb(null, 'greetings... ' + msg);
}
MyModel.remoteMethod(
'greet',
{
accepts: [{arg: 'msg', type: 'string'}],
returns: {arg: 'greeting', type: 'string'}
}
);
```
**NOTE: remote instance method support is also now deprecated...
Use static methods instead. If you absolutely need it you can still set
`options.isStatic = false`** We plan to drop support for instance methods in
`2.0`.
## Remote Instance Methods
All remote instance methods have been replaced with static replacements.
The REST API is backwards compatible.

View File

@ -108,7 +108,9 @@ app.model = function (Model, config) {
assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor');
assert(Model.modelName, 'Model must have a "modelName" property');
var remotingClassName = compat.getClassNameForRemoting(Model);
this.remotes().exports[remotingClassName] = Model;
if(Model.sharedClass) {
this.remotes().addClass(Model.sharedClass);
}
this.models().push(Model);
clearHandlerCache(this);
Model.shared = true;
@ -205,14 +207,9 @@ app.dataSource = function (name, config) {
app.remoteObjects = function () {
var result = {};
var models = this.models();
// add in models
models.forEach(function (ModelCtor) {
// only add shared models
if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') {
result[compat.getClassNameForRemoting(ModelCtor)] = ModelCtor;
}
this.remotes().classes().forEach(function(sharedClass) {
result[sharedClass.name] = sharedClass.ctor;
});
return result;

View File

@ -52,53 +52,40 @@ RemoteConnector.initialize = function(dataSource, callback) {
RemoteConnector.prototype.define = function(definition) {
var Model = definition.model;
var className = compat.getClassNameForRemoting(Model);
var remotes = this.remotes
var remotes = this.remotes;
var SharedClass;
var classes;
var i = 0;
remotes.exports[className] = Model;
assert(Model.sharedClass, 'cannot attach ' + Model.modelName
+ ' to a remote connector without a Model.sharedClass');
classes = remotes.classes();
for(; i < classes.length; i++) {
SharedClass = classes[i];
if(SharedClass.name === className) {
SharedClass
.methods()
.forEach(function(remoteMethod) {
// TODO(ritch) more elegant way of ignoring a nested shared class
if(remoteMethod.name !== 'Change'
&& remoteMethod.name !== 'Checkpoint') {
createProxyMethod(Model, remotes, remoteMethod);
}
});
remotes.addClass(Model.sharedClass);
return;
}
}
Model
.sharedClass
.methods()
.forEach(function(remoteMethod) {
// TODO(ritch) more elegant way of ignoring a nested shared class
if(remoteMethod.name !== 'Change'
&& remoteMethod.name !== 'Checkpoint') {
createProxyMethod(Model, remotes, remoteMethod);
}
});
}
function createProxyMethod(Model, remotes, remoteMethod) {
var scope = remoteMethod.isStatic ? Model : Model.prototype;
var original = scope[remoteMethod.name];
var fn = scope[remoteMethod.name] = function remoteMethodProxy() {
scope[remoteMethod.name] = function remoteMethodProxy() {
var args = Array.prototype.slice.call(arguments);
var lastArgIsFunc = typeof args[args.length - 1] === 'function';
var callback;
if(lastArgIsFunc) {
callback = args.pop();
}
remotes.invoke(remoteMethod.stringName, args, callback);
}
for(var key in original) {
fn[key] = original[key];
}
fn._delegate = true;
}
function noop() {}

View File

@ -155,7 +155,7 @@ loopback.createModel = function (name, properties, options) {
BaseModel = loopback.getModel(BaseModel);
}
BaseModel = BaseModel || loopback.Model;
BaseModel = BaseModel || loopback.DataModel;
var model = BaseModel.extend(name, properties, options);

View File

@ -94,7 +94,7 @@ Change.rectifyModelChanges = function(modelName, modelIds, callback) {
modelIds.forEach(function(id) {
tasks.push(function(cb) {
Change.findOrCreate(modelName, id, function(err, change) {
Change.findOrCreateChange(modelName, id, function(err, change) {
if(err) return Change.handleError(err, cb);
change.rectify(cb);
});
@ -126,7 +126,7 @@ Change.idForModel = function(modelName, modelId) {
* @end
*/
Change.findOrCreate = function(modelName, modelId, callback) {
Change.findOrCreateChange = function(modelName, modelId, callback) {
assert(loopback.getModel(modelName), modelName + ' does not exist');
var id = this.idForModel(modelName, modelId);
var Change = this;

View File

@ -51,9 +51,8 @@ DataModel.setup = function setupDataModel() {
DataModel.enableChangeTracking();
});
}
DataModel.on('dataSourceAttached', function() {
DataModel.setupRemoting();
});
DataModel.setupRemoting();
}
/*!
@ -107,7 +106,7 @@ DataModel.create = function (data, callback) {
*/
DataModel.upsert = DataModel.updateOrCreate = function upsert(data, callback) {
throwNotAttached(this.modelName, 'updateOrCreate');
throwNotAttached(this.modelName, 'upsert');
};
/**
@ -142,7 +141,7 @@ DataModel.exists = function exists(id, cb) {
*/
DataModel.findById = function find(id, cb) {
throwNotAttached(this.modelName, 'find');
throwNotAttached(this.modelName, 'findById');
};
/**
@ -190,9 +189,6 @@ DataModel.destroyAll = function destroyAll(where, cb) {
throwNotAttached(this.modelName, 'destroyAll');
};
// disable remoting by default
DataModel.destroyAll.shared = false;
/**
* Destroy a record by id
* @param {*} id The id value
@ -405,9 +401,9 @@ DataModel.setupRemoting = function() {
var typeName = DataModel.modelName;
var options = DataModel.settings;
function setRemoting(target, name, options) {
var fn = target[name];
options.prototype = target === DataModel.prototype;
function setRemoting(scope, name, options) {
var fn = scope[name];
options.isStatic = scope === DataModel;
DataModel.remoteMethod(name, options);
}

View File

@ -10,6 +10,7 @@ var modeler = new ModelBuilder();
var async = require('async');
var assert = require('assert');
var _ = require('underscore');
var SharedClass = require('strong-remoting').SharedClass;
/**
* The base class for **all models**.
@ -89,10 +90,18 @@ var Model = module.exports = modeler.define('Model');
Model.setup = function () {
var ModelCtor = this;
var options = this.settings;
ModelCtor.sharedClass = new SharedClass(remoteNamespace, ModelCtor);
// create a sharedClass
var sharedClass = ModelCtor.sharedClass = new SharedClass(
compat.getClassNameForRemoting(ModelCtor),
ModelCtor,
options.remoting
);
// support remoting prototype methods
ModelCtor.sharedCtor = function (data, id, fn) {
var ModelCtor = this;
if(typeof data === 'function') {
fn = data;
data = null;
@ -132,6 +141,18 @@ Model.setup = function () {
}
}
var idDesc = ModelCtor.modelName + ' id';
ModelCtor.sharedCtor.accepts = [
{arg: 'id', type: 'any', http: {source: 'path'}, description: idDesc}
// {arg: 'instance', type: 'object', http: {source: 'body'}}
];
ModelCtor.sharedCtor.http = [
{path: '/:id'}
];
ModelCtor.sharedCtor.returns = {root: true};
// before remote hook
ModelCtor.beforeRemote = function (name, fn) {
var self = this;
@ -166,18 +187,20 @@ Model.setup = function () {
}
};
// Map the prototype method to /:id with data in the body
var idDesc = ModelCtor.modelName + ' id';
ModelCtor.sharedCtor.accepts = [
{arg: 'id', type: 'any', http: {source: 'path'}, description: idDesc}
// {arg: 'instance', type: 'object', http: {source: 'body'}}
];
ModelCtor.sharedCtor.http = [
{path: '/:id'}
];
ModelCtor.sharedCtor.returns = {root: true};
// resolve relation functions
sharedClass.resolve(function resolver(define) {
var relations = ModelCtor.relations;
if(!relations) return;
// get the relations
for(var relationName in relations) {
var relation = relations[relationName];
if(relation.type === 'belongsTo') {
ModelCtor.belongsToRemoting(relationName, relation, define)
} else {
ModelCtor.scopeRemoting(relationName, relation, define);
}
}
});
return ModelCtor;
};
@ -297,15 +320,53 @@ Model.getApp = function(callback) {
* ```js
* // static method example (eg. Model.myMethod())
* Model.remoteMethod('myMethod');
* // instance method exampe (eg. Model.prototype.myMethod())
* Model.remoteMethod('prototype.myMethod', {prototype: true});
* @param {Object} options The remoting options.
* See [loopback.remoteMethod()](http://docs.strongloop.com/display/DOC/Remote+methods+and+hooks#Remotemethodsandhooks-loopback.remoteMethod(fn,[options])) for details.
*/
Model.remoteMethod = function(name, options) {
if(options.isStatic === undefined) {
options.isStatic = true;
}
this.sharedClass.defineMethod(name, options);
}
Model.belongsToRemoting = function(relationName, relation, define) {
var fn = this.prototype[relationName];
define(relationName, {
isStatic: false,
http: {verb: 'get', path: '/' + relationName},
accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
description: 'Fetches belongsTo relation ' + relationName,
returns: {arg: relationName, type: relation.modelTo.modelName, root: true}
}, fn);
}
Model.scopeRemoting = function(relationName, relation, define) {
var toModelName = relation.modelTo.modelName;
define('__get__' + relationName, {
isStatic: false,
http: {verb: 'get', path: '/' + relationName},
accepts: {arg: 'filter', type: 'object'},
description: 'Queries ' + relationName + ' of ' + this.modelName + '.',
returns: {arg: relationName, type: [toModelName], root: true}
});
define('__create__' + relationName, {
isStatic: false,
http: {verb: 'post', path: '/' + relationName},
accepts: {arg: 'data', type: 'object', http: {source: 'body'}},
description: 'Creates a new instance in ' + relationName + ' of this model.',
returns: {arg: 'data', type: toModelName, root: true}
});
define('__delete__' + relationName, {
isStatic: false,
http: {verb: 'delete', path: '/' + relationName},
description: 'Deletes all ' + relationName + ' of this model.'
});
}
// setup the initial model
Model.setup();

View File

@ -16,7 +16,7 @@
"dependencies": {
"debug": "~0.7.4",
"express": "~3.4.8",
"strong-remoting": "~1.3.1",
"strong-remoting": "~1.4.0",
"inflection": "~1.3.5",
"passport": "~0.2.0",
"passport-local": "~0.1.6",

View File

@ -31,7 +31,8 @@ describe('app', function() {
var Color = DataModel.extend('color', {name: String});
app.model(Color);
Color.attachTo(db);
expect(app.remotes().exports).to.eql({ color: Color });
var classes = app.remotes().classes().map(function(c) {return c.name});
expect(classes).to.contain('color');
});
it('updates REST API when a new model is added', function(done) {
@ -56,7 +57,8 @@ describe('app', function() {
it('uses plural name as shared class name', function() {
var Color = db.createModel('color', {name: String});
app.model(Color);
expect(app.remotes().exports).to.eql({ colors: Color });
var classes = app.remotes().classes().map(function(c) {return c.name});
expect(classes).to.contain('colors');
});
it('uses plural name as app.remoteObjects() key', function() {

View File

@ -48,14 +48,16 @@ describe('Change', function(){
var test = this;
Change.rectifyModelChanges(this.modelName, [this.modelId], function(err, trackedChanges) {
if(err) return done(err);
test.trackedChanges = trackedChanges;
done();
});
});
it('should create an entry', function () {
assert(Array.isArray(this.trackedChanges));
assert.equal(this.trackedChanges[0].modelId, this.modelId);
it('should create an entry', function (done) {
var test = this;
Change.find(function(err, trackedChanges) {
assert.equal(trackedChanges[0].modelId, test.modelId.toString());
done();
});
});
it('should only create one change', function (done) {
@ -67,12 +69,12 @@ describe('Change', function(){
});
});
describe('Change.findOrCreate(modelName, modelId, callback)', function () {
describe('Change.findOrCreateChange(modelName, modelId, callback)', function () {
describe('when a change doesnt exist', function () {
beforeEach(function(done) {
var test = this;
Change.findOrCreate(this.modelName, this.modelId, function(err, result) {
Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) {
if(err) return done(err);
test.result = result;
done();
@ -102,7 +104,7 @@ describe('Change', function(){
beforeEach(function(done) {
var test = this;
Change.findOrCreate(this.modelName, this.modelId, function(err, result) {
Change.findOrCreateChange(this.modelName, this.modelId, function(err, result) {
if(err) return done(err);
test.result = result;
done();

View File

@ -36,7 +36,7 @@ describe('DataSource', function() {
});
});
describe('DataModel Methods', function() {
describe.skip('DataModel Methods', function() {
it("List the enabled and disabled methods", function() {
var TestModel = loopback.DataModel.extend('TestDataModel');
TestModel.attachTo(loopback.memory());
@ -61,16 +61,18 @@ describe('DataSource', function() {
existsAndShared(TestModel, 'belongsTo', false);
existsAndShared(TestModel, 'hasAndBelongsToMany', false);
// existsAndShared(TestModel.prototype, 'updateAttributes', true);
existsAndShared(TestModel.prototype, 'save', false);
existsAndShared(TestModel.prototype, 'isNewRecord', false);
existsAndShared(TestModel.prototype, '_adapter', false);
existsAndShared(TestModel.prototype, 'destroy', false);
existsAndShared(TestModel.prototype, 'reload', false);
existsAndShared(TestModel, 'save', false, true);
existsAndShared(TestModel, 'isNewRecord', false, true);
existsAndShared(TestModel, '_adapter', false, true);
existsAndShared(TestModel, 'destroy', false, true);
existsAndShared(TestModel, 'reload', false, true);
function existsAndShared(scope, name, isRemoteEnabled) {
function existsAndShared(Model, name, isRemoteEnabled, isProto) {
var scope = isProto ? Model.prototype : Model;
var fn = scope[name];
var actuallyEnabled = Model.getRemoteMethod(name);
assert(fn, name + ' should be defined!');
assert(!!fn.shared === isRemoteEnabled, name + ' ' + (isRemoteEnabled ? 'should' : 'should not') + ' be remote enabled');
assert(actuallyEnabled === isRemoteEnabled, name + ' ' + (isRemoteEnabled ? 'should' : 'should not') + ' be remote enabled');
}
});
});

View File

@ -53,11 +53,11 @@ describe('Model / DataModel', function() {
connector: loopback.Memory
});
assert(MyModel.find === undefined, 'should not have data access methods');
MyModel.attachTo(dataSource);
assert(typeof MyModel.find === 'function', 'should have data access methods after attaching to a data source');
MyModel.find(function(err, results) {
assert(results.length === 0, 'should have data access methods after attaching to a data source');
});
});
});
});

View File

@ -158,7 +158,6 @@ describe('relations - integration', function () {
it.skip('allows to find related objects via where filter', function(done) {
//TODO https://github.com/strongloop/loopback-datasource-juggler/issues/94
var expectedProduct = this.product;
// Note: the URL format is not final
this.get('/api/products?filter[where][categoryId]=' + this.category.id)
.expect(200, function(err, res) {
if (err) return done(err);

View File

@ -340,4 +340,4 @@ describe('Replication / Change APIs', function() {
assert(!this.conflict);
});
});
});
});

View File

@ -198,10 +198,10 @@ describe('Model Tests', function() {
});
});
describe('Model.deleteById([callback])', function () {
describe('Model.removeById(id, [callback])', function () {
it("Delete a model instance from the attached data source", function (done) {
User.create({first: 'joe', last: 'bob'}, function (err, user) {
User.deleteById(user.id, function (err) {
User.removeById(user.id, function (err) {
User.findById(user.id, function (err, notFound) {
assert.equal(notFound, null);
done();