Merge branch 'fabien-feature/dynamic-scope'
This commit is contained in:
commit
a6d59a2ba0
|
@ -5,6 +5,7 @@ var assert = require('assert');
|
||||||
var util = require('util');
|
var util = require('util');
|
||||||
var i8n = require('inflection');
|
var i8n = require('inflection');
|
||||||
var defineScope = require('./scope.js').defineScope;
|
var defineScope = require('./scope.js').defineScope;
|
||||||
|
var mergeQuery = require('./scope.js').mergeQuery;
|
||||||
var ModelBaseClass = require('./model.js');
|
var ModelBaseClass = require('./model.js');
|
||||||
|
|
||||||
exports.Relation = Relation;
|
exports.Relation = Relation;
|
||||||
|
@ -68,6 +69,8 @@ function RelationDefinition(definition) {
|
||||||
this.modelThrough = definition.modelThrough;
|
this.modelThrough = definition.modelThrough;
|
||||||
this.keyThrough = definition.keyThrough;
|
this.keyThrough = definition.keyThrough;
|
||||||
this.multiple = (this.type !== 'belongsTo' && this.type !== 'hasOne');
|
this.multiple = (this.type !== 'belongsTo' && this.type !== 'hasOne');
|
||||||
|
this.properties = definition.properties || {};
|
||||||
|
this.scope = definition.scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
RelationDefinition.prototype.toJSON = function () {
|
RelationDefinition.prototype.toJSON = function () {
|
||||||
|
@ -87,6 +90,41 @@ RelationDefinition.prototype.toJSON = function () {
|
||||||
return json;
|
return json;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the configured scope to the filter/query object.
|
||||||
|
* @param {Object} modelInstance
|
||||||
|
* @param {Object} filter (where, order, limit, fields, ...)
|
||||||
|
*/
|
||||||
|
RelationDefinition.prototype.applyScope = function(modelInstance, filter) {
|
||||||
|
if (typeof this.scope === 'function') {
|
||||||
|
var scope = this.scope.call(this, modelInstance, filter);
|
||||||
|
if (typeof scope === 'object') {
|
||||||
|
mergeQuery(filter, scope);
|
||||||
|
}
|
||||||
|
} else if (typeof this.scope === 'object') {
|
||||||
|
mergeQuery(filter, this.scope);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the configured properties to the target object.
|
||||||
|
* @param {Object} modelInstance
|
||||||
|
* @param {Object} target
|
||||||
|
*/
|
||||||
|
RelationDefinition.prototype.applyProperties = function(modelInstance, target) {
|
||||||
|
if (typeof this.properties === 'function') {
|
||||||
|
var data = this.properties.call(this, modelInstance);
|
||||||
|
for(var k in data) {
|
||||||
|
target[k] = data[k];
|
||||||
|
}
|
||||||
|
} else if (typeof this.properties === 'object') {
|
||||||
|
for(var k in this.properties) {
|
||||||
|
var key = this.properties[k];
|
||||||
|
target[key] = modelInstance[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A relation attaching to a given model instance
|
* A relation attaching to a given model instance
|
||||||
* @param {RelationDefinition|Object} definition
|
* @param {RelationDefinition|Object} definition
|
||||||
|
@ -323,7 +361,9 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
|
||||||
keyFrom: idName,
|
keyFrom: idName,
|
||||||
keyTo: fk,
|
keyTo: fk,
|
||||||
modelTo: modelTo,
|
modelTo: modelTo,
|
||||||
multiple: true
|
multiple: true,
|
||||||
|
properties: params.properties,
|
||||||
|
scope: params.scope
|
||||||
});
|
});
|
||||||
|
|
||||||
if (params.through) {
|
if (params.through) {
|
||||||
|
@ -358,6 +398,9 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
|
||||||
var filter = {};
|
var filter = {};
|
||||||
filter.where = {};
|
filter.where = {};
|
||||||
filter.where[fk] = this[idName];
|
filter.where[fk] = this[idName];
|
||||||
|
|
||||||
|
definition.applyScope(this, filter);
|
||||||
|
|
||||||
if (params.through) {
|
if (params.through) {
|
||||||
filter.collect = i8n.camelize(modelTo.modelName, true);
|
filter.collect = i8n.camelize(modelTo.modelName, true);
|
||||||
filter.include = filter.collect;
|
filter.include = filter.collect;
|
||||||
|
@ -384,42 +427,33 @@ HasMany.prototype.findById = function (id, cb) {
|
||||||
var fk = this.definition.keyTo;
|
var fk = this.definition.keyTo;
|
||||||
var pk = this.definition.keyFrom;
|
var pk = this.definition.keyFrom;
|
||||||
var modelInstance = this.modelInstance;
|
var modelInstance = this.modelInstance;
|
||||||
modelTo.findById(id, function (err, inst) {
|
var idName = this.definition.modelTo.definition.idName();
|
||||||
|
var filter = {};
|
||||||
|
filter.where = {};
|
||||||
|
filter.where[idName] = id;
|
||||||
|
filter.where[fk] = modelInstance[pk];
|
||||||
|
|
||||||
|
this.definition.applyScope(modelInstance, filter);
|
||||||
|
|
||||||
|
modelTo.findOne(filter, function (err, inst) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
if (!inst) {
|
if (!inst) {
|
||||||
return cb(new Error('Not found'));
|
return cb(new Error('Not found'));
|
||||||
}
|
}
|
||||||
// Check if the foreign key matches the primary key
|
cb(null, inst);
|
||||||
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
|
|
||||||
cb(null, inst);
|
|
||||||
} else {
|
|
||||||
cb(new Error('Permission denied: foreign key does not match the primary key'));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
HasMany.prototype.destroyById = function (id, cb) {
|
HasMany.prototype.destroyById = function (id, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var modelTo = this.definition.modelTo;
|
this.findById(id, function(err, inst) {
|
||||||
var fk = this.definition.keyTo;
|
|
||||||
var pk = this.definition.keyFrom;
|
|
||||||
var modelInstance = this.modelInstance;
|
|
||||||
modelTo.findById(id, function (err, inst) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
if (!inst) {
|
self.removeFromCache(inst[fk]);
|
||||||
return cb(new Error('Not found'));
|
inst.destroy(cb);
|
||||||
}
|
|
||||||
// Check if the foreign key matches the primary key
|
|
||||||
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
|
|
||||||
self.removeFromCache(inst[fk]);
|
|
||||||
inst.destroy(cb);
|
|
||||||
} else {
|
|
||||||
cb(new Error('Permission denied: foreign key does not match the primary key'));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -451,6 +485,9 @@ HasManyThrough.prototype.create = function create(data, done) {
|
||||||
var d = {};
|
var d = {};
|
||||||
d[fk1] = modelInstance[definition.keyFrom];
|
d[fk1] = modelInstance[definition.keyFrom];
|
||||||
d[fk2] = to[pk2];
|
d[fk2] = to[pk2];
|
||||||
|
|
||||||
|
definition.applyProperties(modelInstance, d);
|
||||||
|
|
||||||
// Then create the through model
|
// Then create the through model
|
||||||
modelThrough.create(d, function (e, through) {
|
modelThrough.create(d, function (e, through) {
|
||||||
if (e) {
|
if (e) {
|
||||||
|
@ -490,11 +527,16 @@ HasManyThrough.prototype.add = function (acInst, done) {
|
||||||
query[fk1] = this.modelInstance[pk1];
|
query[fk1] = this.modelInstance[pk1];
|
||||||
query[fk2] = acInst[pk2] || acInst;
|
query[fk2] = acInst[pk2] || acInst;
|
||||||
|
|
||||||
|
var filter = { where: query };
|
||||||
|
|
||||||
|
definition.applyScope(this.modelInstance, filter);
|
||||||
|
|
||||||
data[fk1] = this.modelInstance[pk1];
|
data[fk1] = this.modelInstance[pk1];
|
||||||
data[fk2] = acInst[pk2] || acInst;
|
data[fk2] = acInst[pk2] || acInst;
|
||||||
|
definition.applyProperties(this.modelInstance, data);
|
||||||
|
|
||||||
// Create an instance of the through model
|
// Create an instance of the through model
|
||||||
modelThrough.findOrCreate({where: query}, data, function(err, ac) {
|
modelThrough.findOrCreate(filter, data, function(err, ac) {
|
||||||
if(!err) {
|
if(!err) {
|
||||||
if (acInst instanceof definition.modelTo) {
|
if (acInst instanceof definition.modelTo) {
|
||||||
self.addToCache(acInst);
|
self.addToCache(acInst);
|
||||||
|
@ -527,7 +569,11 @@ HasManyThrough.prototype.remove = function (acInst, done) {
|
||||||
query[fk1] = this.modelInstance[pk1];
|
query[fk1] = this.modelInstance[pk1];
|
||||||
query[fk2] = acInst[pk2] || acInst;
|
query[fk2] = acInst[pk2] || acInst;
|
||||||
|
|
||||||
modelThrough.deleteAll(query, function (err) {
|
var filter = { where: query };
|
||||||
|
|
||||||
|
definition.applyScope(this.modelInstance, filter);
|
||||||
|
|
||||||
|
modelThrough.deleteAll(filter.where, function (err) {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
self.removeFromCache(query[fk2]);
|
self.removeFromCache(query[fk2]);
|
||||||
}
|
}
|
||||||
|
@ -616,7 +662,6 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
|
||||||
fn.returns = {arg: relationName, type: 'object', root: true};
|
fn.returns = {arg: relationName, type: 'object', root: true};
|
||||||
|
|
||||||
modelFrom.prototype['__get__' + relationName] = fn;
|
modelFrom.prototype['__get__' + relationName] = fn;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
BelongsTo.prototype.create = function(targetModelData, cb) {
|
BelongsTo.prototype.create = function(targetModelData, cb) {
|
||||||
|
@ -795,7 +840,8 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) {
|
||||||
modelFrom: modelFrom,
|
modelFrom: modelFrom,
|
||||||
keyFrom: pk,
|
keyFrom: pk,
|
||||||
keyTo: fk,
|
keyTo: fk,
|
||||||
modelTo: modelTo
|
modelTo: modelTo,
|
||||||
|
properties: params.properties
|
||||||
});
|
});
|
||||||
|
|
||||||
modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName);
|
modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName);
|
||||||
|
@ -835,7 +881,11 @@ HasOne.prototype.create = function (targetModelData, cb) {
|
||||||
targetModelData = targetModelData || {};
|
targetModelData = targetModelData || {};
|
||||||
targetModelData[fk] = modelInstance[pk];
|
targetModelData[fk] = modelInstance[pk];
|
||||||
var query = {where: {}};
|
var query = {where: {}};
|
||||||
query.where[fk] = targetModelData[fk]
|
query.where[fk] = targetModelData[fk];
|
||||||
|
|
||||||
|
this.definition.applyScope(modelInstance, query);
|
||||||
|
this.definition.applyProperties(modelInstance, targetModelData);
|
||||||
|
|
||||||
modelTo.findOne(query, function(err, result) {
|
modelTo.findOne(query, function(err, result) {
|
||||||
if(err) {
|
if(err) {
|
||||||
cb(err);
|
cb(err);
|
||||||
|
@ -876,6 +926,9 @@ HasMany.prototype.create = function (targetModelData, cb) {
|
||||||
}
|
}
|
||||||
targetModelData = targetModelData || {};
|
targetModelData = targetModelData || {};
|
||||||
targetModelData[fk] = modelInstance[pk];
|
targetModelData[fk] = modelInstance[pk];
|
||||||
|
|
||||||
|
this.definition.applyProperties(modelInstance, targetModelData);
|
||||||
|
|
||||||
modelTo.create(targetModelData, function(err, targetModel) {
|
modelTo.create(targetModelData, function(err, targetModel) {
|
||||||
if(!err) {
|
if(!err) {
|
||||||
// Refresh the cache
|
// Refresh the cache
|
||||||
|
@ -895,8 +948,12 @@ HasMany.prototype.build = HasOne.prototype.build = function(targetModelData) {
|
||||||
var modelTo = this.definition.modelTo;
|
var modelTo = this.definition.modelTo;
|
||||||
var pk = this.definition.keyFrom;
|
var pk = this.definition.keyFrom;
|
||||||
var fk = this.definition.keyTo;
|
var fk = this.definition.keyTo;
|
||||||
|
|
||||||
targetModelData = targetModelData || {};
|
targetModelData = targetModelData || {};
|
||||||
targetModelData[fk] = this.modelInstance[pk];
|
targetModelData[fk] = this.modelInstance[pk];
|
||||||
|
|
||||||
|
this.definition.applyProperties(this.modelInstance, targetModelData);
|
||||||
|
|
||||||
return new modelTo(targetModelData);
|
return new modelTo(targetModelData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -916,6 +973,7 @@ HasOne.prototype.related = function (refresh, params) {
|
||||||
var modelTo = this.definition.modelTo;
|
var modelTo = this.definition.modelTo;
|
||||||
var fk = this.definition.keyTo;
|
var fk = this.definition.keyTo;
|
||||||
var pk = this.definition.keyFrom;
|
var pk = this.definition.keyFrom;
|
||||||
|
var definition = this.definition;
|
||||||
var modelInstance = this.modelInstance;
|
var modelInstance = this.modelInstance;
|
||||||
|
|
||||||
if (arguments.length === 1) {
|
if (arguments.length === 1) {
|
||||||
|
@ -937,6 +995,7 @@ HasOne.prototype.related = function (refresh, params) {
|
||||||
if (cachedValue === undefined) {
|
if (cachedValue === undefined) {
|
||||||
var query = {where: {}};
|
var query = {where: {}};
|
||||||
query.where[fk] = modelInstance[pk];
|
query.where[fk] = modelInstance[pk];
|
||||||
|
definition.applyScope(modelInstance, query);
|
||||||
modelTo.findOne(query, function (err, inst) {
|
modelTo.findOne(query, function (err, inst) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
|
|
@ -5,6 +5,7 @@ var defineCachedRelations = utils.defineCachedRelations;
|
||||||
* Module exports
|
* Module exports
|
||||||
*/
|
*/
|
||||||
exports.defineScope = defineScope;
|
exports.defineScope = defineScope;
|
||||||
|
exports.mergeQuery = mergeQuery;
|
||||||
|
|
||||||
function ScopeDefinition(definition) {
|
function ScopeDefinition(definition) {
|
||||||
this.sourceModel = definition.sourceModel;
|
this.sourceModel = definition.sourceModel;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// This test written in mocha+should.js
|
// This test written in mocha+should.js
|
||||||
var should = require('./init.js');
|
var should = require('./init.js');
|
||||||
|
|
||||||
var db, Book, Chapter, Author, Reader, Publisher;
|
var db, Book, Chapter, Author, Reader;
|
||||||
|
var Category, Product;
|
||||||
|
|
||||||
describe('relations', function () {
|
describe('relations', function () {
|
||||||
before(function (done) {
|
before(function (done) {
|
||||||
|
@ -10,6 +11,8 @@ describe('relations', function () {
|
||||||
Chapter = db.define('Chapter', {name: {type: String, index: true}});
|
Chapter = db.define('Chapter', {name: {type: String, index: true}});
|
||||||
Author = db.define('Author', {name: String});
|
Author = db.define('Author', {name: String});
|
||||||
Reader = db.define('Reader', {name: String});
|
Reader = db.define('Reader', {name: String});
|
||||||
|
Category = db.define('Category', {name: String});
|
||||||
|
Product = db.define('Product', {name: String, type: String});
|
||||||
|
|
||||||
db.automigrate(function () {
|
db.automigrate(function () {
|
||||||
Book.destroyAll(function () {
|
Book.destroyAll(function () {
|
||||||
|
@ -123,6 +126,117 @@ describe('relations', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('hasMany with properties', function () {
|
||||||
|
it('can be declared with properties', function (done) {
|
||||||
|
Book.hasMany(Chapter, { properties: { type: 'bookType' } });
|
||||||
|
db.automigrate(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create record on scope', function (done) {
|
||||||
|
Book.create({ type: 'fiction' }, function (err, book) {
|
||||||
|
book.chapters.create(function (err, c) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(c);
|
||||||
|
c.bookId.should.equal(book.id);
|
||||||
|
c.bookType.should.equal('fiction');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasMany with scope', function () {
|
||||||
|
it('can be declared with properties', function (done) {
|
||||||
|
Category.hasMany(Product, {
|
||||||
|
properties: function(inst) {
|
||||||
|
if (!inst.productType) return; // skip
|
||||||
|
return { type: inst.productType };
|
||||||
|
},
|
||||||
|
scope: function(inst, filter) {
|
||||||
|
var m = this.properties(inst); // re-use properties
|
||||||
|
if (m) return { where: m };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
db.automigrate(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create record on scope', function (done) {
|
||||||
|
Category.create(function (err, c) {
|
||||||
|
c.products.create({ type: 'book' }, function(err, p) {
|
||||||
|
p.categoryId.should.equal(c.id);
|
||||||
|
p.type.should.equal('book');
|
||||||
|
c.products.create({ type: 'widget' }, function(err, p) {
|
||||||
|
p.categoryId.should.equal(c.id);
|
||||||
|
p.type.should.equal('widget');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find record on scope', function (done) {
|
||||||
|
Category.findOne(function (err, c) {
|
||||||
|
c.products(function(err, products) {
|
||||||
|
products.should.have.length(2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find record on scope - filtered', function (done) {
|
||||||
|
Category.findOne(function (err, c) {
|
||||||
|
c.products({ where: { type: 'book' } }, function(err, products) {
|
||||||
|
products.should.have.length(1);
|
||||||
|
products[0].type.should.equal('book');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// So why not just do the above? In LoopBack, the context
|
||||||
|
// that gets passed into a beforeRemote handler contains
|
||||||
|
// a reference to the parent scope/instance: ctx.instance
|
||||||
|
// in order to enforce a (dynamic scope) at runtime
|
||||||
|
// a temporary property can be set in the beforeRemoting
|
||||||
|
// handler. Optionally,properties dynamic properties can be declared.
|
||||||
|
//
|
||||||
|
// The code below simulates this.
|
||||||
|
|
||||||
|
it('should create record on scope - properties', function (done) {
|
||||||
|
Category.findOne(function (err, c) {
|
||||||
|
c.productType = 'tool'; // temporary
|
||||||
|
c.products.create(function(err, p) {
|
||||||
|
p.categoryId.should.equal(c.id);
|
||||||
|
p.type.should.equal('tool');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find record on scope - scoped', function (done) {
|
||||||
|
Category.findOne(function (err, c) {
|
||||||
|
c.productType = 'book'; // temporary, for scoping
|
||||||
|
c.products(function(err, products) {
|
||||||
|
products.should.have.length(1);
|
||||||
|
products[0].type.should.equal('book');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find record on scope - scoped', function (done) {
|
||||||
|
Category.findOne(function (err, c) {
|
||||||
|
c.productType = 'tool'; // temporary, for scoping
|
||||||
|
c.products(function(err, products) {
|
||||||
|
products.should.have.length(1);
|
||||||
|
products[0].type.should.equal('tool');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe('belongsTo', function () {
|
describe('belongsTo', function () {
|
||||||
var List, Item, Fear, Mind;
|
var List, Item, Fear, Mind;
|
||||||
|
|
||||||
|
@ -190,7 +304,7 @@ describe('relations', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be declared using hasOne method', function () {
|
it('can be declared using hasOne method', function () {
|
||||||
Supplier.hasOne(Account);
|
Supplier.hasOne(Account, { properties: { name: 'supplierName' } });
|
||||||
Object.keys((new Account()).toObject()).should.include('supplierId');
|
Object.keys((new Account()).toObject()).should.include('supplierId');
|
||||||
(new Supplier()).account.should.be.an.instanceOf(Function);
|
(new Supplier()).account.should.be.an.instanceOf(Function);
|
||||||
});
|
});
|
||||||
|
@ -207,6 +321,7 @@ describe('relations', function () {
|
||||||
should.exist(act);
|
should.exist(act);
|
||||||
act.should.be.an.instanceOf(Account);
|
act.should.be.an.instanceOf(Account);
|
||||||
supplier.account().id.should.equal(act.id);
|
supplier.account().id.should.equal(act.id);
|
||||||
|
act.supplierName.should.equal(supplier.name);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue