Create scoped methods for belongsTo and improve docs

This commit is contained in:
Raymond Feng 2014-03-11 15:38:07 -07:00
parent 1dc0c34252
commit cadacc44bb
4 changed files with 196 additions and 67 deletions

View File

@ -3,6 +3,7 @@ var ds = new DataSource('memory');
var Order = ds.createModel('Order', {
customerId: Number,
items: [String],
orderDate: Date
});
@ -13,7 +14,7 @@ var Customer = ds.createModel('Customer', {
Order.belongsTo(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) {
order.customer(console.log);
order.customer(true, console.log);
@ -22,6 +23,16 @@ Customer.create({name: 'John'}, function (err, customer) {
order.customer(console.log);
});
});
Order.create({orderDate: new Date(), items: ['Phone']}, function (err, order2) {
order2.customer.create({name: 'Smith'}, function(err, customer2) {
console.log(order2, customer2);
});
var customer3 = order2.customer.build({name: 'Tom'});
console.log('Customer 3', customer3);
});
});
Customer.hasMany(Order, {as: 'orders', foreignKey: 'customerId'});
@ -60,12 +71,31 @@ 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);
});
});
});
});
});
});
});
@ -85,6 +115,15 @@ Assembly.create({name: 'car'}, function (err, assembly) {
Part.create({partNumber: 'engine'}, function (err, part) {
assembly.parts.add(part, function (err) {
assembly.parts(console.log);
// Build an part?
var part3 = assembly.parts.build({partNumber: 'door'});
console.log('Part3: ', part3, part3.constructor.modelName);
// Create a part?
assembly.parts.create({name: 'door'}, function(err, part4) {
console.log('Part4: ', part4, part4.constructor.modelName);
});
});
});

View File

@ -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]);
});
@ -844,7 +846,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 +913,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 +939,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);

View File

@ -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];
@ -71,11 +82,14 @@ Relation.hasMany = function hasMany(anotherClass, params) {
// 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 +100,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 +119,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 +131,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 +152,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 +174,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 +183,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 +192,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 +202,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 +270,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 +284,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 +293,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,15 +334,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>
// 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};
// 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 +379,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 || {};

View File

@ -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