loopback-datasource-juggler/lib/relation-definition.js

1336 lines
40 KiB
JavaScript
Raw Normal View History

2014-06-15 22:53:58 +00:00
/*!
* Dependencies
*/
var assert = require('assert');
var util = require('util');
var i8n = require('inflection');
var defineScope = require('./scope.js').defineScope;
var mergeQuery = require('./scope.js').mergeQuery;
2014-06-15 22:53:58 +00:00
var ModelBaseClass = require('./model.js');
exports.Relation = Relation;
exports.RelationDefinition = RelationDefinition;
var RelationTypes = {
belongsTo: 'belongsTo',
hasMany: 'hasMany',
hasOne: 'hasOne',
hasAndBelongsToMany: 'hasAndBelongsToMany'
};
exports.RelationTypes = RelationTypes;
exports.HasMany = HasMany;
exports.HasManyThrough = HasManyThrough;
exports.HasOne = HasOne;
exports.HasAndBelongsToMany = HasAndBelongsToMany;
exports.BelongsTo = BelongsTo;
var RelationClasses = {
belongsTo: BelongsTo,
hasMany: HasMany,
hasManyThrough: HasManyThrough,
hasOne: HasOne,
hasAndBelongsToMany: HasAndBelongsToMany
};
function normalizeType(type) {
if (!type) {
return type;
}
var t1 = type.toLowerCase();
for (var t2 in RelationTypes) {
if (t2.toLowerCase() === t1) {
return t2;
}
}
return null;
}
/**
* Relation definition class. Use to define relationships between models.
* @param {Object} definition
* @class RelationDefinition
*/
function RelationDefinition(definition) {
if (!(this instanceof RelationDefinition)) {
return new RelationDefinition(definition);
}
definition = definition || {};
this.name = definition.name;
assert(this.name, 'Relation name is missing');
this.type = normalizeType(definition.type);
assert(this.type, 'Invalid relation type: ' + definition.type);
this.modelFrom = definition.modelFrom;
2014-06-21 18:44:33 +00:00
assert(this.modelFrom, 'Source model is required');
2014-06-15 22:53:58 +00:00
this.keyFrom = definition.keyFrom;
this.modelTo = definition.modelTo;
this.keyTo = definition.keyTo;
2014-07-26 10:47:55 +00:00
this.typeTo = definition.typeTo;
if (!this.typeTo) {
assert(this.modelTo, 'Target model is required');
}
2014-06-15 22:53:58 +00:00
this.modelThrough = definition.modelThrough;
this.keyThrough = definition.keyThrough;
this.multiple = (this.type !== 'belongsTo' && this.type !== 'hasOne');
2014-07-11 22:02:16 +00:00
this.properties = definition.properties || {};
this.options = definition.options || {};
this.scope = definition.scope;
2014-06-15 22:53:58 +00:00
}
RelationDefinition.prototype.toJSON = function () {
var json = {
name: this.name,
type: this.type,
modelFrom: this.modelFrom.modelName,
keyFrom: this.keyFrom,
modelTo: this.modelTo.modelName,
keyTo: this.keyTo,
multiple: this.multiple
};
if (this.modelThrough) {
json.modelThrough = this.modelThrough.modelName;
json.keyThrough = this.keyThrough;
}
return json;
};
/**
* Apply the configured scope to the filter/query object.
* @param {Object} modelInstance
* @param {Object} filter (where, order, limit, fields, ...)
*/
RelationDefinition.prototype.applyScope = function(modelInstance, filter) {
2014-07-26 10:47:55 +00:00
filter.where = filter.where || {};
if ((this.type !== 'belongsTo' || this.type === 'hasOne')
&& typeof this.typeTo === 'string') { // polymorphic
filter.where[this.typeTo] = this.modelFrom.modelName;
2014-07-26 10:47:55 +00:00
}
if (typeof this.scope === 'function') {
var scope = this.scope.call(this, modelInstance, filter);
2014-07-26 10:47:55 +00:00
} else {
var scope = this.scope;
}
if (typeof scope === 'object') {
mergeQuery(filter, scope);
}
};
/**
2014-07-11 22:02:16 +00:00
* Apply the configured properties to the target object.
* @param {Object} modelInstance
* @param {Object} target
*/
2014-07-11 22:02:16 +00:00
RelationDefinition.prototype.applyProperties = function(modelInstance, target) {
if (typeof this.properties === 'function') {
var data = this.properties.call(this, modelInstance);
for(var k in data) {
target[k] = data[k];
}
2014-07-11 22:02:16 +00:00
} else if (typeof this.properties === 'object') {
for(var k in this.properties) {
var key = this.properties[k];
target[key] = modelInstance[k];
}
}
if ((this.type !== 'belongsTo' || this.type === 'hasOne')
&& typeof this.typeTo === 'string') { // polymorphic
target[this.typeTo] = this.modelFrom.modelName;
2014-07-26 10:47:55 +00:00
}
};
2014-06-15 22:53:58 +00:00
/**
* A relation attaching to a given model instance
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {Relation}
* @constructor
* @class Relation
*/
function Relation(definition, modelInstance) {
if (!(this instanceof Relation)) {
return new Relation(definition, modelInstance);
}
if (!(definition instanceof RelationDefinition)) {
definition = new RelationDefinition(definition);
}
this.definition = definition;
this.modelInstance = modelInstance;
}
2014-06-21 18:44:33 +00:00
Relation.prototype.resetCache = function (cache) {
cache = cache || undefined;
this.modelInstance.__cachedRelations[this.definition.name] = cache;
};
Relation.prototype.getCache = function () {
return this.modelInstance.__cachedRelations[this.definition.name];
};
2014-06-15 22:53:58 +00:00
/**
* HasMany subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {HasMany}
* @constructor
* @class HasMany
*/
function HasMany(definition, modelInstance) {
if (!(this instanceof HasMany)) {
return new HasMany(definition, modelInstance);
}
assert(definition.type === RelationTypes.hasMany);
Relation.apply(this, arguments);
}
util.inherits(HasMany, Relation);
2014-06-21 18:44:33 +00:00
HasMany.prototype.removeFromCache = function (id) {
var cache = this.modelInstance.__cachedRelations[this.definition.name];
var idName = this.definition.modelTo.definition.idName();
if (Array.isArray(cache)) {
for (var i = 0, n = cache.length; i < n; i++) {
if (cache[i][idName] === id) {
return cache.splice(i, 1);
}
}
}
return null;
};
HasMany.prototype.addToCache = function (inst) {
if (!inst) {
return;
}
var cache = this.modelInstance.__cachedRelations[this.definition.name];
if (cache === undefined) {
cache = this.modelInstance.__cachedRelations[this.definition.name] = [];
}
var idName = this.definition.modelTo.definition.idName();
if (Array.isArray(cache)) {
for (var i = 0, n = cache.length; i < n; i++) {
if (cache[i][idName] === inst[idName]) {
cache[i] = inst;
return;
}
}
cache.push(inst);
}
};
2014-06-15 22:53:58 +00:00
/**
* HasManyThrough subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {HasManyThrough}
* @constructor
* @class HasManyThrough
*/
function HasManyThrough(definition, modelInstance) {
if (!(this instanceof HasManyThrough)) {
return new HasManyThrough(definition, modelInstance);
}
assert(definition.type === RelationTypes.hasMany);
assert(definition.modelThrough);
HasMany.apply(this, arguments);
}
util.inherits(HasManyThrough, HasMany);
/**
* BelongsTo subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {BelongsTo}
* @constructor
* @class BelongsTo
*/
function BelongsTo(definition, modelInstance) {
if (!(this instanceof BelongsTo)) {
return new BelongsTo(definition, modelInstance);
}
assert(definition.type === RelationTypes.belongsTo);
Relation.apply(this, arguments);
}
util.inherits(BelongsTo, Relation);
/**
* HasAndBelongsToMany subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {HasAndBelongsToMany}
* @constructor
* @class HasAndBelongsToMany
*/
function HasAndBelongsToMany(definition, modelInstance) {
if (!(this instanceof HasAndBelongsToMany)) {
return new HasAndBelongsToMany(definition, modelInstance);
}
assert(definition.type === RelationTypes.hasAndBelongsToMany);
Relation.apply(this, arguments);
}
util.inherits(HasAndBelongsToMany, Relation);
/**
* HasOne subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {HasOne}
* @constructor
* @class HasOne
*/
function HasOne(definition, modelInstance) {
if (!(this instanceof HasOne)) {
return new HasOne(definition, modelInstance);
}
assert(definition.type === RelationTypes.hasOne);
Relation.apply(this, arguments);
}
util.inherits(HasOne, Relation);
/*!
* Find the relation by foreign key
* @param {*} foreignKey The foreign key
* @returns {Object} The relation object
*/
2014-06-16 07:36:12 +00:00
function findBelongsTo(modelFrom, modelTo, keyTo) {
var relations = modelFrom.relations;
2014-06-15 22:53:58 +00:00
var keys = Object.keys(relations);
for (var k = 0; k < keys.length; k++) {
var rel = relations[keys[k]];
if (rel.type === RelationTypes.belongsTo &&
2014-06-16 07:36:12 +00:00
rel.modelTo === modelTo &&
2014-06-21 18:44:33 +00:00
(keyTo === undefined || rel.keyTo === keyTo)) {
2014-06-16 07:36:12 +00:00
return rel.keyFrom;
2014-06-15 22:53:58 +00:00
}
}
return null;
}
/*!
* 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];
}
var lookupClassName = modelName.toLowerCase();
for (var name in models) {
if (name.toLowerCase() === lookupClassName) {
return models[name];
}
}
}
/**
* Define a "one to many" relationship by specifying the model name
*
* Examples:
* ```
* User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});
* ```
*
* ```
* Book.hasMany(Chapter);
* ```
* Or, equivalently:
* ```
* Book.hasMany('chapters', {model: Chapter});
* ```
* @param {Model} modelFrom Source model class
* @param {Object|String} modelTo Model object (or String name of model) to which you are creating the relationship.
* @options {Object} params Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
* @property {String} foreignKey Property name of foreign key field.
* @property {Object} model Model object
*/
RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
var thisClassName = modelFrom.modelName;
params = params || {};
if (typeof modelTo === 'string') {
params.as = modelTo;
if (params.model) {
modelTo = params.model;
} else {
var modelToName = i8n.singularize(modelTo).toLowerCase();
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
}
}
2014-07-26 10:47:55 +00:00
2014-06-15 22:53:58 +00:00
var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true);
var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
var typeTo;
2014-07-26 10:47:55 +00:00
if (typeof params.polymorphic === 'string') {
var polymorphic = params.polymorphic;
fk = i8n.camelize(polymorphic + '_id', true);
typeTo = i8n.camelize(polymorphic + '_type', true);
2014-07-26 10:47:55 +00:00
if (!params.through) {
modelTo.dataSource.defineProperty(modelTo.modelName, typeTo, { type: 'string', index: true });
2014-07-26 10:47:55 +00:00
}
}
2014-06-15 22:53:58 +00:00
var definition = new RelationDefinition({
name: relationName,
type: RelationTypes.hasMany,
modelFrom: modelFrom,
keyFrom: idName,
keyTo: fk,
2014-07-26 10:47:55 +00:00
typeTo: typeTo,
2014-06-15 22:53:58 +00:00
modelTo: modelTo,
multiple: true,
2014-07-11 22:02:16 +00:00
properties: params.properties,
scope: params.scope,
options: params.options
2014-06-15 22:53:58 +00:00
});
definition.modelThrough = params.through;
var keyThrough = definition.throughKey || i8n.camelize(modelTo.modelName + '_id', true);
definition.keyThrough = keyThrough;
2014-06-15 22:53:58 +00:00
modelFrom.relations[relationName] = definition;
if (!params.through) {
// obviously, modelTo should have attribute called `fk`
2014-07-26 10:47:55 +00:00
// for polymorphic relations, it is assumed to share the same fk type for all
// polymorphic models
modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName);
2014-06-15 22:53:58 +00:00
}
var scopeMethods = {
findById: scopeMethod(definition, 'findById'),
2014-07-15 15:50:34 +00:00
destroy: scopeMethod(definition, 'destroyById'),
updateById: scopeMethod(definition, 'updateById'),
exists: scopeMethod(definition, 'exists')
};
var findByIdFunc = scopeMethods.findById;
modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
var destroyByIdFunc = scopeMethods.destroy;
modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
var updateByIdFunc = scopeMethods.updateById;
modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
2014-06-15 22:53:58 +00:00
if(definition.modelThrough) {
scopeMethods.create = scopeMethod(definition, 'create');
scopeMethods.add = scopeMethod(definition, 'add');
scopeMethods.remove = scopeMethod(definition, 'remove');
2014-07-15 15:50:34 +00:00
var addFunc = scopeMethods.add;
modelFrom.prototype['__link__' + relationName] = addFunc;
var removeFunc = scopeMethods.remove;
modelFrom.prototype['__unlink__' + relationName] = removeFunc;
} else {
scopeMethods.create = scopeMethod(definition, 'create');
scopeMethods.build = scopeMethod(definition, 'build');
2014-06-15 22:53:58 +00:00
}
2014-06-15 22:53:58 +00:00
// Mix the property and scoped methods into the prototype class
defineScope(modelFrom.prototype, params.through || modelTo, relationName, function () {
var filter = {};
filter.where = {};
filter.where[fk] = this[idName];
definition.applyScope(this, filter);
2014-06-15 22:53:58 +00:00
if (params.through) {
filter.collect = i8n.camelize(modelTo.modelName, true);
filter.include = filter.collect;
}
2014-06-15 22:53:58 +00:00
return filter;
}, scopeMethods);
};
function scopeMethod(definition, methodName) {
2014-07-15 15:50:34 +00:00
var relationClass = RelationClasses[definition.type];
if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
relationClass = RelationClasses.hasManyThrough;
}
2014-06-15 22:53:58 +00:00
var method = function () {
var relation = new relationClass(definition, this);
return relation[methodName].apply(relation, arguments);
};
2014-07-15 15:50:34 +00:00
var relationMethod = relationClass.prototype[methodName];
if (relationMethod.shared) {
method.shared = true;
method.accepts = relationMethod.accepts;
method.returns = relationMethod.returns;
method.http = relationMethod.http;
method.description = relationMethod.description;
}
2014-06-15 22:53:58 +00:00
return method;
}
2014-07-15 15:50:34 +00:00
/**
* Find a related item by foreign key
* @param {*} fkId The foreign key
* @param {Function} cb The callback function
*/
HasMany.prototype.findById = function (fkId, cb) {
2014-06-15 22:53:58 +00:00
var modelTo = this.definition.modelTo;
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
2014-07-15 15:50:34 +00:00
var idName = this.definition.modelTo.definition.idName();
var filter = {};
filter.where = {};
2014-07-15 15:50:34 +00:00
filter.where[idName] = fkId;
filter.where[fk] = modelInstance[pk];
this.definition.applyScope(modelInstance, filter);
modelTo.findOne(filter, function (err, inst) {
2014-06-15 22:53:58 +00:00
if (err) {
return cb(err);
}
if (!inst) {
2014-07-15 15:50:34 +00:00
err = new Error('No instance with id ' + fkId + ' found for ' + modelTo.modelName);
err.statusCode = 404;
return cb(err);
2014-06-15 22:53:58 +00:00
}
// Check if the foreign key matches the primary key
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
cb(null, inst);
} else {
2014-07-15 15:50:34 +00:00
err = new Error('Key mismatch: ' + this.definition.modelFrom.modelName + '.' + pk
+ ': ' + modelInstance[pk]
+ ', ' + modelTo.modelName + '.' + fk + ': ' + inst[fk]);
err.statusCode = 400;
cb(err);
2014-06-15 22:53:58 +00:00
}
});
};
2014-07-15 15:50:34 +00:00
/**
* Find a related item by foreign key
* @param {*} fkId The foreign key
* @param {Function} cb The callback function
*/
HasMany.prototype.exists = function (fkId, cb) {
2014-06-15 22:53:58 +00:00
var modelTo = this.definition.modelTo;
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
2014-07-15 15:50:34 +00:00
modelTo.findById(fkId, function (err, inst) {
2014-06-15 22:53:58 +00:00
if (err) {
return cb(err);
}
if (!inst) {
2014-07-15 15:50:34 +00:00
return cb(null, false);
2014-06-15 22:53:58 +00:00
}
// Check if the foreign key matches the primary key
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
2014-07-15 15:50:34 +00:00
cb(null, true);
2014-06-15 22:53:58 +00:00
} else {
2014-07-15 15:50:34 +00:00
cb(null, false);
}
});
};
/**
* Update a related item by foreign key
* @param {*} fkId The foreign key
* @param {Function} cb The callback function
*/
HasMany.prototype.updateById = function (fkId, data, cb) {
this.findById(fkId, function (err, inst) {
if (err) {
return cb && cb(err);
}
inst.updateAttributes(data, cb);
});
};
/**
* Delete a related item by foreign key
* @param {*} fkId The foreign key
* @param {Function} cb The callback function
*/
HasMany.prototype.destroyById = function (fkId, cb) {
2014-06-21 18:44:33 +00:00
var self = this;
2014-07-15 15:50:34 +00:00
this.findById(fkId, function(err, inst) {
2014-06-15 22:53:58 +00:00
if (err) {
return cb(err);
}
2014-07-15 15:50:34 +00:00
self.removeFromCache(inst[fkId]);
inst.destroy(cb);
2014-06-15 22:53:58 +00:00
});
};
2014-07-26 15:20:25 +00:00
var throughKeys = function(definition) {
var modelThrough = definition.modelThrough;
var pk2 = definition.modelTo.definition.idName();
if (definition.typeTo) { // polymorphic
var fk1 = definition.keyTo;
var fk2 = definition.keyThrough;
} else {
var fk1 = findBelongsTo(modelThrough, definition.modelFrom,
definition.keyFrom);
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2);
}
return [fk1, fk2];
}
2014-07-15 15:50:34 +00:00
/**
* Find a related item by foreign key
* @param {*} fkId The foreign key value
* @param {Function} cb The callback function
*/
HasManyThrough.prototype.findById = function (fkId, cb) {
var self = this;
var modelTo = this.definition.modelTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var modelThrough = this.definition.modelThrough;
self.exists(fkId, function (err, exists) {
if (err || !exists) {
if (!err) {
err = new Error('No relation found in ' + modelThrough.modelName
+ ' for (' + self.definition.modelFrom.modelName + '.' + modelInstance[pk]
+ ',' + modelTo.modelName + '.' + fkId + ')');
err.statusCode = 404;
}
return cb(err);
}
modelTo.findById(fkId, function (err, inst) {
if (err) {
return cb(err);
}
if (!inst) {
err = new Error('No instance with id ' + fkId + ' found for ' + modelTo.modelName);
err.statusCode = 404;
return cb(err);
}
cb(err, inst);
});
});
};
/**
* Delete a related item by foreign key
* @param {*} fkId The foreign key
* @param {Function} cb The callback function
*/
HasManyThrough.prototype.destroyById = function (fkId, cb) {
var self = this;
var modelTo = this.definition.modelTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var modelThrough = this.definition.modelThrough;
self.exists(fkId, function (err, exists) {
if (err || !exists) {
if (!err) {
err = new Error('No record found in ' + modelThrough.modelName
+ ' for (' + self.definition.modelFrom.modelName + '.' + modelInstance[pk]
+ ' ,' + modelTo.modelName + '.' + fkId + ')');
err.statusCode = 404;
}
return cb(err);
2014-06-15 22:53:58 +00:00
}
2014-07-15 15:50:34 +00:00
self.remove(fkId, function(err) {
if(err) {
return cb(err);
}
modelTo.deleteById(fkId, cb);
});
2014-06-15 22:53:58 +00:00
});
};
// 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
HasManyThrough.prototype.create = function create(data, done) {
2014-06-21 18:44:33 +00:00
var self = this;
2014-06-16 07:36:12 +00:00
var definition = this.definition;
var modelTo = definition.modelTo;
var modelThrough = definition.modelThrough;
2014-06-15 22:53:58 +00:00
if (typeof data === 'function' && !done) {
done = data;
data = {};
}
2014-06-16 07:36:12 +00:00
var modelInstance = this.modelInstance;
2014-06-15 22:53:58 +00:00
// First create the target model
2014-06-21 18:44:33 +00:00
modelTo.create(data, function (err, to) {
2014-06-15 22:53:58 +00:00
if (err) {
2014-06-21 18:44:33 +00:00
return done && done(err, to);
2014-06-15 22:53:58 +00:00
}
2014-06-16 07:36:12 +00:00
// The primary key for the target model
2014-06-21 18:44:33 +00:00
var pk2 = definition.modelTo.definition.idName();
2014-07-26 15:20:25 +00:00
var keys = throughKeys(definition);
var fk1 = keys[0];
var fk2 = keys[1];
2014-06-15 22:53:58 +00:00
var d = {};
2014-06-16 07:36:12 +00:00
d[fk1] = modelInstance[definition.keyFrom];
2014-06-21 18:44:33 +00:00
d[fk2] = to[pk2];
2014-07-11 22:02:16 +00:00
definition.applyProperties(modelInstance, d);
2014-06-15 22:53:58 +00:00
// Then create the through model
2014-06-21 18:44:33 +00:00
modelThrough.create(d, function (e, through) {
2014-06-15 22:53:58 +00:00
if (e) {
// Undo creation of the target model
2014-06-21 18:44:33 +00:00
to.destroy(function () {
2014-06-15 22:53:58 +00:00
done && done(e);
});
} else {
2014-06-21 18:44:33 +00:00
self.addToCache(to);
done && done(err, to);
2014-06-15 22:53:58 +00:00
}
});
});
};
2014-07-26 15:20:25 +00:00
2014-06-15 22:53:58 +00:00
/**
* Add the target model instance to the 'hasMany' relation
* @param {Object|ID} acInst The actual instance or id value
*/
HasManyThrough.prototype.add = function (acInst, done) {
2014-06-21 18:44:33 +00:00
var self = this;
2014-06-16 07:36:12 +00:00
var definition = this.definition;
var modelThrough = definition.modelThrough;
var pk1 = definition.keyFrom;
2014-06-15 22:53:58 +00:00
var data = {};
var query = {};
2014-07-26 15:20:25 +00:00
2014-06-16 07:36:12 +00:00
// The primary key for the target model
2014-06-21 18:44:33 +00:00
var pk2 = definition.modelTo.definition.idName();
2014-06-16 07:36:12 +00:00
2014-07-26 15:20:25 +00:00
var keys = throughKeys(definition);
var fk1 = keys[0];
var fk2 = keys[1];
2014-06-16 07:36:12 +00:00
query[fk1] = this.modelInstance[pk1];
2014-07-15 23:09:54 +00:00
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
var filter = { where: query };
definition.applyScope(this.modelInstance, filter);
2014-06-16 07:36:12 +00:00
data[fk1] = this.modelInstance[pk1];
2014-07-15 23:09:54 +00:00
data[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
2014-07-26 15:20:25 +00:00
2014-07-11 22:02:16 +00:00
definition.applyProperties(this.modelInstance, data);
2014-06-16 07:36:12 +00:00
2014-06-15 22:53:58 +00:00
// Create an instance of the through model
modelThrough.findOrCreate(filter, data, function(err, ac) {
2014-06-21 18:44:33 +00:00
if(!err) {
if (acInst instanceof definition.modelTo) {
self.addToCache(acInst);
}
}
done(err, ac);
});
2014-06-15 22:53:58 +00:00
};
2014-07-15 15:50:34 +00:00
/**
* Check if the target model instance is related to the 'hasMany' relation
* @param {Object|ID} acInst The actual instance or id value
*/
HasManyThrough.prototype.exists = function (acInst, done) {
var definition = this.definition;
var modelThrough = definition.modelThrough;
var pk1 = definition.keyFrom;
var data = {};
var query = {};
// The primary key for the target model
var pk2 = definition.modelTo.definition.idName();
2014-07-26 15:20:25 +00:00
var keys = throughKeys(definition);
var fk1 = keys[0];
var fk2 = keys[1];
2014-07-15 23:09:54 +00:00
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
2014-07-15 15:50:34 +00:00
data[fk1] = this.modelInstance[pk1];
2014-07-15 23:09:54 +00:00
data[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
2014-07-15 15:50:34 +00:00
modelThrough.count(query, function(err, ac) {
done(err, ac > 0);
});
};
2014-06-15 22:53:58 +00:00
/**
* Remove the target model instance from the 'hasMany' relation
* @param {Object|ID) acInst The actual instance or id value
*/
HasManyThrough.prototype.remove = function (acInst, done) {
2014-06-21 18:44:33 +00:00
var self = this;
var definition = this.definition;
var modelThrough = definition.modelThrough;
var pk1 = definition.keyFrom;
2014-06-15 22:53:58 +00:00
2014-06-21 18:44:33 +00:00
var query = {};
// The primary key for the target model
var pk2 = definition.modelTo.definition.idName();
2014-07-26 15:20:25 +00:00
var keys = throughKeys(definition);
var fk1 = keys[0];
var fk2 = keys[1];
2014-06-21 18:44:33 +00:00
query[fk1] = this.modelInstance[pk1];
2014-07-15 23:09:54 +00:00
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
var filter = { where: query };
definition.applyScope(this.modelInstance, filter);
2014-07-26 15:20:25 +00:00
modelThrough.deleteAll(filter.where, function (err) {
2014-06-21 18:44:33 +00:00
if (!err) {
self.removeFromCache(query[fk2]);
}
done(err);
});
2014-06-15 22:53:58 +00:00
};
/**
* Declare "belongsTo" relation that sets up a one-to-one connection with
* another model, such that each instance of the declaring model "belongs to"
* one instance of the other model.
*
* For example, if an application includes users and posts, and each post can
* be written by exactly one user. The following code specifies that `Post` has
* a reference called `author` to the `User` model via the `userId` property of
* `Post` as the foreign key.
* ```
* Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
* ```
*
* This optional parameter default value is false, so the related object will
* be loaded from cache if available.
*
* @param {Class|String} modelTo Model object (or String name of model) to
* which you are creating the relationship.
* @options {Object} params Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that
* corresponds to the foreign key field in the related model.
* @property {String} foreignKey Name of foreign key property.
*
*/
RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
2014-07-26 10:47:55 +00:00
var typeTo, params = params || {};
if ('string' === typeof modelTo && !params.polymorphic) {
2014-06-15 22:53:58 +00:00
params.as = modelTo;
if (params.model) {
modelTo = params.model;
} else {
var modelToName = modelTo.toLowerCase();
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
}
}
2014-07-26 10:47:55 +00:00
if (params.polymorphic) {
var polymorphic = modelTo;
modelTo = null; // will lookup dynamically
var idName = params.idName || 'id';
var relationName = params.as || i8n.camelize(polymorphic, true);
var fk = i8n.camelize(polymorphic + '_id', true);
var typeTo = i8n.camelize(polymorphic + '_type', true);
if (typeof params.idType === 'string') { // explicit key type
modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, { type: params.idType, index: true });
2014-07-26 10:47:55 +00:00
} else { // try to use the same foreign key type as modelFrom
modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelFrom.modelName);
}
modelFrom.dataSource.defineProperty(modelFrom.modelName, typeTo, { type: 'string', index: true });
2014-07-26 10:47:55 +00:00
} else {
var idName = modelFrom.dataSource.idName(modelTo.modelName) || 'id';
var relationName = params.as || i8n.camelize(modelTo.modelName, true);
var fk = params.foreignKey || relationName + 'Id';
modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelTo.modelName);
}
2014-06-15 22:53:58 +00:00
var relationDef = modelFrom.relations[relationName] = new RelationDefinition({
name: relationName,
type: RelationTypes.belongsTo,
modelFrom: modelFrom,
keyFrom: fk,
keyTo: idName,
2014-07-26 10:47:55 +00:00
typeTo: typeTo,
modelTo: modelTo,
properties: params.properties,
scope: params.scope,
options: params.options
2014-06-15 22:53:58 +00:00
});
2014-07-26 10:47:55 +00:00
2014-06-15 22:53:58 +00:00
// Define a property for the scope so that we have 'this' for the scoped methods
Object.defineProperty(modelFrom.prototype, relationName, {
enumerable: true,
configurable: true,
get: function() {
var relation = new BelongsTo(relationDef, this);
2014-06-16 20:46:17 +00:00
var relationMethod = relation.related.bind(relation);
2014-06-15 22:53:58 +00:00
relationMethod.create = relation.create.bind(relation);
relationMethod.build = relation.build.bind(relation);
2014-07-26 10:47:55 +00:00
if (relationDef.modelTo) {
relationMethod._targetClass = relationDef.modelTo.modelName;
}
2014-06-15 22:53:58 +00:00
return relationMethod;
}
});
2014-06-16 20:46:17 +00:00
2014-06-21 06:54:46 +00:00
// FIXME: [rfeng] Wrap the property into a function for remoting
2014-06-16 20:46:17 +00:00
// so that it can be accessed as /api/<model>/<id>/<belongsToRelationName>
// For example, /api/orders/1/customer
var fn = function() {
var f = this[relationName];
f.apply(this, arguments);
};
modelFrom.prototype['__get__' + relationName] = fn;
2014-06-15 22:53:58 +00:00
};
BelongsTo.prototype.create = function(targetModelData, cb) {
2014-06-21 18:44:33 +00:00
var self = this;
2014-06-15 22:53:58 +00:00
var modelTo = this.definition.modelTo;
var fk = this.definition.keyFrom;
var pk = this.definition.keyTo;
2014-06-15 22:53:58 +00:00
var modelInstance = this.modelInstance;
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
this.definition.applyProperties(modelInstance, targetModelData || {});
2014-07-26 10:47:55 +00:00
2014-06-15 22:53:58 +00:00
modelTo.create(targetModelData, function(err, targetModel) {
if(!err) {
modelInstance[fk] = targetModel[pk];
2014-06-21 18:44:33 +00:00
self.resetCache(targetModel);
2014-06-15 22:53:58 +00:00
cb && cb(err, targetModel);
} else {
cb && cb(err);
}
});
};
BelongsTo.prototype.build = function(targetModelData) {
var modelTo = this.definition.modelTo;
this.definition.applyProperties(this.modelInstance, targetModelData || {});
2014-06-15 22:53:58 +00:00
return new modelTo(targetModelData);
};
/**
* 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
*
* @param refresh
* @param params
* @returns {*}
*/
BelongsTo.prototype.related = function (refresh, params) {
2014-06-21 18:44:33 +00:00
var self = this;
2014-07-26 10:47:55 +00:00
var modelFrom = this.definition.modelFrom;
2014-06-15 22:53:58 +00:00
var modelTo = this.definition.modelTo;
2014-07-26 10:47:55 +00:00
var typeTo = this.definition.typeTo;
2014-06-15 22:53:58 +00:00
var pk = this.definition.keyTo;
var fk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
2014-07-11 22:02:16 +00:00
2014-06-15 22:53:58 +00:00
if (arguments.length === 1) {
params = refresh;
refresh = false;
} else if (arguments.length > 2) {
throw new Error('Method can\'t be called with more than two arguments');
}
2014-07-26 10:47:55 +00:00
2014-06-15 22:53:58 +00:00
var cachedValue;
2014-06-21 18:44:33 +00:00
if (!refresh) {
cachedValue = self.getCache();
2014-06-15 22:53:58 +00:00
}
if (params instanceof ModelBaseClass) { // acts as setter
2014-07-26 10:47:55 +00:00
modelTo = params.constructor;
2014-06-15 22:53:58 +00:00
modelInstance[fk] = params[pk];
2014-07-26 10:47:55 +00:00
if (typeTo) modelInstance[typeTo] = params.constructor.modelName;
2014-06-21 18:44:33 +00:00
self.resetCache(params);
2014-06-15 22:53:58 +00:00
} else if (typeof params === 'function') { // acts as async getter
2014-07-26 10:47:55 +00:00
if (typeTo && !modelTo) {
var modelToName = modelInstance[typeTo];
if (typeof modelToName !== 'string') {
throw new Error('Polymorphic model not found: `' + typeTo + '` not set');
}
modelToName = modelToName.toLowerCase();
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
if (!modelTo) {
throw new Error('Polymorphic model not found: `' + modelToName + '`');
}
}
2014-06-15 22:53:58 +00:00
var cb = params;
if (cachedValue === undefined) {
var query = {where: {}};
query.where[pk] = modelInstance[fk];
this.definition.applyScope(modelInstance, query);
modelTo.findOne(query, function (err, inst) {
2014-06-15 22:53:58 +00:00
if (err) {
return cb(err);
}
if (!inst) {
return cb(null, null);
}
// Check if the foreign key matches the primary key
if (inst[pk] && modelInstance[fk]
&& inst[pk].toString() === modelInstance[fk].toString()) {
2014-06-21 18:44:33 +00:00
self.resetCache(inst);
2014-06-15 22:53:58 +00:00
cb(null, inst);
} else {
2014-07-15 15:50:34 +00:00
err = new Error('Key mismatch: ' + self.definition.modelFrom.modelName + '.' + fk
+ ': ' + modelInstance[fk]
+ ', ' + modelTo.modelName + '.' + pk + ': ' + inst[pk]);
err.statusCode = 400;
cb(err);
2014-06-15 22:53:58 +00:00
}
});
return modelInstance[fk];
} else {
cb(null, cachedValue);
return cachedValue;
}
} else if (params === undefined) { // acts as sync getter
return cachedValue;
2014-06-15 22:53:58 +00:00
} else { // setter
modelInstance[fk] = params;
2014-06-21 18:44:33 +00:00
self.resetCache();
2014-06-15 22:53:58 +00:00
}
};
/**
2014-06-19 18:13:24 +00:00
* A hasAndBelongsToMany relation creates a direct many-to-many connection with
* another model, with no intervening model. For example, if your application
* includes users and groups, with each group having many users and each user
* appearing in many groups, you could declare the models this way:
2014-06-15 22:53:58 +00:00
* ```
* User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
* ```
*
2014-06-19 18:13:24 +00:00
* @param {String|Object} modelTo Model object (or String name of model) to
* which you are creating the relationship.
2014-06-15 22:53:58 +00:00
* @options {Object} params Configuration parameters; see below.
2014-06-19 18:13:24 +00:00
* @property {String} as Name of the property in the referring model that
* corresponds to the foreign key field in the related model.
2014-06-15 22:53:58 +00:00
* @property {String} foreignKey Property name of foreign key field.
* @property {Object} model Model object
*/
RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, modelTo, params) {
params = params || {};
var models = modelFrom.dataSource.modelBuilder.models;
if ('string' === typeof modelTo) {
params.as = modelTo;
if (params.model) {
modelTo = params.model;
} else {
modelTo = lookupModel(models, i8n.singularize(modelTo)) || modelTo;
2014-06-15 22:53:58 +00:00
}
if (typeof modelTo === 'string') {
throw new Error('Could not find "' + modelTo + '" relation for ' + modelFrom.modelName);
}
}
var isPolymorphic = (typeof params.polymorphic === 'string');
2014-06-15 22:53:58 +00:00
if (!params.through) {
if (isPolymorphic) throw new Error('Polymorphic relations need a through model');
2014-06-15 22:53:58 +00:00
var name1 = modelFrom.modelName + modelTo.modelName;
var name2 = modelTo.modelName + modelFrom.modelName;
params.through = lookupModel(models, name1) || lookupModel(models, name2) ||
modelFrom.dataSource.define(name1);
}
var options = {as: params.as, through: params.through};
if (isPolymorphic) {
options.polymorphic = params.polymorphic;
var accessor = params.through.prototype[params.polymorphic];
if (typeof accessor !== 'function') { // declare once
params.through.belongsTo(params.polymorphic, { polymorphic: true });
}
} else {
params.through.belongsTo(modelFrom);
}
2014-07-26 13:23:40 +00:00
params.through.belongsTo(modelTo);
this.hasMany(modelFrom, modelTo, options);
2014-06-15 22:53:58 +00:00
};
2014-06-16 08:17:37 +00:00
/**
2014-06-19 18:13:24 +00:00
* A HasOne relation creates a one-to-one connection from modelFrom to modelTo.
* This relation indicates that each instance of a model contains or possesses
* one instance of another model. For example, each supplier in your application
* has only one account.
*
* @param {Function} modelFrom The declaring model class
* @param {String|Function} modelTo Model object (or String name of model) to
* which you are creating the relationship.
* @options {Object} params Configuration parameters; see below.
* @property {String} as Name of the property in the referring model that
* corresponds to the foreign key field in the related model.
* @property {String} foreignKey Property name of foreign key field.
* @property {Object} model Model object
2014-06-16 08:17:37 +00:00
*/
RelationDefinition.hasOne = function (modelFrom, modelTo, params) {
params = params || {};
if ('string' === typeof modelTo) {
params.as = modelTo;
if (params.model) {
modelTo = params.model;
} else {
var modelToName = modelTo.toLowerCase();
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
}
}
var pk = modelFrom.dataSource.idName(modelTo.modelName) || 'id';
var relationName = params.as || i8n.camelize(modelTo.modelName, true);
var fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true);
var typeTo;
if (typeof params.polymorphic === 'string') {
var polymorphic = params.polymorphic;
fk = i8n.camelize(polymorphic + '_id', true);
typeTo = i8n.camelize(polymorphic + '_type', true);
if (!params.through) {
modelTo.dataSource.defineProperty(modelTo.modelName, typeTo, { type: 'string', index: true });
}
}
2014-06-16 08:17:37 +00:00
var relationDef = modelFrom.relations[relationName] = new RelationDefinition({
name: relationName,
type: RelationTypes.hasOne,
modelFrom: modelFrom,
keyFrom: pk,
keyTo: fk,
typeTo: typeTo,
modelTo: modelTo,
properties: params.properties,
options: params.options
2014-06-16 08:17:37 +00:00
});
modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName);
// Define a property for the scope so that we have 'this' for the scoped methods
Object.defineProperty(modelFrom.prototype, relationName, {
enumerable: true,
configurable: true,
get: function() {
var relation = new HasOne(relationDef, this);
var relationMethod = relation.related.bind(relation)
relationMethod.create = relation.create.bind(relation);
relationMethod.build = relation.build.bind(relation);
relationMethod._targetClass = relationDef.modelTo.modelName;
2014-06-16 08:17:37 +00:00
return relationMethod;
}
});
};
2014-06-19 18:13:24 +00:00
/**
* Create a target model instance
* @param {Object} targetModelData The target model data
* @callback {Function} [cb] Callback function
* @param {String|Object} err Error string or object
* @param {Object} The newly created target model instance
*/
HasOne.prototype.create = function (targetModelData, cb) {
2014-06-21 18:44:33 +00:00
var self = this;
2014-06-16 08:17:37 +00:00
var modelTo = this.definition.modelTo;
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
2014-06-16 08:17:37 +00:00
targetModelData = targetModelData || {};
targetModelData[fk] = modelInstance[pk];
var query = {where: {}};
query.where[fk] = targetModelData[fk];
this.definition.applyScope(modelInstance, query);
2014-07-11 22:02:16 +00:00
this.definition.applyProperties(modelInstance, targetModelData);
modelTo.findOne(query, function(err, result) {
if(err) {
cb(err);
} else if(result) {
cb(new Error('HasOne relation cannot create more than one instance of '
+ modelTo.modelName));
} else {
modelTo.create(targetModelData, function (err, targetModel) {
if (!err) {
// Refresh the cache
self.resetCache(targetModel);
cb && cb(err, targetModel);
} else {
cb && cb(err);
}
});
}
});
};
/**
* Create a target model instance
* @param {Object} targetModelData The target model data
* @callback {Function} [cb] Callback function
* @param {String|Object} err Error string or object
* @param {Object} The newly created target model instance
*/
HasMany.prototype.create = function (targetModelData, cb) {
2014-06-21 18:44:33 +00:00
var self = this;
2014-06-16 08:17:37 +00:00
var modelTo = this.definition.modelTo;
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
2014-06-16 08:17:37 +00:00
targetModelData = targetModelData || {};
targetModelData[fk] = modelInstance[pk];
2014-07-11 22:02:16 +00:00
this.definition.applyProperties(modelInstance, targetModelData);
2014-06-16 08:17:37 +00:00
modelTo.create(targetModelData, function(err, targetModel) {
if(!err) {
2014-06-21 18:44:33 +00:00
// Refresh the cache
self.addToCache(targetModel);
2014-06-16 08:17:37 +00:00
cb && cb(err, targetModel);
} else {
cb && cb(err);
}
});
};
2014-06-19 18:13:24 +00:00
/**
* Build a target model instance
* @param {Object} targetModelData The target model data
* @returns {Object} The newly built target model instance
*/
HasMany.prototype.build = HasOne.prototype.build = function(targetModelData) {
2014-06-16 08:17:37 +00:00
var modelTo = this.definition.modelTo;
var pk = this.definition.keyFrom;
var fk = this.definition.keyTo;
2014-06-16 08:17:37 +00:00
targetModelData = targetModelData || {};
targetModelData[fk] = this.modelInstance[pk];
2014-07-11 22:02:16 +00:00
this.definition.applyProperties(this.modelInstance, targetModelData);
2014-06-16 08:17:37 +00:00
return new modelTo(targetModelData);
};
/**
* Define the method for the hasOne 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
*
2014-06-19 18:13:24 +00:00
* @param {Boolean} refresh Reload from the data source
* @param {Object|Function} params Query parameters
* @returns {Object}
2014-06-16 08:17:37 +00:00
*/
HasOne.prototype.related = function (refresh, params) {
2014-06-21 18:44:33 +00:00
var self = this;
2014-06-16 08:17:37 +00:00
var modelTo = this.definition.modelTo;
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var definition = this.definition;
2014-06-16 08:17:37 +00:00
var modelInstance = this.modelInstance;
if (arguments.length === 1) {
params = refresh;
refresh = false;
} else if (arguments.length > 2) {
throw new Error('Method can\'t be called with more than two arguments');
}
var cachedValue;
2014-06-21 18:44:33 +00:00
if (!refresh) {
cachedValue = self.getCache();
2014-06-16 08:17:37 +00:00
}
if (params instanceof ModelBaseClass) { // acts as setter
params[fk] = modelInstance[pk];
2014-06-21 18:44:33 +00:00
self.resetCache(params);
2014-06-16 08:17:37 +00:00
} else if (typeof params === 'function') { // acts as async getter
var cb = params;
if (cachedValue === undefined) {
var query = {where: {}};
query.where[fk] = modelInstance[pk];
definition.applyScope(modelInstance, query);
2014-06-16 08:17:37 +00:00
modelTo.findOne(query, function (err, inst) {
if (err) {
return cb(err);
}
if (!inst) {
return cb(null, null);
}
// Check if the foreign key matches the primary key
if (inst[fk] && modelInstance[pk]
&& inst[fk].toString() === modelInstance[pk].toString()) {
2014-06-21 18:44:33 +00:00
self.resetCache(inst);
2014-06-16 08:17:37 +00:00
cb(null, inst);
} else {
2014-07-15 15:50:34 +00:00
err = new Error('Key mismatch: ' + self.definition.modelFrom.modelName + '.' + pk
+ ': ' + modelInstance[pk]
+ ', ' + modelTo.modelName + '.' + fk + ': ' + inst[fk]);
err.statusCode = 400;
cb(err);
2014-06-16 08:17:37 +00:00
}
});
return modelInstance[pk];
} else {
cb(null, cachedValue);
return cachedValue;
}
} else if (params === undefined) { // acts as sync getter
return cachedValue;
2014-06-16 08:17:37 +00:00
} else { // setter
params[fk] = modelInstance[pk];
2014-06-21 18:44:33 +00:00
self.resetCache();
2014-06-16 08:17:37 +00:00
}
};