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

3350 lines
101 KiB
JavaScript
Raw Normal View History

2016-04-06 14:51:49 +00:00
// Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
2014-06-15 22:53:58 +00:00
/*!
* Dependencies
*/
var assert = require('assert');
var util = require('util');
var async = require('async');
var utils = require('./utils');
2014-06-15 22:53:58 +00:00
var i8n = require('inflection');
var defineScope = require('./scope.js').defineScope;
var mergeQuery = utils.mergeQuery;
var idEquals = utils.idEquals;
2014-06-15 22:53:58 +00:00
var ModelBaseClass = require('./model.js');
2014-07-27 14:30:45 +00:00
var applyFilter = require('./connectors/memory').applyFilter;
var ValidationError = require('./validations.js').ValidationError;
var debug = require('debug')('loopback:relations');
2014-06-15 22:53:58 +00:00
var RelationTypes = {
belongsTo: 'belongsTo',
hasMany: 'hasMany',
hasOne: 'hasOne',
2014-07-27 14:30:45 +00:00
hasAndBelongsToMany: 'hasAndBelongsToMany',
referencesMany: 'referencesMany',
2014-08-19 20:10:35 +00:00
embedsOne: 'embedsOne',
embedsMany: 'embedsMany',
2014-06-15 22:53:58 +00:00
};
var RelationClasses = {
belongsTo: BelongsTo,
hasMany: HasMany,
hasManyThrough: HasManyThrough,
hasOne: HasOne,
2014-07-27 14:30:45 +00:00
hasAndBelongsToMany: HasAndBelongsToMany,
referencesMany: ReferencesMany,
2014-08-19 20:10:35 +00:00
embedsOne: EmbedsOne,
embedsMany: EmbedsMany,
2014-06-15 22:53:58 +00:00
};
exports.Relation = Relation;
exports.RelationDefinition = RelationDefinition;
exports.RelationTypes = RelationTypes;
exports.RelationClasses = RelationClasses;
exports.HasMany = HasMany;
exports.HasManyThrough = HasManyThrough;
exports.HasOne = HasOne;
exports.HasAndBelongsToMany = HasAndBelongsToMany;
exports.BelongsTo = BelongsTo;
exports.ReferencesMany = ReferencesMany;
exports.EmbedsOne = EmbedsOne;
exports.EmbedsMany = EmbedsMany;
2014-06-15 22:53:58 +00:00
function normalizeType(type) {
if (!type) {
return type;
}
var t1 = type.toLowerCase();
for (var t2 in RelationTypes) {
if (t2.toLowerCase() === t1) {
return t2;
}
}
return null;
};
function extendScopeMethods(definition, scopeMethods, ext) {
var customMethods = [];
var relationClass = RelationClasses[definition.type];
if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
relationClass = RelationClasses.hasManyThrough;
}
if (typeof ext === 'function') {
customMethods = ext.call(definition, scopeMethods, relationClass);
} else if (typeof ext === 'object') {
function createFunc(definition, relationMethod) {
return function() {
var relation = new relationClass(definition, this);
return relationMethod.apply(relation, arguments);
};
};
for (var key in ext) {
var relationMethod = ext[key];
var method = scopeMethods[key] = createFunc(definition, relationMethod);
if (relationMethod.shared) {
sharedMethod(definition, key, method, relationMethod);
}
customMethods.push(key);
}
}
return [].concat(customMethods || []);
};
2014-06-15 22:53:58 +00:00
function bindRelationMethods(relation, relationMethod, definition) {
var methods = definition.methods || {};
Object.keys(methods).forEach(function(m) {
if (typeof methods[m] !== 'function') return;
relationMethod[m] = methods[m].bind(relation);
});
};
2014-06-15 22:53:58 +00:00
/**
* 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;
this.polymorphic = definition.polymorphic;
if (typeof this.polymorphic !== 'object') {
2014-07-26 10:47:55 +00:00
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 = definition.multiple;
2014-07-11 22:02:16 +00:00
this.properties = definition.properties || {};
this.options = definition.options || {};
this.scope = definition.scope;
2014-07-27 14:30:45 +00:00
this.embed = definition.embed === true;
this.methods = definition.methods || {};
2014-06-15 22:53:58 +00:00
}
RelationDefinition.prototype.toJSON = function() {
var polymorphic = typeof this.polymorphic === 'object';
var modelToName = this.modelTo && this.modelTo.modelName;
if (!modelToName && polymorphic && this.type === 'belongsTo') {
modelToName = '<polymorphic>';
}
2014-06-15 22:53:58 +00:00
var json = {
name: this.name,
type: this.type,
modelFrom: this.modelFrom.modelName,
keyFrom: this.keyFrom,
modelTo: modelToName,
2014-06-15 22:53:58 +00:00
keyTo: this.keyTo,
multiple: this.multiple,
2014-06-15 22:53:58 +00:00
};
if (this.modelThrough) {
json.modelThrough = this.modelThrough.modelName;
json.keyThrough = this.keyThrough;
}
if (polymorphic) {
json.polymorphic = this.polymorphic;
}
2014-06-15 22:53:58 +00:00
return json;
};
/**
* Define a relation scope method
* @param {String} name of the method
* @param {Function} function to define
*/
RelationDefinition.prototype.defineMethod = function(name, fn) {
var relationClass = RelationClasses[this.type];
var relationName = this.name;
var modelFrom = this.modelFrom;
var definition = this;
var method;
if (definition.multiple) {
var scope = this.modelFrom.scopes[this.name];
if (!scope) throw new Error('Unknown relation scope: ' + this.name);
method = scope.defineMethod(name, function() {
var relation = new relationClass(definition, this);
return fn.apply(relation, arguments);
});
} else {
definition.methods[name] = fn;
method = function() {
var rel = this[relationName];
return rel[name].apply(rel, arguments);
};
}
if (method && fn.shared) {
sharedMethod(definition, name, method, fn);
modelFrom.prototype['__' + name + '__' + relationName] = method;
}
return method;
};
/**
* 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-30 11:22:20 +00:00
filter = filter || {};
2014-07-26 10:47:55 +00:00
filter.where = filter.where || {};
2016-05-10 21:25:33 +00:00
if ((this.type !== 'belongsTo' || this.type === 'hasOne') &&
typeof this.polymorphic === 'object') { // polymorphic
var discriminator = this.polymorphic.discriminator;
if (this.polymorphic.invert) {
filter.where[discriminator] = this.modelTo.modelName;
} else {
filter.where[discriminator] = this.modelFrom.modelName;
}
2014-07-26 10:47:55 +00:00
}
2015-02-20 22:57:48 +00:00
var scope;
if (typeof this.scope === 'function') {
2015-02-20 22:57:48 +00:00
scope = this.scope.call(this, modelInstance, filter);
2014-07-26 10:47:55 +00:00
} else {
2015-02-20 22:57:48 +00:00
scope = this.scope;
2014-07-26 10:47:55 +00:00
}
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
*/
RelationDefinition.prototype.applyProperties = function(modelInstance, obj) {
var source = modelInstance, target = obj;
if (this.options.invertProperties) {
2015-02-20 22:57:48 +00:00
source = obj;
target = modelInstance;
}
if (this.options.embedsProperties) {
target = target.__data[this.name] = {};
target[this.keyTo] = source[this.keyTo];
}
2015-02-20 22:57:48 +00:00
var k, key;
2014-07-11 22:02:16 +00:00
if (typeof this.properties === 'function') {
var data = this.properties.call(this, source, target);
for (k in data) {
target[k] = data[k];
}
} else if (Array.isArray(this.properties)) {
for (k = 0; k < this.properties.length; k++) {
2015-02-20 22:57:48 +00:00
key = this.properties[k];
target[key] = source[key];
}
2014-07-11 22:02:16 +00:00
} else if (typeof this.properties === 'object') {
for (k in this.properties) {
2015-02-20 22:57:48 +00:00
key = this.properties[k];
target[key] = source[k];
}
}
2016-05-10 21:25:33 +00:00
if ((this.type !== 'belongsTo' || this.type === 'hasOne') &&
typeof this.polymorphic === 'object') { // polymorphic
var discriminator = this.polymorphic.discriminator;
if (this.polymorphic.invert) {
target[discriminator] = this.modelTo.modelName;
} else {
target[discriminator] = 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;
}
Relation.prototype.resetCache = function(cache) {
2014-06-21 18:44:33 +00:00
cache = cache || undefined;
this.modelInstance.__cachedRelations[this.definition.name] = cache;
};
Relation.prototype.getCache = function() {
2014-06-21 18:44:33 +00:00
return this.modelInstance.__cachedRelations[this.definition.name];
};
Relation.prototype.callScopeMethod = function(methodName) {
var args = Array.prototype.slice.call(arguments, 1);
var modelInstance = this.modelInstance;
var rel = modelInstance[this.definition.name];
if (rel && typeof rel[methodName] === 'function') {
return rel[methodName].apply(rel, args);
} else {
throw new Error('Unknown scope method: ' + methodName);
}
};
/**
* Fetch the related model(s) - this is a helper method to unify access.
* @param (Boolean|Object} condOrRefresh refresh or conditions object
* @param {Object} [options] Options
* @param {Function} cb callback
*/
Relation.prototype.fetch = function(condOrRefresh, options, cb) {
this.modelInstance[this.definition.name].apply(this.modelInstance, arguments);
};
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);
HasMany.prototype.removeFromCache = function(id) {
2014-06-21 18:44:33 +00:00
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 (idEquals(cache[i][idName], id)) {
2014-06-21 18:44:33 +00:00
return cache.splice(i, 1);
}
}
}
return null;
};
HasMany.prototype.addToCache = function(inst) {
2014-06-21 18:44:33 +00:00
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 (idEquals(cache[i][idName], inst[idName])) {
2014-06-21 18:44:33 +00:00
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);
2014-08-19 20:10:35 +00:00
/**
* EmbedsOne subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
2014-09-04 13:37:48 +00:00
* @returns {EmbedsOne}
2014-08-19 20:10:35 +00:00
* @constructor
* @class EmbedsOne
*/
function EmbedsOne(definition, modelInstance) {
if (!(this instanceof EmbedsOne)) {
2014-09-04 13:37:48 +00:00
return new EmbedsOne(definition, modelInstance);
2014-08-19 20:10:35 +00:00
}
assert(definition.type === RelationTypes.embedsOne);
Relation.apply(this, arguments);
}
util.inherits(EmbedsOne, Relation);
2014-07-27 14:30:45 +00:00
/**
* EmbedsMany subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {EmbedsMany}
* @constructor
* @class EmbedsMany
*/
function EmbedsMany(definition, modelInstance) {
if (!(this instanceof EmbedsMany)) {
return new EmbedsMany(definition, modelInstance);
}
assert(definition.type === RelationTypes.embedsMany);
Relation.apply(this, arguments);
}
util.inherits(EmbedsMany, Relation);
2014-06-15 22:53:58 +00:00
/**
* ReferencesMany subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
2014-07-29 19:46:12 +00:00
* @returns {ReferencesMany}
* @constructor
2014-07-29 19:46:12 +00:00
* @class ReferencesMany
*/
function ReferencesMany(definition, modelInstance) {
2014-07-29 19:46:12 +00:00
if (!(this instanceof ReferencesMany)) {
return new ReferencesMany(definition, modelInstance);
}
2014-07-29 19:46:12 +00:00
assert(definition.type === RelationTypes.referencesMany);
Relation.apply(this, arguments);
}
util.inherits(ReferencesMany, Relation);
2014-06-15 22:53:58 +00:00
/*!
* Find the relation by foreign key
* @param {*} foreignKey The foreign key
* @returns {Array} The array of matching relation objects
2014-06-15 22:53:58 +00:00
*/
2014-06-16 07:36:12 +00:00
function findBelongsTo(modelFrom, modelTo, keyTo) {
return Object.keys(modelFrom.relations)
.map(function(k) { return modelFrom.relations[k]; })
.filter(function(rel) {
return (rel.type === RelationTypes.belongsTo &&
rel.modelTo === modelTo &&
(keyTo === undefined || rel.keyTo === keyTo));
})
.map(function(rel) {
return rel.keyFrom;
});
2014-06-15 22:53:58 +00:00
}
/*!
* 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]) {
2014-06-15 22:53:58 +00:00
return models[modelName];
}
var lookupClassName = modelName.toLowerCase();
for (var name in models) {
if (name.toLowerCase() === lookupClassName) {
return models[name];
}
}
}
function lookupModelTo(modelFrom, modelTo, params, singularize) {
if ('string' === typeof modelTo) {
var modelToName;
params.as = params.as || modelTo;
modelTo = params.model || modelTo;
if (typeof modelTo === 'string') {
modelToName = (singularize ? i8n.singularize(modelTo) : modelTo).toLowerCase();
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName) || modelTo;
}
if (typeof modelTo === 'string') {
modelToName = (singularize ? i8n.singularize(params.as) : params.as).toLowerCase();
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName) || modelTo;
}
if (typeof modelTo !== 'function') {
throw new Error('Could not find "' + params.as + '" relation for ' + modelFrom.modelName);
}
}
return modelTo;
}
/*!
* Normalize polymorphic parameters
* @param {Object|String} params Name of the polymorphic relation or params
* @returns {Object} The normalized parameters
*/
function polymorphicParams(params, as) {
if (typeof params === 'string') params = { as: params };
if (typeof params.as !== 'string') params.as = as || 'reference'; // default
params.foreignKey = params.foreignKey || i8n.camelize(params.as + '_id', true);
params.discriminator = params.discriminator || i8n.camelize(params.as + '_type', true);
return params;
}
2014-06-15 22:53:58 +00:00
/**
* Define a "one to many" relationship by specifying the model name
*
2014-06-15 22:53:58 +00:00
* Examples:
* ```
* User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});
* ```
*
2014-06-15 22:53:58 +00:00
* ```
* 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 || {};
modelTo = lookupModelTo(modelFrom, modelTo, params, true);
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 keyThrough = params.keyThrough || i8n.camelize(modelTo.modelName + '_id', true);
2014-06-15 22:53:58 +00:00
var pkName = params.primaryKey || modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
var discriminator, polymorphic;
if (params.polymorphic) {
polymorphic = polymorphicParams(params.polymorphic);
if (params.invert) {
polymorphic.invert = true;
keyThrough = polymorphic.foreignKey;
}
discriminator = polymorphic.discriminator;
if (!params.invert) {
fk = polymorphic.foreignKey;
}
2014-07-26 10:47:55 +00:00
if (!params.through) {
2014-07-28 08:18:42 +00:00
modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, { 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: pkName,
2014-06-15 22:53:58 +00:00
keyTo: fk,
modelTo: modelTo,
multiple: true,
2014-07-11 22:02:16 +00:00
properties: params.properties,
scope: params.scope,
options: params.options,
keyThrough: keyThrough,
polymorphic: polymorphic,
2014-06-15 22:53:58 +00:00
});
definition.modelThrough = params.through;
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, pkName);
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'),
2014-07-15 15:50:34 +00:00
};
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
2014-07-28 09:36:32 +00:00
var existsByIdFunc = scopeMethods.exists;
modelFrom.prototype['__exists__' + relationName] = existsByIdFunc;
if (definition.modelThrough) {
2014-06-15 22:53:58 +00:00
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
}
var customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
for (var i = 0; i < customMethods.length; i++) {
var methodName = customMethods[i];
var method = scopeMethods[methodName];
if (typeof method === 'function' && method.shared === true) {
modelFrom.prototype['__' + methodName + '__' + relationName] = method;
}
};
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() {
2014-06-15 22:53:58 +00:00
var filter = {};
filter.where = {};
filter.where[fk] = this[pkName];
definition.applyScope(this, filter);
if (definition.modelThrough) {
var throughRelationName;
// find corresponding belongsTo relations from through model as collect
for (var r in definition.modelThrough.relations) {
var relation = definition.modelThrough.relations[r];
// should be a belongsTo and match modelTo and keyThrough
// if relation is polymorphic then check keyThrough only
if (relation.type === RelationTypes.belongsTo &&
(relation.polymorphic && !relation.modelTo || relation.modelTo === definition.modelTo) &&
(relation.keyFrom === definition.keyThrough)
) {
throughRelationName = relation.name;
break;
}
}
if (definition.polymorphic && definition.polymorphic.invert) {
filter.collect = definition.polymorphic.as;
filter.include = filter.collect;
} else {
filter.collect = throughRelationName || i8n.camelize(modelTo.modelName, true);
filter.include = filter.collect;
}
2014-06-15 22:53:58 +00:00
}
2014-06-15 22:53:58 +00:00
return filter;
2014-08-08 16:25:49 +00:00
}, scopeMethods, definition.options);
return definition;
2014-06-15 22:53:58 +00:00
};
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;
}
var method = function() {
2014-06-15 22:53:58 +00:00
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) {
sharedMethod(definition, methodName, method, relationMethod);
2014-07-15 15:50:34 +00:00
}
2014-06-15 22:53:58 +00:00
return method;
}
function sharedMethod(definition, methodName, method, relationMethod) {
method.shared = true;
method.accepts = relationMethod.accepts;
method.returns = relationMethod.returns;
method.http = relationMethod.http;
method.description = relationMethod.description;
}
2014-07-15 15:50:34 +00:00
/**
* Find a related item by foreign key
* @param {*} fkId The foreign key
* @param {Object} [options] Options
2014-07-15 15:50:34 +00:00
* @param {Function} cb The callback function
*/
HasMany.prototype.findById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
2014-06-15 22:53:58 +00:00
var modelTo = this.definition.modelTo;
2014-07-29 19:46:12 +00:00
var modelFrom = this.definition.modelFrom;
2014-06-15 22:53:58 +00:00
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];
cb = cb || utils.createPromiseCallback();
if (filter.where[fk] === undefined) {
// Foreign key is undefined
process.nextTick(cb);
return cb.promise;
}
this.definition.applyScope(modelInstance, filter);
modelTo.findOne(filter, options, 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] != null && idEquals(inst[fk], modelInstance[pk])) {
2014-06-15 22:53:58 +00:00
cb(null, inst);
} else {
2016-05-10 21:25:33 +00:00
err = new Error('Key mismatch: ' + modelFrom.modelName + '.' + pk + ': ' +
modelInstance[pk] + ', ' + modelTo.modelName + '.' + fk + ': ' +
inst[fk]);
2014-07-15 15:50:34 +00:00
err.statusCode = 400;
cb(err);
2014-06-15 22:53:58 +00:00
}
});
return cb.promise;
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 {Object} [options] Options
2014-07-15 15:50:34 +00:00
* @param {Function} cb The callback function
*/
HasMany.prototype.exists = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
2014-06-15 22:53:58 +00:00
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
cb = cb || utils.createPromiseCallback();
this.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);
}
});
return cb.promise;
2014-07-15 15:50:34 +00:00
};
/**
* Update a related item by foreign key
* @param {*} fkId The foreign key
* @param {Object} Changes to the data
* @param {Object} [options] Options
2014-07-15 15:50:34 +00:00
* @param {Function} cb The callback function
*/
HasMany.prototype.updateById = function(fkId, data, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
this.findById(fkId, options, function(err, inst) {
2014-07-15 15:50:34 +00:00
if (err) {
return cb && cb(err);
}
inst.updateAttributes(data, options, cb);
2014-07-15 15:50:34 +00:00
});
return cb.promise;
2014-07-15 15:50:34 +00:00
};
/**
* Delete a related item by foreign key
* @param {*} fkId The foreign key
* @param {Object} [options] Options
2014-07-15 15:50:34 +00:00
* @param {Function} cb The callback function
*/
HasMany.prototype.destroyById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
2014-06-21 18:44:33 +00:00
var self = this;
this.findById(fkId, options, function(err, inst) {
2014-06-15 22:53:58 +00:00
if (err) {
return cb(err);
}
2015-05-22 20:42:34 +00:00
self.removeFromCache(fkId);
inst.destroy(options, cb);
2014-06-15 22:53:58 +00:00
});
return cb.promise;
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 (typeof definition.polymorphic === 'object') { // polymorphic
2014-07-26 15:20:25 +00:00
var fk1 = definition.keyTo;
if (definition.polymorphic.invert) {
var fk2 = definition.polymorphic.foreignKey;
} else {
var fk2 = definition.keyThrough;
}
} else if (definition.modelFrom === definition.modelTo) {
return findBelongsTo(modelThrough, definition.modelTo, pk2).
sort(function(fk1, fk2) {
//Fix for bug - https://github.com/strongloop/loopback-datasource-juggler/issues/571
//Make sure that first key is mapped to modelFrom
//& second key to modelTo. Order matters
return (definition.keyTo === fk1) ? -1 : 1;
});
2014-07-26 15:20:25 +00:00
} else {
var fk1 = findBelongsTo(modelThrough, definition.modelFrom,
definition.keyFrom)[0];
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2)[0];
2014-07-26 15:20:25 +00:00
}
return [fk1, fk2];
};
2014-07-26 15:20:25 +00:00
2014-07-15 15:50:34 +00:00
/**
* Find a related item by foreign key
* @param {*} fkId The foreign key value
* @param {Object} [options] Options
2014-07-15 15:50:34 +00:00
* @param {Function} cb The callback function
*/
HasManyThrough.prototype.findById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
2014-07-15 15:50:34 +00:00
var self = this;
var modelTo = this.definition.modelTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var modelThrough = this.definition.modelThrough;
cb = cb || utils.createPromiseCallback();
self.exists(fkId, options, function(err, exists) {
2014-07-15 15:50:34 +00:00
if (err || !exists) {
if (!err) {
2016-05-10 21:25:33 +00:00
err = new Error('No relation found in ' + modelThrough.modelName +
' for (' + self.definition.modelFrom.modelName + '.' +
modelInstance[pk] + ',' + modelTo.modelName + '.' + fkId + ')');
2014-07-15 15:50:34 +00:00
err.statusCode = 404;
}
return cb(err);
}
modelTo.findById(fkId, options, function(err, inst) {
2014-07-15 15:50:34 +00:00
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);
});
});
return cb.promise;
2014-07-15 15:50:34 +00:00
};
/**
* Delete a related item by foreign key
* @param {*} fkId The foreign key
* @param {Object} [options] Options
2014-07-15 15:50:34 +00:00
* @param {Function} cb The callback function
*/
HasManyThrough.prototype.destroyById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
2014-07-15 15:50:34 +00:00
var self = this;
var modelTo = this.definition.modelTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var modelThrough = this.definition.modelThrough;
cb = cb || utils.createPromiseCallback();
self.exists(fkId, options, function(err, exists) {
2014-07-15 15:50:34 +00:00
if (err || !exists) {
if (!err) {
2016-05-10 21:25:33 +00:00
err = new Error('No record found in ' + modelThrough.modelName +
' for (' + self.definition.modelFrom.modelName + '.' +
modelInstance[pk] + ' ,' + modelTo.modelName + '.' + fkId + ')');
2014-07-15 15:50:34 +00:00
err.statusCode = 404;
}
return cb(err);
2014-06-15 22:53:58 +00:00
}
self.remove(fkId, options, function(err) {
if (err) {
2014-07-15 15:50:34 +00:00
return cb(err);
}
modelTo.deleteById(fkId, options, cb);
2014-07-15 15:50:34 +00:00
});
2014-06-15 22:53:58 +00:00
});
return cb.promise;
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, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
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;
if (typeof data === 'function' && !cb) {
cb = data;
2014-06-15 22:53:58 +00:00
data = {};
}
cb = cb || utils.createPromiseCallback();
2014-06-15 22:53:58 +00:00
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
modelTo.create(data, options, function(err, to) {
2014-06-15 22:53:58 +00:00
if (err) {
return cb(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];
function createRelation(to, next) {
2016-05-10 21:25:33 +00:00
var d = {}, q = {}, filter = { where: q };
d[fk1] = q[fk1] = modelInstance[definition.keyFrom];
d[fk2] = q[fk2] = to[pk2];
definition.applyProperties(modelInstance, d);
definition.applyScope(modelInstance, filter);
// Then create the through model
modelThrough.findOrCreate(filter, d, options, function(e, through) {
if (e) {
// Undo creation of the target model
to.destroy(options, function() {
next(e);
});
} else {
self.addToCache(to);
next(err, to);
}
});
}
// process array or single item
if (!Array.isArray(to))
createRelation(to, cb);
else
async.map(to, createRelation, cb);
2014-06-15 22:53:58 +00:00
});
return cb.promise;
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
* @param {Object} [data] Optional data object for the through model to be created
* @param {Object} [options] Options
* @param {Function} [cb] Callback function
2014-06-15 22:53:58 +00:00
*/
HasManyThrough.prototype.add = function(acInst, data, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
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
if (typeof data === 'function') {
cb = data;
data = {};
}
2014-06-15 22:53:58 +00:00
var query = {};
data = data || {};
cb = cb || utils.createPromiseCallback();
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-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, options, function(err, ac) {
if (!err) {
2014-06-21 18:44:33 +00:00
if (acInst instanceof definition.modelTo) {
self.addToCache(acInst);
}
}
cb(err, ac);
2014-06-21 18:44:33 +00:00
});
return cb.promise;
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, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
2014-07-15 15:50:34 +00:00
var definition = this.definition;
var modelThrough = definition.modelThrough;
var pk1 = definition.keyFrom;
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 15:50:34 +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);
cb = cb || utils.createPromiseCallback();
modelThrough.count(filter.where, options, function(err, ac) {
cb(err, ac > 0);
2014-07-15 15:50:34 +00:00
});
return cb.promise;
2014-07-15 15:50:34 +00:00
};
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, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
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);
cb = cb || utils.createPromiseCallback();
modelThrough.deleteAll(filter.where, options, function(err) {
2014-06-21 18:44:33 +00:00
if (!err) {
self.removeFromCache(query[fk2]);
}
cb(err);
2014-06-21 18:44:33 +00:00
});
return cb.promise;
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.
*
2014-06-15 22:53:58 +00:00
* @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.
*
2014-06-15 22:53:58 +00:00
*/
RelationDefinition.belongsTo = function(modelFrom, modelTo, params) {
var discriminator, polymorphic;
2014-06-15 22:53:58 +00:00
params = params || {};
2014-07-26 10:47:55 +00:00
if ('string' === typeof modelTo && !params.polymorphic) {
modelTo = lookupModelTo(modelFrom, modelTo, params);
2014-06-15 22:53:58 +00:00
}
var pkName, relationName, fk;
2014-07-26 10:47:55 +00:00
if (params.polymorphic) {
relationName = params.as || (typeof modelTo === 'string' ? modelTo : null); // initially
if (params.polymorphic === true) {
// modelTo arg will be the name of the polymorphic relation (string)
polymorphic = polymorphicParams(modelTo, relationName);
} else {
polymorphic = polymorphicParams(params.polymorphic, relationName);
}
2014-07-26 10:47:55 +00:00
modelTo = null; // will lookup dynamically
pkName = params.primaryKey || params.idName || 'id';
relationName = params.as || polymorphic.as; // finally
fk = polymorphic.foreignKey;
discriminator = polymorphic.discriminator;
if (polymorphic.idType) { // explicit key type
modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, { type: polymorphic.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, pkName);
2014-07-26 10:47:55 +00:00
}
2014-07-28 08:18:42 +00:00
modelFrom.dataSource.defineProperty(modelFrom.modelName, discriminator, { type: 'string', index: true });
2014-07-26 10:47:55 +00:00
} else {
pkName = params.primaryKey || modelTo.dataSource.idName(modelTo.modelName) || 'id';
relationName = params.as || i8n.camelize(modelTo.modelName, true);
fk = params.foreignKey || relationName + 'Id';
modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelTo.modelName, pkName);
2014-07-26 10:47:55 +00:00
}
var definition = modelFrom.relations[relationName] = new RelationDefinition({
2014-06-15 22:53:58 +00:00
name: relationName,
type: RelationTypes.belongsTo,
modelFrom: modelFrom,
keyFrom: fk,
keyTo: pkName,
modelTo: modelTo,
multiple: false,
properties: params.properties,
scope: params.scope,
options: params.options,
polymorphic: polymorphic,
methods: params.methods,
2014-06-15 22:53:58 +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(definition, this);
2014-06-16 20:46:17 +00:00
var relationMethod = relation.related.bind(relation);
relationMethod.getAsync = relation.getAsync.bind(relation);
relationMethod.update = relation.update.bind(relation);
relationMethod.destroy = relation.destroy.bind(relation);
if (!polymorphic) {
relationMethod.create = relation.create.bind(relation);
relationMethod.build = relation.build.bind(relation);
relationMethod._targetClass = definition.modelTo.modelName;
2014-07-26 10:47:55 +00:00
}
bindRelationMethods(relation, relationMethod, definition);
2014-06-15 22:53:58 +00:00
return relationMethod;
},
2014-06-15 22:53:58 +00:00
});
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;
return definition;
2014-06-15 22:53:58 +00:00
};
BelongsTo.prototype.create = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
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 = {};
}
cb = cb || utils.createPromiseCallback();
this.definition.applyProperties(modelInstance, targetModelData || {});
modelTo.create(targetModelData, options, function(err, targetModel) {
if (!err) {
2014-06-15 22:53:58 +00:00
modelInstance[fk] = targetModel[pk];
if (modelInstance.isNewRecord()) {
self.resetCache(targetModel);
cb && cb(err, targetModel);
} else {
modelInstance.save(options, function(err, inst) {
if (cb && err) return cb && cb(err);
self.resetCache(targetModel);
cb && cb(err, targetModel);
});
}
2014-06-15 22:53:58 +00:00
} else {
cb && cb(err);
}
});
return cb.promise;
2014-06-15 22:53:58 +00:00
};
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);
};
BelongsTo.prototype.update = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
var definition = this.definition;
this.fetch(options, function(err, inst) {
if (inst instanceof ModelBaseClass) {
inst.updateAttributes(targetModelData, options, cb);
} else {
2016-05-10 21:25:33 +00:00
cb(new Error('BelongsTo relation ' + definition.name + ' is empty'));
}
});
return cb.promise;
};
BelongsTo.prototype.destroy = function(options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var fk = this.definition.keyFrom;
cb = cb || utils.createPromiseCallback();
this.fetch(options, function(err, targetModel) {
if (targetModel instanceof ModelBaseClass) {
modelInstance[fk] = null;
modelInstance.save(options, function(err, targetModel) {
if (cb && err) return cb && cb(err);
cb && cb(err, targetModel);
});
} else {
2016-05-10 21:25:33 +00:00
cb(new Error('BelongsTo relation ' + definition.name + ' is empty'));
}
});
return cb.promise;
};
2014-06-15 22:53:58 +00:00
/**
* Define the method for the belongsTo relation itself
* It will support one of the following styles:
* - order.customer(refresh, options, callback): Load the target model instance asynchronously
2014-06-15 22:53:58 +00:00
* - 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(condOrRefresh, options, cb) {
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;
var pk = this.definition.keyTo;
var fk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var discriminator;
2014-10-14 20:47:59 +00:00
var scopeQuery = null;
var newValue;
if ((condOrRefresh instanceof ModelBaseClass) &&
options === undefined && cb === undefined) {
// order.customer(customer)
newValue = condOrRefresh;
condOrRefresh = false;
} else if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
// order.customer(cb)
cb = condOrRefresh;
condOrRefresh = false;
} else if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// order.customer(condOrRefresh, cb)
cb = options;
options = {};
}
if (!newValue) {
scopeQuery = condOrRefresh;
2014-06-15 22:53:58 +00:00
}
if (typeof this.definition.polymorphic === 'object') {
discriminator = this.definition.polymorphic.discriminator;
}
2014-06-15 22:53:58 +00:00
var cachedValue;
if (!condOrRefresh) {
2014-06-21 18:44:33 +00:00
cachedValue = self.getCache();
2014-06-15 22:53:58 +00:00
}
if (newValue) { // acts as setter
modelInstance[fk] = newValue[pk];
if (discriminator) {
modelInstance[discriminator] = newValue.constructor.modelName;
}
this.definition.applyProperties(modelInstance, newValue);
self.resetCache(newValue);
} else if (typeof cb === 'function') { // acts as async getter
if (discriminator) {
2014-07-28 08:18:42 +00:00
var modelToName = modelInstance[discriminator];
2014-07-26 10:47:55 +00:00
if (typeof modelToName !== 'string') {
2014-07-28 08:18:42 +00:00
throw new Error('Polymorphic model not found: `' + discriminator + '` not set');
2014-07-26 10:47:55 +00:00
}
modelToName = modelToName.toLowerCase();
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
if (!modelTo) {
throw new Error('Polymorphic model not found: `' + modelToName + '`');
}
}
if (cachedValue === undefined || !(cachedValue instanceof ModelBaseClass)) {
var query = { where: {}};
query.where[pk] = modelInstance[fk];
2016-05-10 21:25:33 +00:00
if (query.where[pk] === undefined || query.where[pk] === null) {
// Foreign key is undefined
return process.nextTick(cb);
}
this.definition.applyScope(modelInstance, query);
2014-10-14 20:47:59 +00:00
if (scopeQuery) mergeQuery(query, scopeQuery);
2014-10-14 20:47:59 +00:00
if (Array.isArray(query.fields) && query.fields.indexOf(pk) === -1) {
query.fields.push(pk); // always include the pk
2014-10-14 20:47:59 +00:00
}
modelTo.findOne(query, options, 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
2016-05-10 21:25:33 +00:00
if (inst[pk] != null && modelInstance[fk] != null &&
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 {
2016-05-10 21:25:33 +00:00
err = new Error(
'Key mismatch: ' + self.definition.modelFrom.modelName + '.' + fk +
': ' + modelInstance[fk] +
', ' + modelTo.modelName + '.' + pk + ': ' + inst[pk]);
2014-07-15 15:50:34 +00:00
err.statusCode = 400;
cb(err);
2014-06-15 22:53:58 +00:00
}
});
return modelInstance[fk];
} else {
cb(null, cachedValue);
return cachedValue;
}
} else if (condOrRefresh === undefined) { // acts as sync getter
return cachedValue;
2014-06-15 22:53:58 +00:00
} else { // setter
modelInstance[fk] = newValue;
2014-06-21 18:44:33 +00:00
self.resetCache();
2014-06-15 22:53:58 +00:00
}
};
/**
* Define a Promise-based method for the belongsTo relation itself
* - order.customer.get(cb): Load the target model instance asynchronously
*
* @param {Function} cb Callback of the form function (err, inst)
* @returns {Promise | Undefined} returns promise if callback is omitted
*/
BelongsTo.prototype.getAsync = function(options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// order.customer.getAsync(cb)
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
this.related(true, options, cb);
return cb.promise;
};
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 || {};
modelTo = lookupModelTo(modelFrom, modelTo, params, true);
2014-06-15 22:53:58 +00:00
var models = modelFrom.dataSource.modelBuilder.models;
2014-06-15 22:53:58 +00:00
if (!params.through) {
if (params.polymorphic) 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 };
options.properties = params.properties;
options.scope = params.scope;
2016-06-03 09:18:57 +00:00
// Forward relation options like "disableInclude"
options.options = params.options;
if (params.polymorphic) {
var polymorphic = polymorphicParams(params.polymorphic);
options.polymorphic = polymorphic; // pass through
var accessor = params.through.prototype[polymorphic.as];
if (typeof accessor !== 'function') { // declare once
// use the name of the polymorphic rel, not modelTo
params.through.belongsTo(polymorphic.as, { polymorphic: true });
}
} else {
params.through.belongsTo(modelFrom);
}
2014-06-15 22:53:58 +00:00
params.through.belongsTo(modelTo);
return 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) {
2014-06-16 08:17:37 +00:00
params = params || {};
modelTo = lookupModelTo(modelFrom, modelTo, params);
2014-06-16 08:17:37 +00:00
2015-08-11 20:30:15 +00:00
var pk = params.primaryKey || modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
2014-06-16 08:17:37 +00:00
var relationName = params.as || i8n.camelize(modelTo.modelName, true);
var fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true);
var discriminator, polymorphic;
if (params.polymorphic) {
polymorphic = polymorphicParams(params.polymorphic);
fk = polymorphic.foreignKey;
discriminator = polymorphic.discriminator;
if (!params.through) {
2014-07-28 08:18:42 +00:00
modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, { type: 'string', index: true });
}
}
var definition = modelFrom.relations[relationName] = new RelationDefinition({
2014-06-16 08:17:37 +00:00
name: relationName,
type: RelationTypes.hasOne,
modelFrom: modelFrom,
keyFrom: pk,
keyTo: fk,
modelTo: modelTo,
multiple: false,
properties: params.properties,
scope: params.scope,
options: params.options,
polymorphic: polymorphic,
methods: params.methods,
2014-06-16 08:17:37 +00:00
});
2015-08-11 20:30:15 +00:00
modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName, pk);
2014-06-16 08:17:37 +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 HasOne(definition, this);
var relationMethod = relation.related.bind(relation);
relationMethod.getAsync = relation.getAsync.bind(relation);
2014-06-16 08:17:37 +00:00
relationMethod.create = relation.create.bind(relation);
relationMethod.build = relation.build.bind(relation);
2014-08-20 13:54:47 +00:00
relationMethod.update = relation.update.bind(relation);
relationMethod.destroy = relation.destroy.bind(relation);
relationMethod._targetClass = definition.modelTo.modelName;
bindRelationMethods(relation, relationMethod, definition);
2014-06-16 08:17:37 +00:00
return relationMethod;
},
2014-06-16 08:17:37 +00:00
});
// FIXME: [rfeng] Wrap the property into a function for remoting
// so that it can be accessed as /api/<model>/<id>/<hasOneRelationName>
// For example, /api/orders/1/customer
2015-01-30 07:26:11 +00:00
modelFrom.prototype['__get__' + relationName] = function() {
var f = this[relationName];
f.apply(this, arguments);
};
2015-01-30 07:26:11 +00:00
modelFrom.prototype['__create__' + relationName] = function() {
var f = this[relationName].create;
f.apply(this, arguments);
};
modelFrom.prototype['__update__' + relationName] = function() {
var f = this[relationName].update;
f.apply(this, arguments);
};
modelFrom.prototype['__destroy__' + relationName] = function() {
var f = this[relationName].destroy;
f.apply(this, arguments);
};
return definition;
2014-06-16 08:17:37 +00:00
};
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, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.profile.create(options, cb)
cb = options;
options = {};
}
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 || {};
cb = cb || utils.createPromiseCallback();
2014-06-16 08:17:37 +00:00
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.findOrCreate(query, targetModelData, options,
function(err, targetModel, created) {
if (err) {
return cb && cb(err);
}
if (created) {
// Refresh the cache
self.resetCache(targetModel);
cb && cb(err, targetModel);
} else {
2016-05-10 21:25:33 +00:00
cb && cb(new Error(
'HasOne relation cannot create more than one instance of ' +
modelTo.modelName));
}
});
return cb.promise;
};
HasOne.prototype.update = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.profile.update(data, cb)
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
2014-08-20 13:54:47 +00:00
var definition = this.definition;
2015-01-30 07:26:11 +00:00
var fk = this.definition.keyTo;
this.fetch(function(err, targetModel) {
if (targetModel instanceof ModelBaseClass) {
2015-01-30 07:26:11 +00:00
delete targetModelData[fk];
targetModel.updateAttributes(targetModelData, cb);
2014-08-20 13:54:47 +00:00
} else {
2016-05-10 21:25:33 +00:00
cb(new Error('HasOne relation ' + definition.name + ' is empty'));
2014-08-20 13:54:47 +00:00
}
});
return cb.promise;
2014-08-20 13:54:47 +00:00
};
HasOne.prototype.destroy = function(options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.profile.destroy(cb)
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
var definition = this.definition;
this.fetch(function(err, targetModel) {
if (targetModel instanceof ModelBaseClass) {
targetModel.destroy(options, cb);
2014-08-20 13:58:45 +00:00
} else {
2016-05-10 21:25:33 +00:00
cb(new Error('HasOne relation ' + definition.name + ' is empty'));
2014-08-20 13:58:45 +00:00
}
});
return cb.promise;
2014-08-20 13:54:47 +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
*/
HasMany.prototype.create = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.orders.create(data, cb)
cb = options;
options = {};
}
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 || {};
cb = cb || utils.createPromiseCallback();
var fkAndProps = function(item) {
item[fk] = modelInstance[pk];
self.definition.applyProperties(modelInstance, item);
};
var apply = function(data, fn) {
if (Array.isArray(data)) {
data.forEach(fn);
} else {
fn(data);
}
};
apply(targetModelData, fkAndProps);
modelTo.create(targetModelData, options, function(err, targetModel) {
if (!err) {
//Refresh the cache
apply(targetModel, self.addToCache.bind(self));
2014-06-16 08:17:37 +00:00
cb && cb(err, targetModel);
} else {
cb && cb(err);
}
});
return cb.promise;
2014-06-16 08:17:37 +00:00
};
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(condOrRefresh, options, 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 definition = this.definition;
2014-06-16 08:17:37 +00:00
var modelInstance = this.modelInstance;
var newValue;
if ((condOrRefresh instanceof ModelBaseClass) &&
options === undefined && cb === undefined) {
// order.customer(customer)
newValue = condOrRefresh;
condOrRefresh = false;
} else if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.profile(cb)
cb = condOrRefresh;
condOrRefresh = false;
} else if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.profile(condOrRefresh, cb)
cb = options;
options = {};
2014-06-16 08:17:37 +00:00
}
var cachedValue;
if (!condOrRefresh) {
2014-06-21 18:44:33 +00:00
cachedValue = self.getCache();
2014-06-16 08:17:37 +00:00
}
if (newValue) { // acts as setter
newValue[fk] = modelInstance[pk];
self.resetCache(newValue);
} else if (typeof cb === 'function') { // acts as async getter
2014-06-16 08:17:37 +00:00
if (cachedValue === undefined) {
var query = { where: {}};
2014-06-16 08:17:37 +00:00
query.where[fk] = modelInstance[pk];
definition.applyScope(modelInstance, query);
modelTo.findOne(query, options, function(err, inst) {
2014-06-16 08:17:37 +00:00
if (err) {
return cb(err);
}
if (!inst) {
return cb(null, null);
}
// Check if the foreign key matches the primary key
2016-05-10 21:25:33 +00:00
if (inst[fk] != null && modelInstance[pk] != null &&
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 {
2016-05-10 21:25:33 +00:00
err = new Error(
'Key mismatch: ' + self.definition.modelFrom.modelName + '.' + pk +
': ' + modelInstance[pk] +
', ' + modelTo.modelName + '.' + fk + ': ' + inst[fk]
);
2014-07-15 15:50:34 +00:00
err.statusCode = 400;
cb(err);
2014-06-16 08:17:37 +00:00
}
});
return modelInstance[pk];
} else {
cb(null, cachedValue);
return cachedValue;
}
} else if (condOrRefresh === undefined) { // acts as sync getter
return cachedValue;
2014-06-16 08:17:37 +00:00
} else { // setter
newValue[fk] = modelInstance[pk];
2014-06-21 18:44:33 +00:00
self.resetCache();
2014-06-16 08:17:37 +00:00
}
};
2014-07-27 14:30:45 +00:00
/**
* Define a Promise-based method for the hasOne relation itself
* - order.customer.getAsync(cb): Load the target model instance asynchronously
*
* @param {Function} cb Callback of the form function (err, inst)
* @returns {Promise | Undefined} Returns promise if cb is omitted
*/
HasOne.prototype.getAsync = function(options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// order.profile.getAsync(cb)
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
this.related(true, cb);
return cb.promise;
};
RelationDefinition.embedsOne = function(modelFrom, modelTo, params) {
2014-08-19 20:10:35 +00:00
params = params || {};
modelTo = lookupModelTo(modelFrom, modelTo, params);
var thisClassName = modelFrom.modelName;
var relationName = params.as || (i8n.camelize(modelTo.modelName, true) + 'Item');
var propertyName = params.property || i8n.camelize(modelTo.modelName, true);
var idName = modelTo.dataSource.idName(modelTo.modelName) || 'id';
2014-08-19 20:10:35 +00:00
if (relationName === propertyName) {
propertyName = '_' + propertyName;
debug('EmbedsOne property cannot be equal to relation name: ' +
'forcing property %s for relation %s', propertyName, relationName);
}
2014-08-19 20:10:35 +00:00
var definition = modelFrom.relations[relationName] = new RelationDefinition({
name: relationName,
type: RelationTypes.embedsOne,
modelFrom: modelFrom,
keyFrom: propertyName,
keyTo: idName,
modelTo: modelTo,
multiple: false,
properties: params.properties,
scope: params.scope,
options: params.options,
embed: true,
methods: params.methods,
2014-08-19 20:10:35 +00:00
});
2014-08-19 20:10:35 +00:00
var opts = { type: modelTo };
2014-08-19 20:10:35 +00:00
if (params.default === true) {
opts.default = function() { return new modelTo(); };
} else if (typeof params.default === 'object') {
opts.default = (function(def) {
return function() {
return new modelTo(def);
};
2014-08-19 20:10:35 +00:00
}(params.default));
}
2014-08-19 20:10:35 +00:00
modelFrom.dataSource.defineProperty(modelFrom.modelName, propertyName, opts);
2014-08-19 20:10:35 +00:00
// validate the embedded instance
2014-08-19 20:15:30 +00:00
if (definition.options.validate !== false) {
2014-08-19 20:10:35 +00:00
modelFrom.validate(relationName, function(err) {
var inst = this[propertyName];
if (inst instanceof modelTo) {
if (!inst.isValid()) {
var first = Object.keys(inst.errors)[0];
var msg = 'is invalid: `' + first + '` ' + inst.errors[first];
this.errors.add(relationName, msg, 'invalid');
err(false);
}
}
});
}
2014-08-19 20:10:35 +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 EmbedsOne(definition, this);
var relationMethod = relation.related.bind(relation);
2014-08-19 20:10:35 +00:00
relationMethod.create = relation.create.bind(relation);
relationMethod.build = relation.build.bind(relation);
2014-08-20 13:37:40 +00:00
relationMethod.update = relation.update.bind(relation);
2014-08-19 20:10:35 +00:00
relationMethod.destroy = relation.destroy.bind(relation);
relationMethod.value = relation.embeddedValue.bind(relation);
2014-08-19 20:10:35 +00:00
relationMethod._targetClass = definition.modelTo.modelName;
bindRelationMethods(relation, relationMethod, definition);
2014-08-19 20:10:35 +00:00
return relationMethod;
},
2014-08-19 20:10:35 +00:00
});
2014-08-19 20:10:35 +00:00
// FIXME: [rfeng] Wrap the property into a function for remoting
// so that it can be accessed as /api/<model>/<id>/<embedsOneRelationName>
// For example, /api/orders/1/customer
modelFrom.prototype['__get__' + relationName] = function() {
2014-08-19 20:10:35 +00:00
var f = this[relationName];
f.apply(this, arguments);
};
modelFrom.prototype['__create__' + relationName] = function() {
var f = this[relationName].create;
f.apply(this, arguments);
};
modelFrom.prototype['__update__' + relationName] = function() {
var f = this[relationName].update;
f.apply(this, arguments);
};
modelFrom.prototype['__destroy__' + relationName] = function() {
var f = this[relationName].destroy;
f.apply(this, arguments);
};
2014-08-19 20:10:35 +00:00
return definition;
};
EmbedsOne.prototype.related = function(condOrRefresh, options, cb) {
2014-08-19 20:10:35 +00:00
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var propertyName = this.definition.keyFrom;
var newValue;
if ((condOrRefresh instanceof ModelBaseClass) &&
options === undefined && cb === undefined) {
// order.customer(customer)
newValue = condOrRefresh;
condOrRefresh = false;
} else if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
// order.customer(cb)
cb = condOrRefresh;
condOrRefresh = false;
} else if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// order.customer(condOrRefresh, cb)
cb = options;
options = {};
2014-08-19 20:10:35 +00:00
}
if (newValue) { // acts as setter
if (newValue instanceof modelTo) {
this.definition.applyProperties(modelInstance, newValue);
modelInstance.setAttribute(propertyName, newValue);
2014-08-19 20:10:35 +00:00
}
return;
}
2014-08-19 20:10:35 +00:00
var embeddedInstance = modelInstance[propertyName];
if (embeddedInstance) {
embeddedInstance.__persisted = true;
}
if (typeof cb === 'function') { // acts as async getter
process.nextTick(function() {
2014-08-19 20:10:35 +00:00
cb(null, embeddedInstance);
});
} else if (condOrRefresh === undefined) { // acts as sync getter
2014-08-19 20:10:35 +00:00
return embeddedInstance;
}
};
EmbedsOne.prototype.embeddedValue = function(modelInstance) {
modelInstance = modelInstance || this.modelInstance;
return modelInstance[this.definition.keyFrom];
};
EmbedsOne.prototype.create = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// order.customer.create(data, cb)
cb = options;
options = {};
}
2014-08-19 20:10:35 +00:00
var modelTo = this.definition.modelTo;
var propertyName = this.definition.keyFrom;
var modelInstance = this.modelInstance;
2014-08-19 20:10:35 +00:00
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
2014-08-19 20:10:35 +00:00
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
var inst = this.callScopeMethod('build', targetModelData);
var updateEmbedded = function(callback) {
if (modelInstance.isNewRecord()) {
modelInstance.setAttribute(propertyName, inst);
modelInstance.save(options, function(err) {
callback(err, err ? null : inst);
});
} else {
modelInstance.updateAttribute(propertyName,
inst, options, function(err) {
callback(err, err ? null : inst);
});
}
};
if (this.definition.options.persistent) {
inst.save(options, function(err) { // will validate
if (err) return cb(err, inst);
updateEmbedded(cb);
2014-08-19 20:10:35 +00:00
});
} else {
var context = {
Model: modelTo,
instance: inst,
options: options || {},
hookState: {},
};
modelTo.notifyObserversOf('before save', context, function(err) {
if (err) {
return process.nextTick(function() {
cb(err);
});
}
var err = inst.isValid() ? null : new ValidationError(inst);
if (err) {
process.nextTick(function() {
cb(err);
});
} else {
updateEmbedded(function(err, inst) {
if (err) return cb(err);
context.instance = inst;
modelTo.notifyObserversOf('after save', context, function(err) {
cb(err, err ? null : inst);
});
});
}
});
2014-08-19 20:10:35 +00:00
}
return cb.promise;
2014-08-19 20:10:35 +00:00
};
EmbedsOne.prototype.build = function(targetModelData) {
2014-08-19 20:10:35 +00:00
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
2014-08-20 13:37:40 +00:00
var propertyName = this.definition.keyFrom;
var forceId = this.definition.options.forceId;
var persistent = this.definition.options.persistent;
var connector = modelTo.dataSource.connector;
2014-08-19 20:10:35 +00:00
targetModelData = targetModelData || {};
2014-08-19 20:10:35 +00:00
this.definition.applyProperties(modelInstance, targetModelData);
var pk = this.definition.keyTo;
var pkProp = modelTo.definition.properties[pk];
var assignId = (forceId || targetModelData[pk] === undefined);
assignId = assignId && !persistent && (pkProp && pkProp.generated);
if (assignId && typeof connector.generateId === 'function') {
var id = connector.generateId(modelTo.modelName, targetModelData, pk);
targetModelData[pk] = id;
}
2014-08-20 13:37:40 +00:00
var embeddedInstance = new modelTo(targetModelData);
modelInstance[propertyName] = embeddedInstance;
2014-08-20 13:37:40 +00:00
return embeddedInstance;
};
EmbedsOne.prototype.update = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// order.customer.update(data, cb)
cb = options;
options = {};
}
2014-08-20 13:37:40 +00:00
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var propertyName = this.definition.keyFrom;
2014-08-20 13:37:40 +00:00
var isInst = targetModelData instanceof ModelBaseClass;
var data = isInst ? targetModelData.toObject() : targetModelData;
2014-08-20 13:37:40 +00:00
var embeddedInstance = modelInstance[propertyName];
if (embeddedInstance instanceof modelTo) {
cb = cb || utils.createPromiseCallback();
var hookState = {};
var context = {
Model: modelTo,
currentInstance: embeddedInstance,
data: data,
options: options || {},
hookState: hookState,
};
modelTo.notifyObserversOf('before save', context, function(err) {
if (err) return cb(err);
embeddedInstance.setAttributes(context.data);
// TODO support async validations
if (!embeddedInstance.isValid()) {
return cb(new ValidationError(embeddedInstance));
}
modelInstance.save(function(err, inst) {
if (err) return cb(err);
context = {
Model: modelTo,
instance: inst ? inst[propertyName] : embeddedInstance,
options: options || {},
hookState: hookState,
};
modelTo.notifyObserversOf('after save', context, function(err) {
cb(err, context.instance);
});
2014-08-20 13:37:40 +00:00
});
});
2014-08-20 13:37:40 +00:00
} else if (!embeddedInstance && cb) {
return this.callScopeMethod('create', data, cb);
2014-08-20 13:37:40 +00:00
} else if (!embeddedInstance) {
return this.callScopeMethod('build', data);
2014-08-20 13:37:40 +00:00
}
return cb.promise;
2014-08-19 20:10:35 +00:00
};
EmbedsOne.prototype.destroy = function(options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// order.customer.destroy(cb)
cb = options;
options = {};
}
cb = cb || utils.createPromiseCallback();
var modelTo = this.definition.modelTo;
2014-08-19 20:10:35 +00:00
var modelInstance = this.modelInstance;
var propertyName = this.definition.keyFrom;
var embeddedInstance = modelInstance[propertyName];
if (!embeddedInstance) {
cb();
return cb.promise;
}
2014-08-19 20:10:35 +00:00
modelInstance.unsetAttribute(propertyName, true);
var context = {
Model: modelTo,
instance: embeddedInstance,
options: options || {},
hookState: {},
};
modelTo.notifyObserversOf('before delete', context, function(err) {
if (err) return cb(err);
modelInstance.save(function(err, result) {
if (err) return cb(err);
modelTo.notifyObserversOf('after delete', context, cb);
});
});
return cb.promise;
2014-08-19 20:10:35 +00:00
};
2014-07-29 13:01:47 +00:00
RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) {
2014-07-27 14:30:45 +00:00
params = params || {};
modelTo = lookupModelTo(modelFrom, modelTo, params, true);
var thisClassName = modelFrom.modelName;
var relationName = params.as || (i8n.camelize(modelTo.modelName, true) + 'List');
var propertyName = params.property || i8n.camelize(modelTo.pluralModelName, true);
var idName = modelTo.dataSource.idName(modelTo.modelName) || 'id';
if (relationName === propertyName) {
propertyName = '_' + propertyName;
debug('EmbedsMany property cannot be equal to relation name: ' +
'forcing property %s for relation %s', propertyName, relationName);
}
var definition = modelFrom.relations[relationName] = new RelationDefinition({
2014-07-27 14:30:45 +00:00
name: relationName,
type: RelationTypes.embedsMany,
modelFrom: modelFrom,
keyFrom: propertyName,
keyTo: idName,
2014-07-27 14:30:45 +00:00
modelTo: modelTo,
multiple: true,
properties: params.properties,
scope: params.scope,
options: params.options,
embed: true,
2014-07-27 14:30:45 +00:00
});
modelFrom.dataSource.defineProperty(modelFrom.modelName, propertyName, {
type: [modelTo], default: function() { return []; },
2014-07-27 14:30:45 +00:00
});
if (typeof modelTo.dataSource.connector.generateId !== 'function') {
modelFrom.validate(propertyName, function(err) {
var self = this;
var embeddedList = this[propertyName] || [];
var hasErrors = false;
embeddedList.forEach(function(item, idx) {
if (item instanceof modelTo && item[idName] == undefined) {
hasErrors = true;
var msg = 'contains invalid item at index `' + idx + '`:';
msg += ' `' + idName + '` is blank';
self.errors.add(propertyName, msg, 'invalid');
}
});
if (hasErrors) err(false);
});
}
2014-07-29 11:57:49 +00:00
if (!params.polymorphic) {
modelFrom.validate(propertyName, function(err) {
var embeddedList = this[propertyName] || [];
2015-08-17 17:48:26 +00:00
var ids = embeddedList.map(function(m) { return m[idName] && m[idName].toString(); }); // mongodb
2014-07-29 11:57:49 +00:00
var uniqueIds = ids.filter(function(id, pos) {
return utils.findIndexOf(ids, id, idEquals) === pos;
2014-07-29 11:57:49 +00:00
});
if (ids.length !== uniqueIds.length) {
this.errors.add(propertyName, 'contains duplicate `' + idName + '`', 'uniqueness');
2014-07-29 11:57:49 +00:00
err(false);
}
}, { code: 'uniqueness' });
2014-07-29 11:57:49 +00:00
}
// validate all embedded items
2014-08-19 20:15:30 +00:00
if (definition.options.validate !== false) {
modelFrom.validate(propertyName, function(err) {
var self = this;
var embeddedList = this[propertyName] || [];
var hasErrors = false;
embeddedList.forEach(function(item, idx) {
if (item instanceof modelTo) {
if (!item.isValid()) {
hasErrors = true;
var id = item[idName];
var first = Object.keys(item.errors)[0];
if (id) {
var msg = 'contains invalid item: `' + id + '`';
} else {
var msg = 'contains invalid item at index `' + idx + '`';
}
2014-08-19 20:10:35 +00:00
msg += ' (`' + first + '` ' + item.errors[first] + ')';
self.errors.add(propertyName, msg, 'invalid');
}
} else {
hasErrors = true;
self.errors.add(propertyName, 'contains invalid item', 'invalid');
}
});
if (hasErrors) err(false);
});
}
2014-07-27 14:30:45 +00:00
var scopeMethods = {
findById: scopeMethod(definition, 'findById'),
destroy: scopeMethod(definition, 'destroyById'),
updateById: scopeMethod(definition, 'updateById'),
exists: scopeMethod(definition, 'exists'),
add: scopeMethod(definition, 'add'),
remove: scopeMethod(definition, 'remove'),
get: scopeMethod(definition, 'get'),
set: scopeMethod(definition, 'set'),
unset: scopeMethod(definition, 'unset'),
at: scopeMethod(definition, 'at'),
value: scopeMethod(definition, 'embeddedValue'),
2014-07-27 14:30:45 +00:00
};
2014-07-27 14:30:45 +00:00
var findByIdFunc = scopeMethods.findById;
modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
2014-07-27 14:30:45 +00:00
var destroyByIdFunc = scopeMethods.destroy;
modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
2014-07-27 14:30:45 +00:00
var updateByIdFunc = scopeMethods.updateById;
modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
var addFunc = scopeMethods.add;
modelFrom.prototype['__link__' + relationName] = addFunc;
var removeFunc = scopeMethods.remove;
modelFrom.prototype['__unlink__' + relationName] = removeFunc;
2014-07-27 14:30:45 +00:00
scopeMethods.create = scopeMethod(definition, 'create');
scopeMethods.build = scopeMethod(definition, 'build');
scopeMethods.related = scopeMethod(definition, 'related'); // bound to definition
2015-03-24 14:35:56 +00:00
if (!definition.options.persistent) {
scopeMethods.destroyAll = scopeMethod(definition, 'destroyAll');
}
var customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
for (var i = 0; i < customMethods.length; i++) {
var methodName = customMethods[i];
var method = scopeMethods[methodName];
if (typeof method === 'function' && method.shared === true) {
modelFrom.prototype['__' + methodName + '__' + relationName] = method;
}
};
2014-07-27 14:30:45 +00:00
// Mix the property and scoped methods into the prototype class
var scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function() {
2014-07-27 14:30:45 +00:00
return {};
2014-08-08 16:25:49 +00:00
}, scopeMethods, definition.options);
2014-07-27 14:30:45 +00:00
scopeDefinition.related = scopeMethods.related;
return definition;
2014-07-27 14:30:45 +00:00
};
EmbedsMany.prototype.prepareEmbeddedInstance = function(inst) {
if (inst && inst.triggerParent !== 'function') {
var self = this;
var propertyName = this.definition.keyFrom;
var modelInstance = this.modelInstance;
if (this.definition.options.persistent) {
var pk = this.definition.keyTo;
inst.__persisted = !!inst[pk];
} else {
inst.__persisted = true;
}
inst.triggerParent = function(actionName, callback) {
if (actionName === 'save' || actionName === 'destroy') {
var embeddedList = self.embeddedList();
if (actionName === 'destroy') {
var index = embeddedList.indexOf(inst);
if (index > -1) embeddedList.splice(index, 1);
}
modelInstance.updateAttribute(propertyName,
embeddedList, function(err, modelInst) {
callback(err, err ? null : modelInst);
});
} else {
process.nextTick(callback);
}
};
var originalTrigger = inst.trigger;
inst.trigger = function(actionName, work, data, callback) {
if (typeof work === 'function') {
var originalWork = work;
work = function(next) {
originalWork.call(this, function(done) {
inst.triggerParent(actionName, function(err, inst) {
next(done); // TODO [fabien] - error handling?
});
});
};
}
originalTrigger.call(this, actionName, work, data, callback);
};
}
};
EmbedsMany.prototype.embeddedList =
EmbedsMany.prototype.embeddedValue = function(modelInstance) {
modelInstance = modelInstance || this.modelInstance;
var embeddedList = modelInstance[this.definition.keyFrom] || [];
embeddedList.forEach(this.prepareEmbeddedInstance.bind(this));
return embeddedList;
};
EmbedsMany.prototype.related = function(receiver, scopeParams, condOrRefresh, options, cb) {
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
2014-07-27 14:30:45 +00:00
var actualCond = {};
if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.emails(receiver, scopeParams, cb)
2014-07-27 14:30:45 +00:00
cb = condOrRefresh;
condOrRefresh = false;
} else if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.emails(receiver, scopeParams, condOrRefresh, cb)
cb = options;
options = {};
}
if (typeof condOrRefresh === 'object') {
actualCond = condOrRefresh;
2014-07-27 14:30:45 +00:00
}
var embeddedList = this.embeddedList(receiver);
this.definition.applyScope(receiver, actualCond);
2014-07-27 14:30:45 +00:00
var params = mergeQuery(actualCond, scopeParams);
if (params.where && Object.keys(params.where).length > 0) { // TODO [fabien] Support order/sorting
2014-07-27 14:30:45 +00:00
embeddedList = embeddedList ? embeddedList.filter(applyFilter(params)) : embeddedList;
}
var returnRelated = function(list) {
if (params.include) {
modelTo.include(list, params.include, options, cb);
} else {
process.nextTick(function() { cb(null, list); });
}
};
returnRelated(embeddedList);
2014-07-27 14:30:45 +00:00
};
EmbedsMany.prototype.findById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// order.emails(fkId, cb)
cb = options;
options = {};
}
var pk = this.definition.keyTo;
2014-07-27 14:30:45 +00:00
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var embeddedList = this.embeddedList();
2014-07-27 14:30:45 +00:00
var find = function(id) {
for (var i = 0; i < embeddedList.length; i++) {
var item = embeddedList[i];
if (idEquals(item[pk], id)) return item;
2014-07-27 14:30:45 +00:00
}
return null;
};
2014-07-30 11:22:20 +00:00
var item = find(fkId.toString()); // in case of explicit id
2014-07-27 14:30:45 +00:00
item = (item instanceof modelTo) ? item : null;
2014-07-27 14:30:45 +00:00
if (typeof cb === 'function') {
process.nextTick(function() {
cb(null, item);
});
};
return item; // sync
2014-07-27 14:30:45 +00:00
};
EmbedsMany.prototype.exists = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.emails.exists(fkId, cb)
cb = options;
options = {};
}
2014-07-27 14:30:45 +00:00
var modelTo = this.definition.modelTo;
var inst = this.findById(fkId, options, function(err, inst) {
2014-07-27 14:30:45 +00:00
if (cb) cb(err, inst instanceof modelTo);
});
return inst instanceof modelTo; // sync
2014-07-27 14:30:45 +00:00
};
EmbedsMany.prototype.updateById = function(fkId, data, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.emails.updateById(fkId, data, cb)
cb = options;
options = {};
}
2014-07-27 14:30:45 +00:00
if (typeof data === 'function') {
2015-05-18 16:34:25 +00:00
// customer.emails.updateById(fkId, cb)
2014-07-27 14:30:45 +00:00
cb = data;
data = {};
}
options = options || {};
2014-07-27 14:30:45 +00:00
var modelTo = this.definition.modelTo;
var propertyName = this.definition.keyFrom;
2014-07-27 14:30:45 +00:00
var modelInstance = this.modelInstance;
var embeddedList = this.embeddedList();
2014-07-27 14:30:45 +00:00
var inst = this.findById(fkId);
2014-07-27 14:30:45 +00:00
if (inst instanceof modelTo) {
var hookState = {};
var context = {
Model: modelTo,
currentInstance: inst,
data: data,
options: options,
hookState: hookState,
};
modelTo.notifyObserversOf('before save', context, function(err) {
if (err) return cb && cb(err);
inst.setAttributes(data);
var err = inst.isValid() ? null : new ValidationError(inst);
if (err && typeof cb === 'function') {
return process.nextTick(function() {
cb(err, inst);
});
}
context = {
Model: modelTo,
instance: inst,
options: options,
hookState: hookState,
};
if (typeof cb === 'function') {
modelInstance.updateAttribute(propertyName, embeddedList, options,
function(err) {
if (err) return cb(err, inst);
modelTo.notifyObserversOf('after save', context, function(err) {
cb(err, inst);
});
});
} else {
modelTo.notifyObserversOf('after save', context, function(err) {
if (!err) return;
debug('Unhandled error in "after save" hooks: %s', err.stack || err);
});
}
});
2014-07-27 14:30:45 +00:00
} else if (typeof cb === 'function') {
process.nextTick(function() {
2014-07-27 14:30:45 +00:00
cb(null, null); // not found
});
}
return inst; // sync
2014-07-27 14:30:45 +00:00
};
EmbedsMany.prototype.destroyById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.emails.destroyById(fkId, cb)
cb = options;
options = {};
}
2014-07-27 14:30:45 +00:00
var modelTo = this.definition.modelTo;
var propertyName = this.definition.keyFrom;
2014-07-27 14:30:45 +00:00
var modelInstance = this.modelInstance;
var embeddedList = this.embeddedList();
var inst = (fkId instanceof modelTo) ? fkId : this.findById(fkId);
2014-07-27 14:30:45 +00:00
if (inst instanceof modelTo) {
var context = {
Model: modelTo,
instance: inst,
options: options || {},
hookState: {},
};
modelTo.notifyObserversOf('before delete', context, function(err) {
if (err) return cb(err);
var index = embeddedList.indexOf(inst);
if (index > -1) embeddedList.splice(index, 1);
if (typeof cb !== 'function') return;
modelInstance.updateAttribute(propertyName,
2014-07-27 14:30:45 +00:00
embeddedList, function(err) {
if (err) return cb(err);
modelTo.notifyObserversOf('after delete', context, function(err) {
cb(err);
modelTo.emit('deleted', inst.id, inst.toJSON());
});
});
});
2014-07-27 14:30:45 +00:00
} else if (typeof cb === 'function') {
2014-07-30 11:22:20 +00:00
process.nextTick(cb); // not found
2014-07-27 14:30:45 +00:00
}
return inst; // sync
};
EmbedsMany.prototype.destroyAll = function(where, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.emails.destroyAll(where, cb);
cb = options;
options = {};
2015-05-18 16:34:25 +00:00
} else if (typeof where === 'function' &&
options === undefined && cb === undefined) {
// customer.emails.destroyAll(cb);
cb = where;
where = {};
}
2015-03-24 14:35:56 +00:00
var propertyName = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var embeddedList = this.embeddedList();
if (where && Object.keys(where).length > 0) {
var filter = applyFilter({ where: where });
var reject = function(v) { return !filter(v); };
2015-03-24 14:35:56 +00:00
embeddedList = embeddedList ? embeddedList.filter(reject) : embeddedList;
} else {
embeddedList = [];
}
if (typeof cb === 'function') {
modelInstance.updateAttribute(propertyName,
embeddedList, function(err) {
cb(err);
});
2015-03-24 14:35:56 +00:00
} else {
modelInstance.setAttribute(propertyName, embeddedList);
}
};
EmbedsMany.prototype.get = EmbedsMany.prototype.findById;
EmbedsMany.prototype.set = EmbedsMany.prototype.updateById;
EmbedsMany.prototype.unset = EmbedsMany.prototype.destroyById;
EmbedsMany.prototype.at = function(index, cb) {
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var embeddedList = this.embeddedList();
var item = embeddedList[parseInt(index)];
item = (item instanceof modelTo) ? item : null;
if (typeof cb === 'function') {
process.nextTick(function() {
cb(null, item);
});
};
return item; // sync
};
EmbedsMany.prototype.create = function(targetModelData, options, cb) {
var pk = this.definition.keyTo;
2014-07-27 14:30:45 +00:00
var modelTo = this.definition.modelTo;
var propertyName = this.definition.keyFrom;
2014-07-27 14:30:45 +00:00
var modelInstance = this.modelInstance;
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.emails.create(cb)
cb = options;
options = {};
}
2014-07-27 14:30:45 +00:00
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
var embeddedList = this.embeddedList();
var inst = this.callScopeMethod('build', targetModelData);
var updateEmbedded = function(callback) {
if (modelInstance.isNewRecord()) {
modelInstance.setAttribute(propertyName, embeddedList);
modelInstance.save(options, function(err) {
callback(err, err ? null : inst);
});
} else {
modelInstance.updateAttribute(propertyName,
embeddedList, options, function(err) {
callback(err, err ? null : inst);
});
}
};
if (this.definition.options.persistent) {
inst.save(function(err) { // will validate
if (err) return cb(err, inst);
updateEmbedded(cb);
2014-07-27 14:30:45 +00:00
});
} else {
var err = inst.isValid() ? null : new ValidationError(inst);
if (err) {
process.nextTick(function() {
cb(err);
});
} else {
var context = {
Model: modelTo,
instance: inst,
options: options || {},
hookState: {},
};
modelTo.notifyObserversOf('before save', context, function(err) {
if (err) return cb(err);
updateEmbedded(function(err, inst) {
if (err) return cb(err, null);
modelTo.notifyObserversOf('after save', context, function(err) {
cb(err, err ? null : inst);
});
});
});
}
2014-07-30 11:22:20 +00:00
}
return cb.promise;
2014-07-27 14:30:45 +00:00
};
EmbedsMany.prototype.build = function(targetModelData) {
2014-07-27 14:30:45 +00:00
var modelTo = this.definition.modelTo;
2014-07-27 15:16:25 +00:00
var modelInstance = this.modelInstance;
var forceId = this.definition.options.forceId;
var persistent = this.definition.options.persistent;
var connector = modelTo.dataSource.connector;
var pk = this.definition.keyTo;
var pkProp = modelTo.definition.properties[pk];
var pkType = pkProp && pkProp.type;
var embeddedList = this.embeddedList();
2014-07-27 14:30:45 +00:00
targetModelData = targetModelData || {};
var assignId = (forceId || targetModelData[pk] === undefined);
assignId = assignId && !persistent;
if (assignId && pkType === Number) {
var ids = embeddedList.map(function(m) {
2014-07-27 15:16:25 +00:00
return (typeof m[pk] === 'number' ? m[pk] : 0);
});
2014-07-30 11:22:20 +00:00
if (ids.length > 0) {
targetModelData[pk] = Math.max.apply(null, ids) + 1;
} else {
targetModelData[pk] = 1;
}
} else if (assignId && typeof connector.generateId === 'function') {
var id = connector.generateId(modelTo.modelName, targetModelData, pk);
targetModelData[pk] = id;
2014-07-27 15:16:25 +00:00
}
2014-08-19 20:10:35 +00:00
this.definition.applyProperties(modelInstance, targetModelData);
2014-07-27 14:30:45 +00:00
var inst = new modelTo(targetModelData);
2014-07-27 14:30:45 +00:00
if (this.definition.options.prepend) {
embeddedList.unshift(inst);
} else {
embeddedList.push(inst);
}
this.prepareEmbeddedInstance(inst);
2014-07-27 14:30:45 +00:00
return inst;
};
2014-07-29 13:01:47 +00:00
/**
* Add the target model instance to the 'embedsMany' relation
* @param {Object|ID} acInst The actual instance or id value
*/
EmbedsMany.prototype.add = function(acInst, data, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.emails.add(acInst, data, cb)
cb = options;
options = {};
2015-05-18 16:34:25 +00:00
} else if (typeof data === 'function' &&
options === undefined && cb === undefined) {
// customer.emails.add(acInst, cb)
2014-07-29 19:46:12 +00:00
cb = data;
data = {};
}
cb = cb || utils.createPromiseCallback();
var self = this;
var definition = this.definition;
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var options = definition.options;
var belongsTo = options.belongsTo && modelTo.relations[options.belongsTo];
if (!belongsTo) {
throw new Error('Invalid reference: ' + options.belongsTo || '(none)');
}
var fk2 = belongsTo.keyTo;
var pk2 = belongsTo.modelTo.definition.idName() || 'id';
var query = {};
query[fk2] = (acInst instanceof belongsTo.modelTo) ? acInst[pk2] : acInst;
var filter = { where: query };
belongsTo.applyScope(modelInstance, filter);
belongsTo.modelTo.findOne(filter, options, function(err, ref) {
if (ref instanceof belongsTo.modelTo) {
2014-07-29 19:46:12 +00:00
var inst = self.build(data || {});
inst[options.belongsTo](ref);
modelInstance.save(function(err) {
cb(err, err ? null : inst);
});
} else {
cb(null, null);
}
});
return cb.promise;
};
/**
* Remove the target model instance from the 'embedsMany' relation
* @param {Object|ID) acInst The actual instance or id value
*/
EmbedsMany.prototype.remove = function(acInst, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.emails.remove(acInst, cb)
cb = options;
options = {};
}
var self = this;
var definition = this.definition;
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var options = definition.options;
var belongsTo = options.belongsTo && modelTo.relations[options.belongsTo];
if (!belongsTo) {
throw new Error('Invalid reference: ' + options.belongsTo || '(none)');
}
var fk2 = belongsTo.keyTo;
var pk2 = belongsTo.modelTo.definition.idName() || 'id';
var query = {};
query[fk2] = (acInst instanceof belongsTo.modelTo) ? acInst[pk2] : acInst;
var filter = { where: query };
belongsTo.applyScope(modelInstance, filter);
cb = cb || utils.createPromiseCallback();
modelInstance[definition.name](filter, options, function(err, items) {
if (err) return cb(err);
items.forEach(function(item) {
self.unset(item);
});
modelInstance.save(options, function(err) {
2014-07-30 11:22:20 +00:00
cb(err);
});
});
return cb.promise;
};
2014-07-29 13:01:47 +00:00
RelationDefinition.referencesMany = function referencesMany(modelFrom, modelTo, params) {
2014-07-29 19:46:12 +00:00
params = params || {};
modelTo = lookupModelTo(modelFrom, modelTo, params, true);
var thisClassName = modelFrom.modelName;
2014-07-29 19:46:12 +00:00
var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
var fk = params.foreignKey || i8n.camelize(modelTo.modelName + '_ids', true);
var idName = modelTo.dataSource.idName(modelTo.modelName) || 'id';
2014-07-30 05:19:52 +00:00
var idType = modelTo.definition.properties[idName].type;
2014-07-29 19:46:12 +00:00
var definition = modelFrom.relations[relationName] = new RelationDefinition({
name: relationName,
type: RelationTypes.referencesMany,
modelFrom: modelFrom,
keyFrom: fk,
keyTo: idName,
modelTo: modelTo,
multiple: true,
properties: params.properties,
scope: params.scope,
options: params.options,
2014-07-29 19:46:12 +00:00
});
modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, {
type: [idType], default: function() { return []; },
2014-07-29 19:46:12 +00:00
});
2014-07-29 19:46:12 +00:00
modelFrom.validate(relationName, function(err) {
var ids = this[fk] || [];
2014-07-29 19:46:12 +00:00
var uniqueIds = ids.filter(function(id, pos) {
return utils.findIndexOf(ids, id, idEquals) === pos;
2014-07-29 19:46:12 +00:00
});
if (ids.length !== uniqueIds.length) {
var msg = 'contains duplicate `' + modelTo.modelName + '` instance';
2014-07-29 19:46:12 +00:00
this.errors.add(relationName, msg, 'uniqueness');
err(false);
}
}, { code: 'uniqueness' });
2014-07-29 19:46:12 +00:00
var scopeMethods = {
findById: scopeMethod(definition, 'findById'),
destroy: scopeMethod(definition, 'destroyById'),
updateById: scopeMethod(definition, 'updateById'),
exists: scopeMethod(definition, 'exists'),
add: scopeMethod(definition, 'add'),
remove: scopeMethod(definition, 'remove'),
at: scopeMethod(definition, 'at'),
2014-07-29 19:46:12 +00:00
};
2014-07-29 19:46:12 +00:00
var findByIdFunc = scopeMethods.findById;
modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
2014-07-29 19:46:12 +00:00
var destroyByIdFunc = scopeMethods.destroy;
modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
2014-07-29 19:46:12 +00:00
var updateByIdFunc = scopeMethods.updateById;
modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
var addFunc = scopeMethods.add;
modelFrom.prototype['__link__' + relationName] = addFunc;
var removeFunc = scopeMethods.remove;
modelFrom.prototype['__unlink__' + relationName] = removeFunc;
2014-07-29 19:46:12 +00:00
scopeMethods.create = scopeMethod(definition, 'create');
scopeMethods.build = scopeMethod(definition, 'build');
scopeMethods.related = scopeMethod(definition, 'related');
var customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
for (var i = 0; i < customMethods.length; i++) {
var methodName = customMethods[i];
var method = scopeMethods[methodName];
if (typeof method === 'function' && method.shared === true) {
modelFrom.prototype['__' + methodName + '__' + relationName] = method;
}
};
2014-07-29 19:46:12 +00:00
// Mix the property and scoped methods into the prototype class
var scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function() {
2014-07-29 19:46:12 +00:00
return {};
2014-08-08 16:25:49 +00:00
}, scopeMethods, definition.options);
scopeDefinition.related = scopeMethods.related; // bound to definition
return definition;
2014-07-29 19:46:12 +00:00
};
2014-07-29 13:01:47 +00:00
ReferencesMany.prototype.related = function(receiver, scopeParams, condOrRefresh, options, cb) {
2014-07-29 19:46:12 +00:00
var fk = this.definition.keyFrom;
var modelTo = this.definition.modelTo;
var relationName = this.definition.name;
var modelInstance = this.modelInstance;
var self = receiver;
2014-07-29 19:46:12 +00:00
var actualCond = {};
var actualRefresh = false;
if (typeof condOrRefresh === 'function' &&
options === undefined && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.orders(receiver, scopeParams, cb)
2014-07-29 19:46:12 +00:00
cb = condOrRefresh;
condOrRefresh = undefined;
} else if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.orders(receiver, scopeParams, condOrRefresh, cb)
cb = options;
options = {};
2014-07-29 19:46:12 +00:00
if (typeof condOrRefresh === 'boolean') {
actualRefresh = condOrRefresh;
condOrRefresh = {};
2014-07-29 19:46:12 +00:00
} else {
actualRefresh = true;
}
}
actualCond = condOrRefresh || {};
2014-07-29 19:46:12 +00:00
var ids = self[fk] || [];
2014-07-29 19:46:12 +00:00
this.definition.applyScope(modelInstance, actualCond);
2014-07-29 19:46:12 +00:00
var params = mergeQuery(actualCond, scopeParams);
return modelTo.findByIds(ids, params, options, cb);
2014-07-29 13:01:47 +00:00
};
ReferencesMany.prototype.findById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.orders.findById(fkId, cb)
cb = options;
options = {};
}
2014-07-29 19:46:12 +00:00
var modelTo = this.definition.modelTo;
var modelFrom = this.definition.modelFrom;
var relationName = this.definition.name;
var modelInstance = this.modelInstance;
2014-07-29 19:46:12 +00:00
var pk = this.definition.keyTo;
var fk = this.definition.keyFrom;
2014-07-29 19:46:12 +00:00
if (typeof fkId === 'object') {
fkId = fkId.toString(); // mongodb
}
var ids = modelInstance[fk] || [];
2014-07-29 19:46:12 +00:00
var filter = {};
2014-07-29 19:46:12 +00:00
this.definition.applyScope(modelInstance, filter);
cb = cb || utils.createPromiseCallback();
modelTo.findByIds([fkId], filter, options, function(err, instances) {
2014-07-29 19:46:12 +00:00
if (err) {
return cb(err);
}
2014-07-29 19:46:12 +00:00
var inst = instances[0];
if (!inst) {
err = new Error('No instance with id ' + fkId + ' found for ' + modelTo.modelName);
err.statusCode = 404;
return cb(err);
}
2014-07-29 19:46:12 +00:00
// Check if the foreign key is amongst the ids
if (utils.findIndexOf(ids, inst[pk], idEquals) > -1) {
2014-07-29 19:46:12 +00:00
cb(null, inst);
} else {
2016-05-10 21:25:33 +00:00
err = new Error('Key mismatch: ' + modelFrom.modelName + '.' + fk +
': ' + modelInstance[fk] +
', ' + modelTo.modelName + '.' + pk + ': ' + inst[pk]);
2014-07-29 19:46:12 +00:00
err.statusCode = 400;
cb(err);
}
});
return cb.promise;
2014-07-29 19:46:12 +00:00
};
ReferencesMany.prototype.exists = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.orders.exists(fkId, cb)
cb = options;
options = {};
}
2014-07-29 19:46:12 +00:00
var fk = this.definition.keyFrom;
var ids = this.modelInstance[fk] || [];
cb = cb || utils.createPromiseCallback();
process.nextTick(function() { cb(null, utils.findIndexOf(ids, fkId, idEquals) > -1); });
return cb.promise;
2014-07-29 19:46:12 +00:00
};
ReferencesMany.prototype.updateById = function(fkId, data, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.orders.updateById(fkId, data, cb)
cb = options;
options = {};
2015-05-18 16:34:25 +00:00
} else if (typeof data === 'function' &&
options === undefined && cb === undefined) {
// customer.orders.updateById(fkId, cb)
2014-07-29 19:46:12 +00:00
cb = data;
data = {};
}
cb = cb || utils.createPromiseCallback();
this.findById(fkId, options, function(err, inst) {
2014-07-29 19:46:12 +00:00
if (err) return cb(err);
inst.updateAttributes(data, options, cb);
2014-07-29 19:46:12 +00:00
});
return cb.promise;
2014-07-29 19:46:12 +00:00
};
ReferencesMany.prototype.destroyById = function(fkId, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.orders.destroyById(fkId, cb)
cb = options;
options = {};
}
2014-07-29 19:46:12 +00:00
var self = this;
cb = cb || utils.createPromiseCallback();
2014-07-29 19:46:12 +00:00
this.findById(fkId, function(err, inst) {
if (err) return cb(err);
self.remove(inst, function(err, ids) {
inst.destroy(cb);
});
});
return cb.promise;
2014-07-29 19:46:12 +00:00
};
ReferencesMany.prototype.at = function(index, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.orders.at(index, cb)
cb = options;
options = {};
}
2014-07-29 19:46:12 +00:00
var fk = this.definition.keyFrom;
var ids = this.modelInstance[fk] || [];
cb = cb || utils.createPromiseCallback();
this.findById(ids[index], options, cb);
return cb.promise;
2014-07-29 19:46:12 +00:00
};
ReferencesMany.prototype.create = function(targetModelData, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.orders.create(data, cb)
cb = options;
options = {};
}
2014-07-29 19:46:12 +00:00
var definition = this.definition;
var modelTo = this.definition.modelTo;
var relationName = this.definition.name;
var modelInstance = this.modelInstance;
2014-07-29 19:46:12 +00:00
var pk = this.definition.keyTo;
var fk = this.definition.keyFrom;
2014-07-29 19:46:12 +00:00
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
2014-07-29 19:46:12 +00:00
var ids = modelInstance[fk] || [];
var inst = this.callScopeMethod('build', targetModelData);
inst.save(options, function(err, inst) {
2014-07-29 19:46:12 +00:00
if (err) return cb(err, inst);
2014-07-29 19:46:12 +00:00
var id = inst[pk];
2014-07-29 19:46:12 +00:00
if (typeof id === 'object') {
id = id.toString(); // mongodb
}
2014-07-29 19:46:12 +00:00
if (definition.options.prepend) {
ids.unshift(id);
} else {
ids.push(id);
}
2014-07-29 19:46:12 +00:00
modelInstance.updateAttribute(fk,
ids, options, function(err, modelInst) {
cb(err, inst);
});
2014-07-29 19:46:12 +00:00
});
return cb.promise;
2014-07-29 19:46:12 +00:00
};
ReferencesMany.prototype.build = function(targetModelData) {
var modelTo = this.definition.modelTo;
targetModelData = targetModelData || {};
2014-07-29 19:46:12 +00:00
this.definition.applyProperties(this.modelInstance, targetModelData);
2014-07-29 19:46:12 +00:00
return new modelTo(targetModelData);
};
/**
* Add the target model instance to the 'embedsMany' relation
* @param {Object|ID} acInst The actual instance or id value
*/
ReferencesMany.prototype.add = function(acInst, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.orders.add(acInst, cb)
cb = options;
options = {};
}
2014-07-29 19:46:12 +00:00
var self = this;
var definition = this.definition;
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
2014-07-29 19:46:12 +00:00
var pk = this.definition.keyTo;
var fk = this.definition.keyFrom;
var insert = function(inst, done) {
var id = inst[pk];
2014-07-29 19:46:12 +00:00
if (typeof id === 'object') {
id = id.toString(); // mongodb
}
2014-07-29 19:46:12 +00:00
var ids = modelInstance[fk] || [];
2014-07-29 19:46:12 +00:00
if (definition.options.prepend) {
ids.unshift(id);
} else {
ids.push(id);
}
modelInstance.updateAttribute(fk, ids, options, function(err) {
done(err, err ? null : inst);
2014-07-29 19:46:12 +00:00
});
};
cb = cb || utils.createPromiseCallback();
2014-07-29 19:46:12 +00:00
if (acInst instanceof modelTo) {
insert(acInst, cb);
2014-07-29 19:46:12 +00:00
} else {
var filter = { where: {}};
2014-07-29 19:46:12 +00:00
filter.where[pk] = acInst;
2014-07-29 19:46:12 +00:00
definition.applyScope(modelInstance, filter);
modelTo.findOne(filter, options, function(err, inst) {
if (err || !inst) return cb(err, null);
insert(inst, cb);
2014-07-29 19:46:12 +00:00
});
}
return cb.promise;
2014-07-29 19:46:12 +00:00
};
/**
* Remove the target model instance from the 'embedsMany' relation
* @param {Object|ID) acInst The actual instance or id value
*/
ReferencesMany.prototype.remove = function(acInst, options, cb) {
if (typeof options === 'function' && cb === undefined) {
2015-05-18 16:34:25 +00:00
// customer.orders.remove(acInst, cb)
cb = options;
options = {};
}
2014-07-29 19:46:12 +00:00
var definition = this.definition;
var modelInstance = this.modelInstance;
2014-07-29 19:46:12 +00:00
var pk = this.definition.keyTo;
var fk = this.definition.keyFrom;
2014-07-29 19:46:12 +00:00
var ids = modelInstance[fk] || [];
2014-07-29 19:46:12 +00:00
var id = (acInst instanceof definition.modelTo) ? acInst[pk] : acInst;
cb = cb || utils.createPromiseCallback();
var index = utils.findIndexOf(ids, id, idEquals);
2014-07-29 19:46:12 +00:00
if (index > -1) {
ids.splice(index, 1);
modelInstance.updateAttribute(fk, ids, options, function(err, inst) {
2014-07-29 19:46:12 +00:00
cb(err, inst[fk] || []);
});
} else {
process.nextTick(function() { cb(null, ids); });
}
return cb.promise;
};