diff --git a/examples/relations.js b/examples/relations.js index 04973f907..ed3474ccb 100644 --- a/examples/relations.js +++ b/examples/relations.js @@ -2,7 +2,7 @@ var DataSource = require('../index').DataSource; var ds = new DataSource('memory'); var Order = ds.createModel('Order', { - customerId: Number, + items: [String], orderDate: Date }); @@ -12,8 +12,11 @@ var Customer = ds.createModel('Customer', { Order.belongsTo(Customer); +var order1, order2, order3; + 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(true, console.log); @@ -22,20 +25,34 @@ Customer.create({name: 'John'}, function (err, customer) { 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.create({name: 'Ray'}, function (err, customer) { Order.create({customerId: customer.id, orderDate: new Date()}, function (err, order) { + order3 = order; customer.orders(console.log); customer.orders.create({orderDate: new Date()}, function (err, order) { console.log(order); Customer.include([customer], 'orders', function (err, results) { console.log('Results: ', results); }); - customer.orders.findById('2', console.log); - customer.orders.destroy('2', console.log); + customer.orders.findById(order3.id, console.log); + customer.orders.destroy(order3.id, console.log); }); }); }); @@ -60,13 +77,32 @@ Appointment.belongsTo(Physician); Physician.hasMany(Patient, {through: Appointment}); Patient.hasMany(Physician, {through: Appointment}); -Physician.create({name: 'Smith'}, function (err, physician) { - Patient.create({name: 'Mary'}, function (err, patient) { - Appointment.create({appointmentDate: new Date(), physicianId: physician.id, patientId: patient.id}, - function (err, appt) { - physician.patients(console.log); - patient.physicians(console.log); +Physician.create({name: 'Dr John'}, function (err, physician1) { + Physician.create({name: 'Dr Smith'}, function (err, physician2) { + Patient.create({name: 'Mary'}, function (err, patient1) { + Patient.create({name: 'Ben'}, function (err, patient2) { + Appointment.create({appointmentDate: new Date(), physicianId: physician1.id, patientId: patient1.id}, + 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) { Part.create({partNumber: 'engine'}, function (err, part) { 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); + }); + }); }); }); diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 1184ac447..0db0dc327 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -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) { var self = this; if (self.settings.file) { @@ -142,7 +160,7 @@ Memory.prototype.create = function create(model, data, callback) { if(!this.cache[model]) { this.cache[model] = {}; } - this.cache[model][id] = JSON.stringify(data); + this.cache[model][id] = serialize(data); this.saveToFile(id, callback); }; @@ -161,7 +179,7 @@ Memory.prototype.updateOrCreate = function (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); }; @@ -185,7 +203,7 @@ Memory.prototype.destroy = function destroy(model, id, callback) { Memory.prototype.fromDb = function (model, data) { if (!data) return null; - data = JSON.parse(data); + data = deserialize(data); var props = this._models[model].properties; for (var key in data) { var val = data[key]; @@ -374,8 +392,8 @@ Memory.prototype.updateAttributes = function updateAttributes(model, id, data, c this.setIdValue(model, data, id); var cachedModels = this.cache[model]; - var modelAsString = cachedModels && this.cache[model][id]; - var modelData = modelAsString && JSON.parse(modelAsString); + var modelData = cachedModels && this.cache[model][id]; + modelData = modelData && deserialize(modelData); if (modelData) { this.save(model, merge(modelData, data), cb); diff --git a/lib/dao.js b/lib/dao.js index 2df44e625..0eb7da46a 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -458,8 +458,10 @@ DataAccessObject._coerce = function (where) { /** * Find all instances of Model, matched by query - * make sure you have marked as `index: true` fields for filter or sort. - * The params object: + * make sure you have marked as `index: true` fields for filter or sort + * + * @param {Object} [query] the query object + * * - where: Object `{ key: val, key2: {gt: 'val2'}}` * - include: String, Object or Array. See `DataAccessObject.include()`. * - 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 */ -DataAccessObject.find = function find(params, cb) { +DataAccessObject.find = function find(query, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; if (arguments.length === 1) { - cb = params; - params = null; + cb = query; + query = null; } var constr = this; - params = params || {}; + query = query || {}; - if (params.where) { - params.where = this._coerce(params.where); + if (query.where) { + query.where = this._coerce(query.where); } - var fields = params.fields; - var near = params && geo.nearFilter(params.where); + var fields = query.fields; + var near = query && geo.nearFilter(query.where); var supportsGeo = !!this.getDataSource().connector.buildNearFilter; // normalize fields as array of included property names 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 (supportsGeo) { // convert it - this.getDataSource().connector.buildNearFilter(params, near); - } else if (params.where) { + this.getDataSource().connector.buildNearFilter(query, near); + } else if (query.where) { // do in memory query // using all documents 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 { 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) { data.forEach(function (d, i) { var obj = new constr(); - obj._initProperties(d, {fields: params.fields}); + obj._initProperties(d, {fields: query.fields}); - if (params && params.include) { - if (params.collect) { + if (query && query.include) { + if (query.collect) { // The collect property indicates that the query is to return the // standlone items for a related model, not as child of the parent object // For example, article.tags - obj = obj.__cachedRelations[params.collect]; + obj = obj.__cachedRelations[query.collect]; } else { // This handles the case to return parent items including the related // models. For example, Article.find({include: 'tags'}, ...); // Try to normalize the include - var includes = params.include || []; + var includes = query.include || []; if (typeof includes === 'string') { includes = [includes]; } 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 * - * @param {Object} params Search conditions: {where: {test: 'me'}} - * @param {Function} cb Callback called with (err, instance) + * @param {Object} query - search conditions: {where: {test: 'me'}} + * @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 (typeof params === 'function') { - cb = params; - params = {}; + if (typeof query === 'function') { + cb = query; + query = {}; } - params = params || {}; - params.limit = 1; - this.find(params, function (err, collection) { + query = query || {}; + query.limit = 1; + this.find(query, function (err, collection) { if (err || !collection || !collection.length > 0) return cb(err, null); cb(err, collection[0]); }); @@ -730,7 +732,6 @@ DataAccessObject.prototype.save = function (options, callback) { var inst = this; var data = inst.toObject(true); - var Model = this.constructor; var modelName = Model.modelName; if (!getIdValue(Model, this)) { @@ -844,7 +845,7 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb if (stillConnecting(this.getDataSource(), this, arguments)) return; var inst = this; - var Model = this.constructor + var Model = this.constructor; var model = Model.modelName; if (typeof data === 'function') { @@ -911,19 +912,15 @@ setRemoting(DataAccessObject.prototype.updateAttributes, { * @param {Function} callback Called with (err, instance) arguments */ 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); }; -/* - 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 * * @param {Object} obj @@ -941,13 +938,25 @@ function defineReadonlyProp(obj, key, value) { 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) { - defineScope(this, targetClass || this, name, filter); +DataAccessObject.scope = function (name, query, targetClass) { + defineScope(this, targetClass || this, name, query); }; -// jutil.mixin(DataAccessObject, validations.Validatable); +/*! + * Add 'include' + */ jutil.mixin(DataAccessObject, Inclusion); + +/*! + * Add 'relation' + */ jutil.mixin(DataAccessObject, Relation); diff --git a/lib/datasource.js b/lib/datasource.js index 32a0ee4b0..dc7461caa 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -178,13 +178,18 @@ DataSource.prototype._setupConnector = function () { // List possible connector module names function connectorModuleNames(name) { - var names = [name]; // Check the name as is + var names = []; // Check the name as is if (!name.match(/^\//)) { names.push('./connectors/' + name); // Check built-in connectors if (name.indexOf('loopback-connector-') !== 0) { names.push('loopback-connector-' + name); // Try loopback-connector- } } + // 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; } diff --git a/lib/include.js b/lib/include.js index 62926ccce..5a0bb0623 100644 --- a/lib/include.js +++ b/lib/include.js @@ -1,3 +1,4 @@ +var async = require('async'); var utils = require('./utils'); var isPlainObject = utils.isPlainObject; var defineCachedRelations = utils.defineCachedRelations; @@ -42,52 +43,46 @@ function Inclusion() { Inclusion.include = function (objects, include, cb) { var self = this; - if ( - !include || (Array.isArray(include) && include.length === 0) || - (isPlainObject(include) && Object.keys(include).length === 0) - ) { - cb(null, objects); - return; + if (!include || (Array.isArray(include) && include.length === 0) || + (isPlainObject(include) && Object.keys(include).length === 0)) { + // The objects are empty + return process.nextTick(function() { + cb && cb(null, objects); + }); } - 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) { - if (typeof ij === 'string') { - ij = [ij]; - } - if (isPlainObject(ij)) { - var newIj = []; - for (var key in ij) { + include = normalizeInclude(include); + + async.each(include, function(item, callback) { + processIncludeItem(objects, item, callback); + }, function(err) { + cb && cb(err, objects); + }); + + + /*! + * 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 = {}; - obj[key] = ij[key]; - newIj.push(obj); + obj[key] = include[key]; + 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 relationName, subInclude; @@ -96,7 +91,7 @@ Inclusion.include = function (objects, include, cb) { subInclude = include[relationName]; } else { relationName = include; - subInclude = []; + subInclude = null; } var relation = relations[relationName]; @@ -107,68 +102,40 @@ Inclusion.include = function (objects, include, cb) { }; } - var req = {'where': {}}; - - if (!keyVals[relation.keyFrom]) { - objsByKeys[relation.keyFrom] = {}; - objs.filter(Boolean).forEach(function (obj) { - if (!objsByKeys[relation.keyFrom][obj[relation.keyFrom]]) { - objsByKeys[relation.keyFrom][obj[relation.keyFrom]] = []; - } - objsByKeys[relation.keyFrom][obj[relation.keyFrom]].push(obj); - }); - 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]); + // Calling the relation method for each object + async.each(objs, function (obj, callback) { + if(relation.type === 'belongsTo') { + // If the belongsTo relation doesn't have an owner + if(obj[relation.keyFrom] === null || obj[relation.keyFrom] === undefined) { + defineCachedRelations(obj); + // Set to null if the owner doesn't exist + obj.__cachedRelations[relationName] = null; + obj[relationName] = null; + return callback(); } } - - req.where[relation.keyTo] = {inq: inValues}; - req.include = subInclude; - - return function (cb) { - relation.modelTo.find(req, function (err, objsIncluded) { - 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 { - objectsFrom[j].__cachedRelations[relationName] = objsIncluded[i]; - } - } + var inst = (obj instanceof self) ? obj : new self(obj); + var relationMethod = inst[relationName]; + // FIXME: [rfeng] How do we pass in the refresh flag? + relationMethod(function (err, result) { + if (err) { + return callback(err); + } else { + defineCachedRelations(obj); + obj.__cachedRelations[relationName] = result; + obj[relationName] = result; + + if (subInclude && result) { + var subItems = relation.multiple ? result : [result]; + // Recursively include the related models + relation.modelTo.include(subItems, subInclude, callback); + } else { + callback(null, result); } + } + }); + }, cb); - // No relation have been found for these keys - for (var key in keysToBeProcessed) { - objectsFrom = objsByKeys[relation.keyFrom][key]; - for (j = 0; j < objectsFrom.length; j++) { - defineCachedRelations(objectsFrom[j]); - objectsFrom[j].__cachedRelations[relationName] = - relation.multiple ? [] : null; - } - } - cb(err, objsIncluded); - }); - }; - } - - return null; } }; diff --git a/lib/model.js b/lib/model.js index a587c8261..3e57ee67a 100644 --- a/lib/model.js +++ b/lib/model.js @@ -116,7 +116,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) { if (i in properties && typeof data[i] !== 'function') { this.__data[i] = this.__dataWas[i] = clone(data[i]); } else if (i in ctor.relations) { - this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo]; + 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.__cachedRelations[i] = data[i]; } else { if (strict === false) { diff --git a/lib/relations.js b/lib/relations.js index e8b44ec1d..3b4a05a79 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -15,6 +15,11 @@ module.exports = Relation; function Relation() { } +/** + * Find the relation by foreign key + * @param {*} foreignKey The foreign key + * @returns {Object} The relation object + */ Relation.relationNameFor = function relationNameFor(foreignKey) { for (var rel in this.relations) { 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) { if(models[modelName]) { return models[modelName]; @@ -67,15 +78,22 @@ Relation.hasMany = function hasMany(anotherClass, params) { modelTo: anotherClass, multiple: true }; + + if (params.through) { + this.relations[methodName].modelThrough = params.through; + } // each instance of this class should have method named // pluralize(anotherClass.modelName) // which is actually just anotherClass.find({where: {thisModelNameId: this[idName]}}, cb); var scopeMethods = { - findById: find, - destroy: destroy + findById: findById, + destroy: destroyById }; if (params.through) { 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) { if (typeof data !== 'object') { done = data; @@ -86,13 +104,16 @@ Relation.hasMany = function hasMany(anotherClass, params) { }; } var self = this; + // First create the target model anotherClass.create(data, function (err, ac) { if (err) return done(err, ac); var d = {}; d[params.through.relationNameFor(fk)] = self; d[params.through.relationNameFor(fk2)] = ac; + // Then create the through model params.through.create(d, function (e) { if (e) { + // Undo creation of the target model ac.destroy(function () { 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) { var data = {}; var query = {}; @@ -109,8 +135,14 @@ Relation.hasMany = function hasMany(anotherClass, params) { data[params.through.relationNameFor(fk)] = this; query[fk2] = acInst[idName] || acInst; data[params.through.relationNameFor(fk2)] = acInst; + // Create an instance of the through model 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) { var q = {}; q[fk2] = acInst[idName] || acInst; @@ -124,8 +156,12 @@ Relation.hasMany = function hasMany(anotherClass, params) { d.destroy(done); }); }; + + // No destroy method will be injected delete scopeMethods.destroy; } + + // Mix the property and scoped methods into the prototype class defineScope(this.prototype, params.through || anotherClass, methodName, function () { var filter = {}; filter.where = {}; @@ -142,7 +178,8 @@ Relation.hasMany = function hasMany(anotherClass, params) { 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) { if (err) { return cb(err); @@ -150,6 +187,7 @@ Relation.hasMany = function hasMany(anotherClass, params) { if (!inst) { return cb(new Error('Not found')); } + // Check if the foreign key matches the primary key if (inst[fk] && inst[fk].toString() === this[idName].toString()) { cb(null, inst); } else { @@ -158,7 +196,8 @@ Relation.hasMany = function hasMany(anotherClass, params) { }.bind(this)); } - function destroy(id, cb) { + // Destroy the target model instance by id + function destroyById(id, cb) { var self = this; anotherClass.findById(id, function (err, inst) { if (err) { @@ -167,6 +206,7 @@ Relation.hasMany = function hasMany(anotherClass, params) { if (!inst) { return cb(new Error('Not found')); } + // Check if the foreign key matches the primary key if (inst[fk] && inst[fk].toString() === self[idName].toString()) { inst.destroy(cb); } else { @@ -234,6 +274,8 @@ Relation.belongsTo = function (anotherClass, params) { this.dataSource.defineForeignKey(this.modelName, fk, anotherClass.modelName); 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) { if (id === null) { cb(null, null); @@ -246,6 +288,7 @@ Relation.belongsTo = function (anotherClass, params) { if (!inst) { return cb(null, null); } + // Check if the foreign key matches the primary key if (inst[idName] === this[fk]) { cb(null, inst); } else { @@ -254,7 +297,12 @@ Relation.belongsTo = function (anotherClass, params) { }.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) { p = refresh; refresh = false; @@ -290,14 +338,41 @@ Relation.belongsTo = function (anotherClass, params) { } }; - // Set the remoting metadata so that it can be accessed as /api/// - // For example, /api/orders/1/customer - var fn = this.prototype[methodName]; - fn.shared = true; - fn.http = {verb: 'get', path: '/' + methodName}; - fn.accepts = {arg: 'refresh', type: 'boolean', http: {source: 'query'}}; - fn.description = 'Fetches belongsTo relation ' + methodName; - fn.returns = {arg: methodName, type: 'object', root: true}; + // 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/// + // For example, /api/orders/1/customer + fn.shared = true; + fn.http = {verb: 'get', path: '/' + methodName}; + fn.accepts = {arg: 'refresh', type: 'boolean', http: {source: 'query'}}; + fn.description = 'Fetches belongsTo relation ' + methodName; + 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 * Post.hasAndBelongsToMany('tags'); * ``` - * @param {String} anotherClass - * @param {Object} params - * + * @param {String|Function} anotherClass - target class to hasAndBelongsToMany or name of + * the relation + * @param {Object} params - configuration {as: String, foreignKey: *, model: ModelClass} */ Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) { params = params || {}; diff --git a/lib/scope.js b/lib/scope.js index 5c8d43c56..da8aa1ada 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -5,6 +5,15 @@ var defineCachedRelations = utils.defineCachedRelations; */ 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) { // collect meta info about scope @@ -38,6 +47,7 @@ function defineScope(cls, targetClass, name, params, methods) { * */ get: function () { + var self = this; var f = function caller(condOrRefresh, cb) { var actualCond = {}; 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'); } - if (!this.__cachedRelations || (this.__cachedRelations[name] === undefined) || actualRefresh) { - var self = this; + if (!self.__cachedRelations || self.__cachedRelations[name] === undefined + || actualRefresh) { + // It either doesn't hit the cache or reresh is required var params = mergeParams(actualCond, caller._scope); return targetClass.find(params, function (err, data) { if (!err && saveOnCache) { @@ -67,7 +78,8 @@ function defineScope(cls, targetClass, name, params, methods) { cb(err, data); }); } else { - cb(null, this.__cachedRelations[name]); + // Return from cache + cb(null, self.__cachedRelations[name]); } }; f._scope = typeof params === 'function' ? params.call(this) : params; diff --git a/lib/sql.js b/lib/sql.js index 701c8dc00..e2006c84f 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -194,8 +194,11 @@ BaseSQL.prototype.exists = function (model, id, callback) { * @param {Function} callback The callback function */ 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 ' + - this.tableEscaped(model) + ' WHERE ' + this.idColumnEscaped(model) + ' = ' + id + ' LIMIT 1'; + this.tableEscaped(model) + ' WHERE ' + idQuery + ' LIMIT 1'; this.query(sql, function (err, data) { if (data && data.length === 1) { diff --git a/test/include.test.js b/test/include.test.js index d8ca49fd2..bd918d5d1 100644 --- a/test/include.test.js +++ b/test/include.test.js @@ -1,15 +1,14 @@ // This test written in mocha+should.js var should = require('./init.js'); -var db, User, Post, Passport, City, Street, Building; -var nbSchemaRequests = 0; +var db, User, Post, Passport, City, Street, Building, Assembly, Part; describe('include', function () { before(setup); 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.forEach(function (p) { p.__cachedRelations.should.have.property('owner'); @@ -32,7 +31,7 @@ describe('include', function () { }); 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.exist(users); users.length.should.be.ok; @@ -52,7 +51,7 @@ describe('include', function () { }); 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.exist(passports); passports.length.should.be.ok; @@ -81,7 +80,7 @@ describe('include', function () { }); it('should fetch Passports - User - Posts - User', function (done) { - Passport.all({ + Passport.find({ include: {owner: {posts: 'author'}} }, function (err, passports) { should.not.exist(err); @@ -109,7 +108,7 @@ describe('include', function () { }); 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.exist(users); 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) { @@ -163,6 +195,17 @@ function setup(done) { User.hasMany('posts', {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 () { var createdUsers = []; var createdPassports = []; diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 4ac1f91b5..01cc01142 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -615,9 +615,17 @@ describe('DataSource connector types', function() { describe('DataSource constructor', function () { // Mocked require var loader = function (name) { + if (name.indexOf('./connectors/') !== -1) { + // ./connectors/ doesn't exist + return null; + } + if (name === 'loopback-connector-abc') { + // Assume loopback-connector-abc doesn't exist + return null; + } return { name: name - } + }; }; it('should resolve connector by path', function () { @@ -632,9 +640,20 @@ describe('DataSource constructor', function () { var connector = DataSource._resolveConnector('loopback-connector-xyz', loader); 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); 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 () { var connector = DataSource._resolveConnector('loopback-xyz', loader);