Create scoped methods for belongsTo and improve docs
This commit is contained in:
parent
1dc0c34252
commit
cadacc44bb
|
@ -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,13 +71,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);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
98
lib/dao.js
98
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]);
|
||||
});
|
||||
|
@ -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);
|
||||
|
|
103
lib/relations.js
103
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];
|
||||
|
@ -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,14 +334,41 @@ Relation.belongsTo = function (anotherClass, params) {
|
|||
}
|
||||
};
|
||||
|
||||
// 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};
|
||||
// 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
|
||||
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 || {};
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue