Merge pull request #86 from strongloop/feature/enhance-relations
Enhancements to connector resolution, model relations and inclusions
This commit is contained in:
commit
b848bfb5f8
|
@ -2,7 +2,7 @@ var DataSource = require('../index').DataSource;
|
||||||
var ds = new DataSource('memory');
|
var ds = new DataSource('memory');
|
||||||
|
|
||||||
var Order = ds.createModel('Order', {
|
var Order = ds.createModel('Order', {
|
||||||
customerId: Number,
|
items: [String],
|
||||||
orderDate: Date
|
orderDate: Date
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -12,8 +12,11 @@ var Customer = ds.createModel('Customer', {
|
||||||
|
|
||||||
Order.belongsTo(Customer);
|
Order.belongsTo(Customer);
|
||||||
|
|
||||||
|
var order1, order2, order3;
|
||||||
|
|
||||||
Customer.create({name: 'John'}, function (err, customer) {
|
Customer.create({name: 'John'}, function (err, customer) {
|
||||||
Order.create({customerId: customer.id, orderDate: new Date()}, function (err, order) {
|
Order.create({customerId: customer.id, orderDate: new Date(), items: ['Book']}, function (err, order) {
|
||||||
|
order1 = order;
|
||||||
order.customer(console.log);
|
order.customer(console.log);
|
||||||
order.customer(true, console.log);
|
order.customer(true, console.log);
|
||||||
|
|
||||||
|
@ -22,20 +25,34 @@ Customer.create({name: 'John'}, function (err, customer) {
|
||||||
order.customer(console.log);
|
order.customer(console.log);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Order.create({orderDate: new Date(), items: ['Phone']}, function (err, order) {
|
||||||
|
|
||||||
|
order.customer.create({name: 'Smith'}, function(err, customer2) {
|
||||||
|
console.log(order, customer2);
|
||||||
|
order.save(function(err, order) {
|
||||||
|
order2 = order;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var customer3 = order.customer.build({name: 'Tom'});
|
||||||
|
console.log('Customer 3', customer3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Customer.hasMany(Order, {as: 'orders', foreignKey: 'customerId'});
|
Customer.hasMany(Order, {as: 'orders', foreignKey: 'customerId'});
|
||||||
|
|
||||||
Customer.create({name: 'Ray'}, function (err, customer) {
|
Customer.create({name: 'Ray'}, function (err, customer) {
|
||||||
Order.create({customerId: customer.id, orderDate: new Date()}, function (err, order) {
|
Order.create({customerId: customer.id, orderDate: new Date()}, function (err, order) {
|
||||||
|
order3 = order;
|
||||||
customer.orders(console.log);
|
customer.orders(console.log);
|
||||||
customer.orders.create({orderDate: new Date()}, function (err, order) {
|
customer.orders.create({orderDate: new Date()}, function (err, order) {
|
||||||
console.log(order);
|
console.log(order);
|
||||||
Customer.include([customer], 'orders', function (err, results) {
|
Customer.include([customer], 'orders', function (err, results) {
|
||||||
console.log('Results: ', results);
|
console.log('Results: ', results);
|
||||||
});
|
});
|
||||||
customer.orders.findById('2', console.log);
|
customer.orders.findById(order3.id, console.log);
|
||||||
customer.orders.destroy('2', console.log);
|
customer.orders.destroy(order3.id, console.log);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -60,12 +77,31 @@ Appointment.belongsTo(Physician);
|
||||||
Physician.hasMany(Patient, {through: Appointment});
|
Physician.hasMany(Patient, {through: Appointment});
|
||||||
Patient.hasMany(Physician, {through: Appointment});
|
Patient.hasMany(Physician, {through: Appointment});
|
||||||
|
|
||||||
Physician.create({name: 'Smith'}, function (err, physician) {
|
Physician.create({name: 'Dr John'}, function (err, physician1) {
|
||||||
Patient.create({name: 'Mary'}, function (err, patient) {
|
Physician.create({name: 'Dr Smith'}, function (err, physician2) {
|
||||||
Appointment.create({appointmentDate: new Date(), physicianId: physician.id, patientId: patient.id},
|
Patient.create({name: 'Mary'}, function (err, patient1) {
|
||||||
function (err, appt) {
|
Patient.create({name: 'Ben'}, function (err, patient2) {
|
||||||
physician.patients(console.log);
|
Appointment.create({appointmentDate: new Date(), physicianId: physician1.id, patientId: patient1.id},
|
||||||
patient.physicians(console.log);
|
function (err, appt1) {
|
||||||
|
Appointment.create({appointmentDate: new Date(), physicianId: physician1.id, patientId: patient2.id},
|
||||||
|
function (err, appt2) {
|
||||||
|
physician1.patients(console.log);
|
||||||
|
physician1.patients({where: {name: 'Mary'}}, console.log);
|
||||||
|
patient1.physicians(console.log);
|
||||||
|
|
||||||
|
// Build an appointment?
|
||||||
|
var patient3 = patient1.physicians.build({name: 'Dr X'});
|
||||||
|
console.log('Physician 3: ', patient3, patient3.constructor.modelName);
|
||||||
|
|
||||||
|
// Create a physician?
|
||||||
|
patient1.physicians.create({name: 'Dr X'}, function(err, patient4) {
|
||||||
|
console.log('Physician 4: ', patient4, patient4.constructor.modelName);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -84,7 +120,22 @@ Part.hasAndBelongsToMany(Assembly);
|
||||||
Assembly.create({name: 'car'}, function (err, assembly) {
|
Assembly.create({name: 'car'}, function (err, assembly) {
|
||||||
Part.create({partNumber: 'engine'}, function (err, part) {
|
Part.create({partNumber: 'engine'}, function (err, part) {
|
||||||
assembly.parts.add(part, function (err) {
|
assembly.parts.add(part, function (err) {
|
||||||
assembly.parts(console.log);
|
assembly.parts(function(err, parts) {
|
||||||
|
console.log('Parts: ', parts);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build an part?
|
||||||
|
var part3 = assembly.parts.build({partNumber: 'door'});
|
||||||
|
console.log('Part3: ', part3, part3.constructor.modelName);
|
||||||
|
|
||||||
|
// Create a part?
|
||||||
|
assembly.parts.create({partNumber: 'door'}, function(err, part4) {
|
||||||
|
console.log('Part4: ', part4, part4.constructor.modelName);
|
||||||
|
|
||||||
|
Assembly.find({include: 'parts'}, function(err, assemblies) {
|
||||||
|
console.log('Assemblies: ', assemblies);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,6 +51,24 @@ Memory.prototype.connect = function (callback) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function serialize(obj) {
|
||||||
|
if(obj === null || obj === undefined) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
return JSON.stringify(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deserialize(dbObj) {
|
||||||
|
if(dbObj === null || dbObj === undefined) {
|
||||||
|
return dbObj;
|
||||||
|
}
|
||||||
|
if(typeof dbObj === 'string') {
|
||||||
|
return JSON.parse(dbObj);
|
||||||
|
} else {
|
||||||
|
return dbObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Memory.prototype.loadFromFile = function(callback) {
|
Memory.prototype.loadFromFile = function(callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
if (self.settings.file) {
|
if (self.settings.file) {
|
||||||
|
@ -142,7 +160,7 @@ Memory.prototype.create = function create(model, data, callback) {
|
||||||
if(!this.cache[model]) {
|
if(!this.cache[model]) {
|
||||||
this.cache[model] = {};
|
this.cache[model] = {};
|
||||||
}
|
}
|
||||||
this.cache[model][id] = JSON.stringify(data);
|
this.cache[model][id] = serialize(data);
|
||||||
this.saveToFile(id, callback);
|
this.saveToFile(id, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -161,7 +179,7 @@ Memory.prototype.updateOrCreate = function (model, data, callback) {
|
||||||
};
|
};
|
||||||
|
|
||||||
Memory.prototype.save = function save(model, data, callback) {
|
Memory.prototype.save = function save(model, data, callback) {
|
||||||
this.cache[model][this.getIdValue(model, data)] = JSON.stringify(data);
|
this.cache[model][this.getIdValue(model, data)] = serialize(data);
|
||||||
this.saveToFile(data, callback);
|
this.saveToFile(data, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -185,7 +203,7 @@ Memory.prototype.destroy = function destroy(model, id, callback) {
|
||||||
|
|
||||||
Memory.prototype.fromDb = function (model, data) {
|
Memory.prototype.fromDb = function (model, data) {
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
data = JSON.parse(data);
|
data = deserialize(data);
|
||||||
var props = this._models[model].properties;
|
var props = this._models[model].properties;
|
||||||
for (var key in data) {
|
for (var key in data) {
|
||||||
var val = data[key];
|
var val = data[key];
|
||||||
|
@ -374,8 +392,8 @@ Memory.prototype.updateAttributes = function updateAttributes(model, id, data, c
|
||||||
this.setIdValue(model, data, id);
|
this.setIdValue(model, data, id);
|
||||||
|
|
||||||
var cachedModels = this.cache[model];
|
var cachedModels = this.cache[model];
|
||||||
var modelAsString = cachedModels && this.cache[model][id];
|
var modelData = cachedModels && this.cache[model][id];
|
||||||
var modelData = modelAsString && JSON.parse(modelAsString);
|
modelData = modelData && deserialize(modelData);
|
||||||
|
|
||||||
if (modelData) {
|
if (modelData) {
|
||||||
this.save(model, merge(modelData, data), cb);
|
this.save(model, merge(modelData, data), cb);
|
||||||
|
|
99
lib/dao.js
99
lib/dao.js
|
@ -458,8 +458,10 @@ DataAccessObject._coerce = function (where) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all instances of Model, matched by query
|
* Find all instances of Model, matched by query
|
||||||
* make sure you have marked as `index: true` fields for filter or sort.
|
* make sure you have marked as `index: true` fields for filter or sort
|
||||||
* The params object:
|
*
|
||||||
|
* @param {Object} [query] the query object
|
||||||
|
*
|
||||||
* - where: Object `{ key: val, key2: {gt: 'val2'}}`
|
* - where: Object `{ key: val, key2: {gt: 'val2'}}`
|
||||||
* - include: String, Object or Array. See `DataAccessObject.include()`.
|
* - include: String, Object or Array. See `DataAccessObject.include()`.
|
||||||
* - order: String
|
* - order: String
|
||||||
|
@ -470,36 +472,36 @@ DataAccessObject._coerce = function (where) {
|
||||||
* @param {Function} callback (required) called with two arguments: err (null or Error), array of instances
|
* @param {Function} callback (required) called with two arguments: err (null or Error), array of instances
|
||||||
*/
|
*/
|
||||||
|
|
||||||
DataAccessObject.find = function find(params, cb) {
|
DataAccessObject.find = function find(query, cb) {
|
||||||
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||||
|
|
||||||
if (arguments.length === 1) {
|
if (arguments.length === 1) {
|
||||||
cb = params;
|
cb = query;
|
||||||
params = null;
|
query = null;
|
||||||
}
|
}
|
||||||
var constr = this;
|
var constr = this;
|
||||||
|
|
||||||
params = params || {};
|
query = query || {};
|
||||||
|
|
||||||
if (params.where) {
|
if (query.where) {
|
||||||
params.where = this._coerce(params.where);
|
query.where = this._coerce(query.where);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fields = params.fields;
|
var fields = query.fields;
|
||||||
var near = params && geo.nearFilter(params.where);
|
var near = query && geo.nearFilter(query.where);
|
||||||
var supportsGeo = !!this.getDataSource().connector.buildNearFilter;
|
var supportsGeo = !!this.getDataSource().connector.buildNearFilter;
|
||||||
|
|
||||||
// normalize fields as array of included property names
|
// normalize fields as array of included property names
|
||||||
if (fields) {
|
if (fields) {
|
||||||
params.fields = fieldsToArray(fields, Object.keys(this.definition.properties));
|
query.fields = fieldsToArray(fields, Object.keys(this.definition.properties));
|
||||||
}
|
}
|
||||||
|
|
||||||
params = removeUndefined(params);
|
query = removeUndefined(query);
|
||||||
if (near) {
|
if (near) {
|
||||||
if (supportsGeo) {
|
if (supportsGeo) {
|
||||||
// convert it
|
// convert it
|
||||||
this.getDataSource().connector.buildNearFilter(params, near);
|
this.getDataSource().connector.buildNearFilter(query, near);
|
||||||
} else if (params.where) {
|
} else if (query.where) {
|
||||||
// do in memory query
|
// do in memory query
|
||||||
// using all documents
|
// using all documents
|
||||||
this.getDataSource().connector.all(this.modelName, {}, function (err, data) {
|
this.getDataSource().connector.all(this.modelName, {}, function (err, data) {
|
||||||
|
@ -521,7 +523,7 @@ DataAccessObject.find = function find(params, cb) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
memory.all(modelName, params, cb);
|
memory.all(modelName, query, cb);
|
||||||
} else {
|
} else {
|
||||||
cb(null, []);
|
cb(null, []);
|
||||||
}
|
}
|
||||||
|
@ -532,24 +534,24 @@ DataAccessObject.find = function find(params, cb) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.getDataSource().connector.all(this.modelName, params, function (err, data) {
|
this.getDataSource().connector.all(this.modelName, query, function (err, data) {
|
||||||
if (data && data.forEach) {
|
if (data && data.forEach) {
|
||||||
data.forEach(function (d, i) {
|
data.forEach(function (d, i) {
|
||||||
var obj = new constr();
|
var obj = new constr();
|
||||||
|
|
||||||
obj._initProperties(d, {fields: params.fields});
|
obj._initProperties(d, {fields: query.fields});
|
||||||
|
|
||||||
if (params && params.include) {
|
if (query && query.include) {
|
||||||
if (params.collect) {
|
if (query.collect) {
|
||||||
// The collect property indicates that the query is to return the
|
// The collect property indicates that the query is to return the
|
||||||
// standlone items for a related model, not as child of the parent object
|
// standlone items for a related model, not as child of the parent object
|
||||||
// For example, article.tags
|
// For example, article.tags
|
||||||
obj = obj.__cachedRelations[params.collect];
|
obj = obj.__cachedRelations[query.collect];
|
||||||
} else {
|
} else {
|
||||||
// This handles the case to return parent items including the related
|
// This handles the case to return parent items including the related
|
||||||
// models. For example, Article.find({include: 'tags'}, ...);
|
// models. For example, Article.find({include: 'tags'}, ...);
|
||||||
// Try to normalize the include
|
// Try to normalize the include
|
||||||
var includes = params.include || [];
|
var includes = query.include || [];
|
||||||
if (typeof includes === 'string') {
|
if (typeof includes === 'string') {
|
||||||
includes = [includes];
|
includes = [includes];
|
||||||
} else if (!Array.isArray(includes) && typeof includes === 'object') {
|
} else if (!Array.isArray(includes) && typeof includes === 'object') {
|
||||||
|
@ -594,19 +596,19 @@ setRemoting(DataAccessObject.find, {
|
||||||
/**
|
/**
|
||||||
* Find one record, same as `all`, limited by 1 and return object, not collection
|
* Find one record, same as `all`, limited by 1 and return object, not collection
|
||||||
*
|
*
|
||||||
* @param {Object} params Search conditions: {where: {test: 'me'}}
|
* @param {Object} query - search conditions: {where: {test: 'me'}}
|
||||||
* @param {Function} cb Callback called with (err, instance)
|
* @param {Function} cb - callback called with (err, instance)
|
||||||
*/
|
*/
|
||||||
DataAccessObject.findOne = function findOne(params, cb) {
|
DataAccessObject.findOne = function findOne(query, cb) {
|
||||||
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||||
|
|
||||||
if (typeof params === 'function') {
|
if (typeof query === 'function') {
|
||||||
cb = params;
|
cb = query;
|
||||||
params = {};
|
query = {};
|
||||||
}
|
}
|
||||||
params = params || {};
|
query = query || {};
|
||||||
params.limit = 1;
|
query.limit = 1;
|
||||||
this.find(params, function (err, collection) {
|
this.find(query, function (err, collection) {
|
||||||
if (err || !collection || !collection.length > 0) return cb(err, null);
|
if (err || !collection || !collection.length > 0) return cb(err, null);
|
||||||
cb(err, collection[0]);
|
cb(err, collection[0]);
|
||||||
});
|
});
|
||||||
|
@ -730,7 +732,6 @@ DataAccessObject.prototype.save = function (options, callback) {
|
||||||
|
|
||||||
var inst = this;
|
var inst = this;
|
||||||
var data = inst.toObject(true);
|
var data = inst.toObject(true);
|
||||||
var Model = this.constructor;
|
|
||||||
var modelName = Model.modelName;
|
var modelName = Model.modelName;
|
||||||
|
|
||||||
if (!getIdValue(Model, this)) {
|
if (!getIdValue(Model, this)) {
|
||||||
|
@ -844,7 +845,7 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
|
||||||
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||||
|
|
||||||
var inst = this;
|
var inst = this;
|
||||||
var Model = this.constructor
|
var Model = this.constructor;
|
||||||
var model = Model.modelName;
|
var model = Model.modelName;
|
||||||
|
|
||||||
if (typeof data === 'function') {
|
if (typeof data === 'function') {
|
||||||
|
@ -911,19 +912,15 @@ setRemoting(DataAccessObject.prototype.updateAttributes, {
|
||||||
* @param {Function} callback Called with (err, instance) arguments
|
* @param {Function} callback Called with (err, instance) arguments
|
||||||
*/
|
*/
|
||||||
DataAccessObject.prototype.reload = function reload(callback) {
|
DataAccessObject.prototype.reload = function reload(callback) {
|
||||||
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
if (stillConnecting(this.getDataSource(), this, arguments)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.constructor.findById(getIdValue(this.constructor, this), callback);
|
this.constructor.findById(getIdValue(this.constructor, this), callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
setRemoting(DataAccessObject.prototype.reload, {
|
|
||||||
description: 'Reload a model instance from the data source',
|
|
||||||
returns: {arg: 'data', type: 'object', root: true}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/*!
|
||||||
* Define readonly property on object
|
* Define readonly property on object
|
||||||
*
|
*
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
|
@ -941,13 +938,25 @@ function defineReadonlyProp(obj, key, value) {
|
||||||
|
|
||||||
var defineScope = require('./scope.js').defineScope;
|
var defineScope = require('./scope.js').defineScope;
|
||||||
|
|
||||||
/*!
|
/**
|
||||||
* Define scope. N.B. Not clear if this needs to be exposed in API doc.
|
* Define a scope for the model class. Scopes enable you to specify commonly-used
|
||||||
|
* queries that you can reference as method calls on a model.
|
||||||
|
*
|
||||||
|
* @param {String} name The scope name
|
||||||
|
* @param {Object} query The query object for DataAccessObject.find()
|
||||||
|
* @param {ModelClass} [targetClass] The model class for the query, default to
|
||||||
|
* the declaring model
|
||||||
*/
|
*/
|
||||||
DataAccessObject.scope = function (name, filter, targetClass) {
|
DataAccessObject.scope = function (name, query, targetClass) {
|
||||||
defineScope(this, targetClass || this, name, filter);
|
defineScope(this, targetClass || this, name, query);
|
||||||
};
|
};
|
||||||
|
|
||||||
// jutil.mixin(DataAccessObject, validations.Validatable);
|
/*!
|
||||||
|
* Add 'include'
|
||||||
|
*/
|
||||||
jutil.mixin(DataAccessObject, Inclusion);
|
jutil.mixin(DataAccessObject, Inclusion);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Add 'relation'
|
||||||
|
*/
|
||||||
jutil.mixin(DataAccessObject, Relation);
|
jutil.mixin(DataAccessObject, Relation);
|
||||||
|
|
|
@ -178,13 +178,18 @@ DataSource.prototype._setupConnector = function () {
|
||||||
|
|
||||||
// List possible connector module names
|
// List possible connector module names
|
||||||
function connectorModuleNames(name) {
|
function connectorModuleNames(name) {
|
||||||
var names = [name]; // Check the name as is
|
var names = []; // Check the name as is
|
||||||
if (!name.match(/^\//)) {
|
if (!name.match(/^\//)) {
|
||||||
names.push('./connectors/' + name); // Check built-in connectors
|
names.push('./connectors/' + name); // Check built-in connectors
|
||||||
if (name.indexOf('loopback-connector-') !== 0) {
|
if (name.indexOf('loopback-connector-') !== 0) {
|
||||||
names.push('loopback-connector-' + name); // Try loopback-connector-<name>
|
names.push('loopback-connector-' + name); // Try loopback-connector-<name>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Only try the short name if the connector is not from StrongLoop
|
||||||
|
if(['mongodb', 'oracle', 'mysql', 'postgresql', 'mssql', 'rest', 'soap']
|
||||||
|
.indexOf(name) === -1) {
|
||||||
|
names.push(name);
|
||||||
|
}
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
153
lib/include.js
153
lib/include.js
|
@ -1,3 +1,4 @@
|
||||||
|
var async = require('async');
|
||||||
var utils = require('./utils');
|
var utils = require('./utils');
|
||||||
var isPlainObject = utils.isPlainObject;
|
var isPlainObject = utils.isPlainObject;
|
||||||
var defineCachedRelations = utils.defineCachedRelations;
|
var defineCachedRelations = utils.defineCachedRelations;
|
||||||
|
@ -42,52 +43,46 @@ function Inclusion() {
|
||||||
Inclusion.include = function (objects, include, cb) {
|
Inclusion.include = function (objects, include, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (
|
if (!include || (Array.isArray(include) && include.length === 0) ||
|
||||||
!include || (Array.isArray(include) && include.length === 0) ||
|
(isPlainObject(include) && Object.keys(include).length === 0)) {
|
||||||
(isPlainObject(include) && Object.keys(include).length === 0)
|
// The objects are empty
|
||||||
) {
|
return process.nextTick(function() {
|
||||||
cb(null, objects);
|
cb && cb(null, objects);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
include = processIncludeJoin(include);
|
|
||||||
|
|
||||||
var keyVals = {};
|
|
||||||
var objsByKeys = {};
|
|
||||||
|
|
||||||
var nbCallbacks = 0;
|
|
||||||
for (var i = 0; i < include.length; i++) {
|
|
||||||
var callback = processIncludeItem(objects, include[i], keyVals, objsByKeys);
|
|
||||||
if (callback !== null) {
|
|
||||||
nbCallbacks++;
|
|
||||||
callback(function () {
|
|
||||||
nbCallbacks--;
|
|
||||||
if (nbCallbacks === 0) {
|
|
||||||
cb(null, objects);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
cb(null, objects);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function processIncludeJoin(ij) {
|
include = normalizeInclude(include);
|
||||||
if (typeof ij === 'string') {
|
|
||||||
ij = [ij];
|
async.each(include, function(item, callback) {
|
||||||
}
|
processIncludeItem(objects, item, callback);
|
||||||
if (isPlainObject(ij)) {
|
}, function(err) {
|
||||||
var newIj = [];
|
cb && cb(err, objects);
|
||||||
for (var key in ij) {
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Normalize the include to be an array
|
||||||
|
* @param include
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
function normalizeInclude(include) {
|
||||||
|
if (typeof include === 'string') {
|
||||||
|
return [include];
|
||||||
|
} else if (isPlainObject(include)) {
|
||||||
|
// Build an array of key/value pairs
|
||||||
|
var newInclude = [];
|
||||||
|
for (var key in include) {
|
||||||
var obj = {};
|
var obj = {};
|
||||||
obj[key] = ij[key];
|
obj[key] = include[key];
|
||||||
newIj.push(obj);
|
newInclude.push(obj);
|
||||||
}
|
}
|
||||||
return newIj;
|
return newInclude;
|
||||||
|
} else {
|
||||||
|
return include;
|
||||||
}
|
}
|
||||||
return ij;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function processIncludeItem(objs, include, keyVals, objsByKeys) {
|
function processIncludeItem(objs, include, cb) {
|
||||||
var relations = self.relations;
|
var relations = self.relations;
|
||||||
|
|
||||||
var relationName, subInclude;
|
var relationName, subInclude;
|
||||||
|
@ -96,7 +91,7 @@ Inclusion.include = function (objects, include, cb) {
|
||||||
subInclude = include[relationName];
|
subInclude = include[relationName];
|
||||||
} else {
|
} else {
|
||||||
relationName = include;
|
relationName = include;
|
||||||
subInclude = [];
|
subInclude = null;
|
||||||
}
|
}
|
||||||
var relation = relations[relationName];
|
var relation = relations[relationName];
|
||||||
|
|
||||||
|
@ -107,68 +102,40 @@ Inclusion.include = function (objects, include, cb) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var req = {'where': {}};
|
// Calling the relation method for each object
|
||||||
|
async.each(objs, function (obj, callback) {
|
||||||
if (!keyVals[relation.keyFrom]) {
|
if(relation.type === 'belongsTo') {
|
||||||
objsByKeys[relation.keyFrom] = {};
|
// If the belongsTo relation doesn't have an owner
|
||||||
objs.filter(Boolean).forEach(function (obj) {
|
if(obj[relation.keyFrom] === null || obj[relation.keyFrom] === undefined) {
|
||||||
if (!objsByKeys[relation.keyFrom][obj[relation.keyFrom]]) {
|
defineCachedRelations(obj);
|
||||||
objsByKeys[relation.keyFrom][obj[relation.keyFrom]] = [];
|
// Set to null if the owner doesn't exist
|
||||||
}
|
obj.__cachedRelations[relationName] = null;
|
||||||
objsByKeys[relation.keyFrom][obj[relation.keyFrom]].push(obj);
|
obj[relationName] = null;
|
||||||
});
|
return callback();
|
||||||
keyVals[relation.keyFrom] = Object.keys(objsByKeys[relation.keyFrom]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyVals[relation.keyFrom].length > 0) {
|
|
||||||
// deep clone is necessary since inq seems to change the processed array
|
|
||||||
var keysToBeProcessed = {};
|
|
||||||
var inValues = [];
|
|
||||||
for (var j = 0; j < keyVals[relation.keyFrom].length; j++) {
|
|
||||||
keysToBeProcessed[keyVals[relation.keyFrom][j]] = true;
|
|
||||||
if (keyVals[relation.keyFrom][j] !== 'null'
|
|
||||||
&& keyVals[relation.keyFrom][j] !== 'undefined') {
|
|
||||||
inValues.push(keyVals[relation.keyFrom][j]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var inst = (obj instanceof self) ? obj : new self(obj);
|
||||||
req.where[relation.keyTo] = {inq: inValues};
|
var relationMethod = inst[relationName];
|
||||||
req.include = subInclude;
|
// FIXME: [rfeng] How do we pass in the refresh flag?
|
||||||
|
relationMethod(function (err, result) {
|
||||||
return function (cb) {
|
if (err) {
|
||||||
relation.modelTo.find(req, function (err, objsIncluded) {
|
return callback(err);
|
||||||
var objectsFrom, j;
|
|
||||||
for (var i = 0; i < objsIncluded.length; i++) {
|
|
||||||
delete keysToBeProcessed[objsIncluded[i][relation.keyTo]];
|
|
||||||
objectsFrom = objsByKeys[relation.keyFrom][objsIncluded[i][relation.keyTo]];
|
|
||||||
for (j = 0; j < objectsFrom.length; j++) {
|
|
||||||
defineCachedRelations(objectsFrom[j]);
|
|
||||||
if (relation.multiple) {
|
|
||||||
if (!objectsFrom[j].__cachedRelations[relationName]) {
|
|
||||||
objectsFrom[j].__cachedRelations[relationName] = [];
|
|
||||||
}
|
|
||||||
objectsFrom[j].__cachedRelations[relationName].push(objsIncluded[i]);
|
|
||||||
} else {
|
} else {
|
||||||
objectsFrom[j].__cachedRelations[relationName] = objsIncluded[i];
|
defineCachedRelations(obj);
|
||||||
}
|
obj.__cachedRelations[relationName] = result;
|
||||||
}
|
obj[relationName] = result;
|
||||||
}
|
|
||||||
|
|
||||||
// No relation have been found for these keys
|
if (subInclude && result) {
|
||||||
for (var key in keysToBeProcessed) {
|
var subItems = relation.multiple ? result : [result];
|
||||||
objectsFrom = objsByKeys[relation.keyFrom][key];
|
// Recursively include the related models
|
||||||
for (j = 0; j < objectsFrom.length; j++) {
|
relation.modelTo.include(subItems, subInclude, callback);
|
||||||
defineCachedRelations(objectsFrom[j]);
|
} else {
|
||||||
objectsFrom[j].__cachedRelations[relationName] =
|
callback(null, result);
|
||||||
relation.multiple ? [] : null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cb(err, objsIncluded);
|
|
||||||
});
|
});
|
||||||
};
|
}, cb);
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -116,7 +116,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
|
||||||
if (i in properties && typeof data[i] !== 'function') {
|
if (i in properties && typeof data[i] !== 'function') {
|
||||||
this.__data[i] = this.__dataWas[i] = clone(data[i]);
|
this.__data[i] = this.__dataWas[i] = clone(data[i]);
|
||||||
} else if (i in ctor.relations) {
|
} else if (i in ctor.relations) {
|
||||||
|
if (ctor.relations[i].type === 'belongsTo' && data[i] !== null && data[i] !== undefined) {
|
||||||
|
// If the related model is populated
|
||||||
this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo];
|
this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo];
|
||||||
|
}
|
||||||
this.__cachedRelations[i] = data[i];
|
this.__cachedRelations[i] = data[i];
|
||||||
} else {
|
} else {
|
||||||
if (strict === false) {
|
if (strict === false) {
|
||||||
|
|
|
@ -15,6 +15,11 @@ module.exports = Relation;
|
||||||
function Relation() {
|
function Relation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the relation by foreign key
|
||||||
|
* @param {*} foreignKey The foreign key
|
||||||
|
* @returns {Object} The relation object
|
||||||
|
*/
|
||||||
Relation.relationNameFor = function relationNameFor(foreignKey) {
|
Relation.relationNameFor = function relationNameFor(foreignKey) {
|
||||||
for (var rel in this.relations) {
|
for (var rel in this.relations) {
|
||||||
if (this.relations[rel].type === 'belongsTo' && this.relations[rel].keyFrom === foreignKey) {
|
if (this.relations[rel].type === 'belongsTo' && this.relations[rel].keyFrom === foreignKey) {
|
||||||
|
@ -23,6 +28,12 @@ Relation.relationNameFor = function relationNameFor(foreignKey) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Look up a model by name from the list of given models
|
||||||
|
* @param {Object} models Models keyed by name
|
||||||
|
* @param {String} modelName The model name
|
||||||
|
* @returns {*} The matching model class
|
||||||
|
*/
|
||||||
function lookupModel(models, modelName) {
|
function lookupModel(models, modelName) {
|
||||||
if(models[modelName]) {
|
if(models[modelName]) {
|
||||||
return models[modelName];
|
return models[modelName];
|
||||||
|
@ -67,15 +78,22 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
||||||
modelTo: anotherClass,
|
modelTo: anotherClass,
|
||||||
multiple: true
|
multiple: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (params.through) {
|
||||||
|
this.relations[methodName].modelThrough = params.through;
|
||||||
|
}
|
||||||
// each instance of this class should have method named
|
// each instance of this class should have method named
|
||||||
// pluralize(anotherClass.modelName)
|
// pluralize(anotherClass.modelName)
|
||||||
// which is actually just anotherClass.find({where: {thisModelNameId: this[idName]}}, cb);
|
// which is actually just anotherClass.find({where: {thisModelNameId: this[idName]}}, cb);
|
||||||
var scopeMethods = {
|
var scopeMethods = {
|
||||||
findById: find,
|
findById: findById,
|
||||||
destroy: destroy
|
destroy: destroyById
|
||||||
};
|
};
|
||||||
if (params.through) {
|
if (params.through) {
|
||||||
var fk2 = i8n.camelize(anotherClass.modelName + '_id', true);
|
var fk2 = i8n.camelize(anotherClass.modelName + '_id', true);
|
||||||
|
|
||||||
|
// Create an instance of the target model and connect it to the instance of
|
||||||
|
// the source model by creating an instance of the through model
|
||||||
scopeMethods.create = function create(data, done) {
|
scopeMethods.create = function create(data, done) {
|
||||||
if (typeof data !== 'object') {
|
if (typeof data !== 'object') {
|
||||||
done = data;
|
done = data;
|
||||||
|
@ -86,13 +104,16 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
var self = this;
|
var self = this;
|
||||||
|
// First create the target model
|
||||||
anotherClass.create(data, function (err, ac) {
|
anotherClass.create(data, function (err, ac) {
|
||||||
if (err) return done(err, ac);
|
if (err) return done(err, ac);
|
||||||
var d = {};
|
var d = {};
|
||||||
d[params.through.relationNameFor(fk)] = self;
|
d[params.through.relationNameFor(fk)] = self;
|
||||||
d[params.through.relationNameFor(fk2)] = ac;
|
d[params.through.relationNameFor(fk2)] = ac;
|
||||||
|
// Then create the through model
|
||||||
params.through.create(d, function (e) {
|
params.through.create(d, function (e) {
|
||||||
if (e) {
|
if (e) {
|
||||||
|
// Undo creation of the target model
|
||||||
ac.destroy(function () {
|
ac.destroy(function () {
|
||||||
done(e);
|
done(e);
|
||||||
});
|
});
|
||||||
|
@ -102,6 +123,11 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the target model instance to the 'hasMany' relation
|
||||||
|
* @param {Object|ID) acInst The actual instance or id value
|
||||||
|
*/
|
||||||
scopeMethods.add = function (acInst, done) {
|
scopeMethods.add = function (acInst, done) {
|
||||||
var data = {};
|
var data = {};
|
||||||
var query = {};
|
var query = {};
|
||||||
|
@ -109,8 +135,14 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
||||||
data[params.through.relationNameFor(fk)] = this;
|
data[params.through.relationNameFor(fk)] = this;
|
||||||
query[fk2] = acInst[idName] || acInst;
|
query[fk2] = acInst[idName] || acInst;
|
||||||
data[params.through.relationNameFor(fk2)] = acInst;
|
data[params.through.relationNameFor(fk2)] = acInst;
|
||||||
|
// Create an instance of the through model
|
||||||
params.through.findOrCreate({where: query}, data, done);
|
params.through.findOrCreate({where: query}, data, done);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the target model instance from the 'hasMany' relation
|
||||||
|
* @param {Object|ID) acInst The actual instance or id value
|
||||||
|
*/
|
||||||
scopeMethods.remove = function (acInst, done) {
|
scopeMethods.remove = function (acInst, done) {
|
||||||
var q = {};
|
var q = {};
|
||||||
q[fk2] = acInst[idName] || acInst;
|
q[fk2] = acInst[idName] || acInst;
|
||||||
|
@ -124,8 +156,12 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
||||||
d.destroy(done);
|
d.destroy(done);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// No destroy method will be injected
|
||||||
delete scopeMethods.destroy;
|
delete scopeMethods.destroy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mix the property and scoped methods into the prototype class
|
||||||
defineScope(this.prototype, params.through || anotherClass, methodName, function () {
|
defineScope(this.prototype, params.through || anotherClass, methodName, function () {
|
||||||
var filter = {};
|
var filter = {};
|
||||||
filter.where = {};
|
filter.where = {};
|
||||||
|
@ -142,7 +178,8 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
||||||
anotherClass.dataSource.defineForeignKey(anotherClass.modelName, fk, this.modelName);
|
anotherClass.dataSource.defineForeignKey(anotherClass.modelName, fk, this.modelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
function find(id, cb) {
|
// Find the target model instance by id
|
||||||
|
function findById(id, cb) {
|
||||||
anotherClass.findById(id, function (err, inst) {
|
anotherClass.findById(id, function (err, inst) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
@ -150,6 +187,7 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
||||||
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
|
||||||
if (inst[fk] && inst[fk].toString() === this[idName].toString()) {
|
if (inst[fk] && inst[fk].toString() === this[idName].toString()) {
|
||||||
cb(null, inst);
|
cb(null, inst);
|
||||||
} else {
|
} else {
|
||||||
|
@ -158,7 +196,8 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
function destroy(id, cb) {
|
// Destroy the target model instance by id
|
||||||
|
function destroyById(id, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
anotherClass.findById(id, function (err, inst) {
|
anotherClass.findById(id, function (err, inst) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -167,6 +206,7 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
||||||
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
|
||||||
if (inst[fk] && inst[fk].toString() === self[idName].toString()) {
|
if (inst[fk] && inst[fk].toString() === self[idName].toString()) {
|
||||||
inst.destroy(cb);
|
inst.destroy(cb);
|
||||||
} else {
|
} else {
|
||||||
|
@ -234,6 +274,8 @@ Relation.belongsTo = function (anotherClass, params) {
|
||||||
this.dataSource.defineForeignKey(this.modelName, fk, anotherClass.modelName);
|
this.dataSource.defineForeignKey(this.modelName, fk, anotherClass.modelName);
|
||||||
this.prototype.__finders__ = this.prototype.__finders__ || {};
|
this.prototype.__finders__ = this.prototype.__finders__ || {};
|
||||||
|
|
||||||
|
// Set up a finder to find by id and make sure the foreign key of the declaring
|
||||||
|
// model matches the primary key of the target model
|
||||||
this.prototype.__finders__[methodName] = function (id, cb) {
|
this.prototype.__finders__[methodName] = function (id, cb) {
|
||||||
if (id === null) {
|
if (id === null) {
|
||||||
cb(null, null);
|
cb(null, null);
|
||||||
|
@ -246,6 +288,7 @@ Relation.belongsTo = function (anotherClass, params) {
|
||||||
if (!inst) {
|
if (!inst) {
|
||||||
return cb(null, null);
|
return cb(null, null);
|
||||||
}
|
}
|
||||||
|
// Check if the foreign key matches the primary key
|
||||||
if (inst[idName] === this[fk]) {
|
if (inst[idName] === this[fk]) {
|
||||||
cb(null, inst);
|
cb(null, inst);
|
||||||
} else {
|
} else {
|
||||||
|
@ -254,7 +297,12 @@ Relation.belongsTo = function (anotherClass, params) {
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
};
|
};
|
||||||
|
|
||||||
this.prototype[methodName] = function (refresh, p) {
|
// Define the method for the belongsTo relation itself
|
||||||
|
// It will support one of the following styles:
|
||||||
|
// - order.customer(refresh, callback): Load the target model instance asynchronously
|
||||||
|
// - order.customer(customer): Synchronous setter of the target model instance
|
||||||
|
// - order.customer(): Synchronous getter of the target model instance
|
||||||
|
var relationMethod = function (refresh, p) {
|
||||||
if (arguments.length === 1) {
|
if (arguments.length === 1) {
|
||||||
p = refresh;
|
p = refresh;
|
||||||
refresh = false;
|
refresh = false;
|
||||||
|
@ -290,15 +338,42 @@ Relation.belongsTo = function (anotherClass, params) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Define a property for the scope so that we have 'this' for the scoped methods
|
||||||
|
Object.defineProperty(this.prototype, methodName, {
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
get: function () {
|
||||||
|
var fn = relationMethod.bind(this);
|
||||||
// Set the remoting metadata so that it can be accessed as /api/<model>/<id>/<belongsToRelationName>
|
// Set the remoting metadata so that it can be accessed as /api/<model>/<id>/<belongsToRelationName>
|
||||||
// For example, /api/orders/1/customer
|
// For example, /api/orders/1/customer
|
||||||
var fn = this.prototype[methodName];
|
|
||||||
fn.shared = true;
|
fn.shared = true;
|
||||||
fn.http = {verb: 'get', path: '/' + methodName};
|
fn.http = {verb: 'get', path: '/' + methodName};
|
||||||
fn.accepts = {arg: 'refresh', type: 'boolean', http: {source: 'query'}};
|
fn.accepts = {arg: 'refresh', type: 'boolean', http: {source: 'query'}};
|
||||||
fn.description = 'Fetches belongsTo relation ' + methodName;
|
fn.description = 'Fetches belongsTo relation ' + methodName;
|
||||||
fn.returns = {arg: methodName, type: 'object', root: true};
|
fn.returns = {arg: methodName, type: 'object', root: true};
|
||||||
|
|
||||||
|
// Create an instance of the target model and set the foreign key of the
|
||||||
|
// declaring model instance to the id of the target instance
|
||||||
|
fn.create = function(targetModelData, cb) {
|
||||||
|
var self = this;
|
||||||
|
anotherClass.create(targetModelData, function(err, targetModel) {
|
||||||
|
if(!err) {
|
||||||
|
self[fk] = targetModel[idName];
|
||||||
|
cb && cb(err, targetModel);
|
||||||
|
} else {
|
||||||
|
cb && cb(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
// Build an instance of the target model
|
||||||
|
fn.build = function(targetModelData) {
|
||||||
|
return new anotherClass(targetModelData);
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
return fn;
|
||||||
|
}});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -308,9 +383,9 @@ Relation.belongsTo = function (anotherClass, params) {
|
||||||
* ```js
|
* ```js
|
||||||
* Post.hasAndBelongsToMany('tags');
|
* Post.hasAndBelongsToMany('tags');
|
||||||
* ```
|
* ```
|
||||||
* @param {String} anotherClass
|
* @param {String|Function} anotherClass - target class to hasAndBelongsToMany or name of
|
||||||
* @param {Object} params
|
* the relation
|
||||||
*
|
* @param {Object} params - configuration {as: String, foreignKey: *, model: ModelClass}
|
||||||
*/
|
*/
|
||||||
Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) {
|
Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
|
|
18
lib/scope.js
18
lib/scope.js
|
@ -5,6 +5,15 @@ var defineCachedRelations = utils.defineCachedRelations;
|
||||||
*/
|
*/
|
||||||
exports.defineScope = defineScope;
|
exports.defineScope = defineScope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a scope to the class
|
||||||
|
* @param {Model} cls The class where the scope method is added
|
||||||
|
* @param {Model} targetClass The class that a query to run against
|
||||||
|
* @param {String} name The name of the scope
|
||||||
|
* @param {Object|Function} params The parameters object for the query or a function
|
||||||
|
* to return the query object
|
||||||
|
* @param methods An object of methods keyed by the method name to be bound to the class
|
||||||
|
*/
|
||||||
function defineScope(cls, targetClass, name, params, methods) {
|
function defineScope(cls, targetClass, name, params, methods) {
|
||||||
|
|
||||||
// collect meta info about scope
|
// collect meta info about scope
|
||||||
|
@ -38,6 +47,7 @@ function defineScope(cls, targetClass, name, params, methods) {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
get: function () {
|
get: function () {
|
||||||
|
var self = this;
|
||||||
var f = function caller(condOrRefresh, cb) {
|
var f = function caller(condOrRefresh, cb) {
|
||||||
var actualCond = {};
|
var actualCond = {};
|
||||||
var actualRefresh = false;
|
var actualRefresh = false;
|
||||||
|
@ -56,8 +66,9 @@ function defineScope(cls, targetClass, name, params, methods) {
|
||||||
throw new Error('Method can be only called with one or two arguments');
|
throw new Error('Method can be only called with one or two arguments');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.__cachedRelations || (this.__cachedRelations[name] === undefined) || actualRefresh) {
|
if (!self.__cachedRelations || self.__cachedRelations[name] === undefined
|
||||||
var self = this;
|
|| actualRefresh) {
|
||||||
|
// It either doesn't hit the cache or reresh is required
|
||||||
var params = mergeParams(actualCond, caller._scope);
|
var params = mergeParams(actualCond, caller._scope);
|
||||||
return targetClass.find(params, function (err, data) {
|
return targetClass.find(params, function (err, data) {
|
||||||
if (!err && saveOnCache) {
|
if (!err && saveOnCache) {
|
||||||
|
@ -67,7 +78,8 @@ function defineScope(cls, targetClass, name, params, methods) {
|
||||||
cb(err, data);
|
cb(err, data);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
cb(null, this.__cachedRelations[name]);
|
// Return from cache
|
||||||
|
cb(null, self.__cachedRelations[name]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
f._scope = typeof params === 'function' ? params.call(this) : params;
|
f._scope = typeof params === 'function' ? params.call(this) : params;
|
||||||
|
|
|
@ -194,8 +194,11 @@ BaseSQL.prototype.exists = function (model, id, callback) {
|
||||||
* @param {Function} callback The callback function
|
* @param {Function} callback The callback function
|
||||||
*/
|
*/
|
||||||
BaseSQL.prototype.find = function find(model, id, callback) {
|
BaseSQL.prototype.find = function find(model, id, callback) {
|
||||||
|
var idQuery = (id === null || id === undefined)
|
||||||
|
? this.idColumnEscaped(model) + ' IS NULL'
|
||||||
|
: this.idColumnEscaped(model) + ' = ' + id;
|
||||||
var sql = 'SELECT * FROM ' +
|
var sql = 'SELECT * FROM ' +
|
||||||
this.tableEscaped(model) + ' WHERE ' + this.idColumnEscaped(model) + ' = ' + id + ' LIMIT 1';
|
this.tableEscaped(model) + ' WHERE ' + idQuery + ' LIMIT 1';
|
||||||
|
|
||||||
this.query(sql, function (err, data) {
|
this.query(sql, function (err, data) {
|
||||||
if (data && data.length === 1) {
|
if (data && data.length === 1) {
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
// 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, User, Post, Passport, City, Street, Building;
|
var db, User, Post, Passport, City, Street, Building, Assembly, Part;
|
||||||
var nbSchemaRequests = 0;
|
|
||||||
|
|
||||||
describe('include', function () {
|
describe('include', function () {
|
||||||
|
|
||||||
before(setup);
|
before(setup);
|
||||||
|
|
||||||
it('should fetch belongsTo relation', function (done) {
|
it('should fetch belongsTo relation', function (done) {
|
||||||
Passport.all({include: 'owner'}, function (err, passports) {
|
Passport.find({include: 'owner'}, function (err, passports) {
|
||||||
passports.length.should.be.ok;
|
passports.length.should.be.ok;
|
||||||
passports.forEach(function (p) {
|
passports.forEach(function (p) {
|
||||||
p.__cachedRelations.should.have.property('owner');
|
p.__cachedRelations.should.have.property('owner');
|
||||||
|
@ -32,7 +31,7 @@ describe('include', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch hasMany relation', function (done) {
|
it('should fetch hasMany relation', function (done) {
|
||||||
User.all({include: 'posts'}, function (err, users) {
|
User.find({include: 'posts'}, function (err, users) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
should.exist(users);
|
should.exist(users);
|
||||||
users.length.should.be.ok;
|
users.length.should.be.ok;
|
||||||
|
@ -52,7 +51,7 @@ describe('include', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch Passport - Owner - Posts', function (done) {
|
it('should fetch Passport - Owner - Posts', function (done) {
|
||||||
Passport.all({include: {owner: 'posts'}}, function (err, passports) {
|
Passport.find({include: {owner: 'posts'}}, function (err, passports) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
should.exist(passports);
|
should.exist(passports);
|
||||||
passports.length.should.be.ok;
|
passports.length.should.be.ok;
|
||||||
|
@ -81,7 +80,7 @@ describe('include', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch Passports - User - Posts - User', function (done) {
|
it('should fetch Passports - User - Posts - User', function (done) {
|
||||||
Passport.all({
|
Passport.find({
|
||||||
include: {owner: {posts: 'author'}}
|
include: {owner: {posts: 'author'}}
|
||||||
}, function (err, passports) {
|
}, function (err, passports) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
|
@ -109,7 +108,7 @@ describe('include', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch User - Posts AND Passports', function (done) {
|
it('should fetch User - Posts AND Passports', function (done) {
|
||||||
User.all({include: ['posts', 'passports']}, function (err, users) {
|
User.find({include: ['posts', 'passports']}, function (err, users) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
should.exist(users);
|
should.exist(users);
|
||||||
users.length.should.be.ok;
|
users.length.should.be.ok;
|
||||||
|
@ -140,6 +139,39 @@ describe('include', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support hasAndBelongsToMany', function (done) {
|
||||||
|
|
||||||
|
Assembly.destroyAll(function(err) {
|
||||||
|
Part.destroyAll(function(err) {
|
||||||
|
Assembly.relations.parts.modelThrough.destroyAll(function(err) {
|
||||||
|
Assembly.create({name: 'car'}, function (err, assembly) {
|
||||||
|
Part.create({partNumber: 'engine'}, function (err, part) {
|
||||||
|
assembly.parts.add(part, function (err, data) {
|
||||||
|
assembly.parts(function (err, parts) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exists(parts);
|
||||||
|
parts.length.should.equal(1);
|
||||||
|
parts[0].partNumber.should.equal('engine');
|
||||||
|
|
||||||
|
// Create a part
|
||||||
|
assembly.parts.create({partNumber: 'door'}, function (err, part4) {
|
||||||
|
|
||||||
|
Assembly.find({include: 'parts'}, function (err, assemblies) {
|
||||||
|
assemblies.length.should.equal(1);
|
||||||
|
assemblies[0].parts.length.should.equal(2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function setup(done) {
|
function setup(done) {
|
||||||
|
@ -163,6 +195,17 @@ function setup(done) {
|
||||||
User.hasMany('posts', {foreignKey: 'userId'});
|
User.hasMany('posts', {foreignKey: 'userId'});
|
||||||
Post.belongsTo('author', {model: User, foreignKey: 'userId'});
|
Post.belongsTo('author', {model: User, foreignKey: 'userId'});
|
||||||
|
|
||||||
|
Assembly = db.define('Assembly', {
|
||||||
|
name: String
|
||||||
|
});
|
||||||
|
|
||||||
|
Part = db.define('Part', {
|
||||||
|
partNumber: String
|
||||||
|
});
|
||||||
|
|
||||||
|
Assembly.hasAndBelongsToMany(Part);
|
||||||
|
Part.hasAndBelongsToMany(Assembly);
|
||||||
|
|
||||||
db.automigrate(function () {
|
db.automigrate(function () {
|
||||||
var createdUsers = [];
|
var createdUsers = [];
|
||||||
var createdPassports = [];
|
var createdPassports = [];
|
||||||
|
|
|
@ -615,9 +615,17 @@ describe('DataSource connector types', function() {
|
||||||
describe('DataSource constructor', function () {
|
describe('DataSource constructor', function () {
|
||||||
// Mocked require
|
// Mocked require
|
||||||
var loader = function (name) {
|
var loader = function (name) {
|
||||||
|
if (name.indexOf('./connectors/') !== -1) {
|
||||||
|
// ./connectors/<name> doesn't exist
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (name === 'loopback-connector-abc') {
|
||||||
|
// Assume loopback-connector-abc doesn't exist
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
name: name
|
name: name
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should resolve connector by path', function () {
|
it('should resolve connector by path', function () {
|
||||||
|
@ -632,9 +640,20 @@ describe('DataSource constructor', function () {
|
||||||
var connector = DataSource._resolveConnector('loopback-connector-xyz', loader);
|
var connector = DataSource._resolveConnector('loopback-connector-xyz', loader);
|
||||||
assert(connector.connector);
|
assert(connector.connector);
|
||||||
});
|
});
|
||||||
it('should try to resolve connector by short module name', function () {
|
it('should try to resolve connector by short module name with full name first', function () {
|
||||||
var connector = DataSource._resolveConnector('xyz', loader);
|
var connector = DataSource._resolveConnector('xyz', loader);
|
||||||
assert(connector.connector);
|
assert(connector.connector);
|
||||||
|
assert.equal(connector.connector.name, 'loopback-connector-xyz');
|
||||||
|
});
|
||||||
|
it('should try to resolve connector by short module name', function () {
|
||||||
|
var connector = DataSource._resolveConnector('abc', loader);
|
||||||
|
assert(connector.connector);
|
||||||
|
assert.equal(connector.connector.name, 'abc');
|
||||||
|
});
|
||||||
|
it('should try to resolve connector by short module name for known connectors', function () {
|
||||||
|
var connector = DataSource._resolveConnector('oracle', loader);
|
||||||
|
assert(connector.connector);
|
||||||
|
assert.equal(connector.connector.name, 'loopback-connector-oracle');
|
||||||
});
|
});
|
||||||
it('should try to resolve connector by full module name', function () {
|
it('should try to resolve connector by full module name', function () {
|
||||||
var connector = DataSource._resolveConnector('loopback-xyz', loader);
|
var connector = DataSource._resolveConnector('loopback-xyz', loader);
|
||||||
|
|
Loading…
Reference in New Issue