3510 lines
108 KiB
JavaScript
3510 lines
108 KiB
JavaScript
// Copyright IBM Corp. 2014,2018. 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
|
|
|
|
'use strict';
|
|
|
|
/*!
|
|
* Dependencies
|
|
*/
|
|
const assert = require('assert');
|
|
const util = require('util');
|
|
const async = require('async');
|
|
const utils = require('./utils');
|
|
const i8n = require('inflection');
|
|
const defineScope = require('./scope.js').defineScope;
|
|
const g = require('strong-globalize')();
|
|
const mergeQuery = utils.mergeQuery;
|
|
const idEquals = utils.idEquals;
|
|
const idsHaveDuplicates = utils.idsHaveDuplicates;
|
|
const ModelBaseClass = require('./model.js');
|
|
const applyFilter = require('./connectors/memory').applyFilter;
|
|
const ValidationError = require('./validations.js').ValidationError;
|
|
const deprecated = require('depd')('loopback-datasource-juggler');
|
|
const debug = require('debug')('loopback:relations');
|
|
|
|
const RelationTypes = {
|
|
belongsTo: 'belongsTo',
|
|
hasMany: 'hasMany',
|
|
hasOne: 'hasOne',
|
|
hasAndBelongsToMany: 'hasAndBelongsToMany',
|
|
referencesMany: 'referencesMany',
|
|
embedsOne: 'embedsOne',
|
|
embedsMany: 'embedsMany',
|
|
};
|
|
|
|
const RelationClasses = {
|
|
belongsTo: BelongsTo,
|
|
hasMany: HasMany,
|
|
hasManyThrough: HasManyThrough,
|
|
hasOne: HasOne,
|
|
hasAndBelongsToMany: HasAndBelongsToMany,
|
|
referencesMany: ReferencesMany,
|
|
embedsOne: EmbedsOne,
|
|
embedsMany: EmbedsMany,
|
|
};
|
|
|
|
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;
|
|
|
|
function normalizeType(type) {
|
|
if (!type) {
|
|
return type;
|
|
}
|
|
const t1 = type.toLowerCase();
|
|
for (const t2 in RelationTypes) {
|
|
if (t2.toLowerCase() === t1) {
|
|
return t2;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function extendScopeMethods(definition, scopeMethods, ext) {
|
|
let customMethods = [];
|
|
let 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() {
|
|
const relation = new relationClass(definition, this);
|
|
return relationMethod.apply(relation, arguments);
|
|
};
|
|
}
|
|
for (const key in ext) {
|
|
const relationMethod = ext[key];
|
|
const method = scopeMethods[key] = createFunc(definition, relationMethod);
|
|
if (relationMethod.shared) {
|
|
sharedMethod(definition, key, method, relationMethod);
|
|
}
|
|
customMethods.push(key);
|
|
}
|
|
}
|
|
return [].concat(customMethods || []);
|
|
}
|
|
|
|
function bindRelationMethods(relation, relationMethod, definition) {
|
|
const methods = definition.methods || {};
|
|
Object.keys(methods).forEach(function(m) {
|
|
if (typeof methods[m] !== 'function') return;
|
|
relationMethod[m] = methods[m].bind(relation);
|
|
});
|
|
}
|
|
|
|
function preventFkOverride(inst, data, fkProp) {
|
|
if (!fkProp) return undefined;
|
|
if (data[fkProp] !== undefined && !idEquals(data[fkProp], inst[fkProp])) {
|
|
return new Error(g.f(
|
|
'Cannot override foreign key %s from %s to %s',
|
|
fkProp,
|
|
inst[fkProp],
|
|
data[fkProp]
|
|
));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
assert(this.modelFrom, 'Source model is required');
|
|
this.keyFrom = definition.keyFrom;
|
|
this.modelTo = definition.modelTo;
|
|
this.keyTo = definition.keyTo;
|
|
this.polymorphic = definition.polymorphic;
|
|
if (typeof this.polymorphic !== 'object') {
|
|
assert(this.modelTo, 'Target model is required');
|
|
}
|
|
this.modelThrough = definition.modelThrough;
|
|
this.keyThrough = definition.keyThrough;
|
|
this.multiple = definition.multiple;
|
|
this.properties = definition.properties || {};
|
|
this.options = definition.options || {};
|
|
this.scope = definition.scope;
|
|
this.embed = definition.embed === true;
|
|
this.methods = definition.methods || {};
|
|
}
|
|
|
|
RelationDefinition.prototype.toJSON = function() {
|
|
const polymorphic = typeof this.polymorphic === 'object';
|
|
|
|
let modelToName = this.modelTo && this.modelTo.modelName;
|
|
if (!modelToName && polymorphic && this.type === 'belongsTo') {
|
|
modelToName = '<polymorphic>';
|
|
}
|
|
|
|
const json = {
|
|
name: this.name,
|
|
type: this.type,
|
|
modelFrom: this.modelFrom.modelName,
|
|
keyFrom: this.keyFrom,
|
|
modelTo: modelToName,
|
|
keyTo: this.keyTo,
|
|
multiple: this.multiple,
|
|
};
|
|
if (this.modelThrough) {
|
|
json.modelThrough = this.modelThrough.modelName;
|
|
json.keyThrough = this.keyThrough;
|
|
}
|
|
if (polymorphic) {
|
|
json.polymorphic = this.polymorphic;
|
|
}
|
|
return json;
|
|
};
|
|
|
|
/**
|
|
* Define a relation scope method
|
|
* @param {String} name of the method
|
|
* @param {Function} function to define
|
|
*/
|
|
RelationDefinition.prototype.defineMethod = function(name, fn) {
|
|
const relationClass = RelationClasses[this.type];
|
|
const relationName = this.name;
|
|
const modelFrom = this.modelFrom;
|
|
const definition = this;
|
|
let method;
|
|
if (definition.multiple) {
|
|
const scope = this.modelFrom.scopes[this.name];
|
|
if (!scope) throw new Error(g.f('Unknown relation {{scope}}: %s', this.name));
|
|
method = scope.defineMethod(name, function() {
|
|
const relation = new relationClass(definition, this);
|
|
return fn.apply(relation, arguments);
|
|
});
|
|
} else {
|
|
definition.methods[name] = fn;
|
|
method = function() {
|
|
const 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) {
|
|
filter = filter || {};
|
|
filter.where = filter.where || {};
|
|
if ((this.type !== 'belongsTo' || this.type === 'hasOne') &&
|
|
typeof this.polymorphic === 'object') { // polymorphic
|
|
const discriminator = this.polymorphic.discriminator;
|
|
if (this.polymorphic.invert) {
|
|
filter.where[discriminator] = this.modelTo.modelName;
|
|
} else {
|
|
filter.where[discriminator] = this.modelFrom.modelName;
|
|
}
|
|
}
|
|
let scope;
|
|
if (typeof this.scope === 'function') {
|
|
scope = this.scope.call(this, modelInstance, filter);
|
|
} else {
|
|
scope = this.scope;
|
|
}
|
|
if (typeof scope === 'object') {
|
|
mergeQuery(filter, scope);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Apply the configured properties to the target object.
|
|
* @param {Object} modelInstance
|
|
* @param {Object} target
|
|
*/
|
|
RelationDefinition.prototype.applyProperties = function(modelInstance, obj) {
|
|
let source = modelInstance, target = obj;
|
|
if (this.options.invertProperties) {
|
|
source = obj;
|
|
target = modelInstance;
|
|
}
|
|
if (this.options.embedsProperties) {
|
|
target = target.__data[this.name] = {};
|
|
target[this.keyTo] = source[this.keyTo];
|
|
}
|
|
let k, key;
|
|
if (typeof this.properties === 'function') {
|
|
const 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++) {
|
|
key = this.properties[k];
|
|
target[key] = source[key];
|
|
}
|
|
} else if (typeof this.properties === 'object') {
|
|
for (k in this.properties) {
|
|
key = this.properties[k];
|
|
target[key] = source[k];
|
|
}
|
|
}
|
|
if ((this.type !== 'belongsTo' || this.type === 'hasOne') &&
|
|
typeof this.polymorphic === 'object') { // polymorphic
|
|
const discriminator = this.polymorphic.discriminator;
|
|
if (this.polymorphic.invert) {
|
|
target[discriminator] = this.modelTo.modelName;
|
|
} else {
|
|
target[discriminator] = this.modelFrom.modelName;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
cache = cache || undefined;
|
|
this.modelInstance.__cachedRelations[this.definition.name] = cache;
|
|
};
|
|
|
|
Relation.prototype.getCache = function() {
|
|
return this.modelInstance.__cachedRelations[this.definition.name];
|
|
};
|
|
|
|
Relation.prototype.callScopeMethod = function(methodName) {
|
|
const args = Array.prototype.slice.call(arguments, 1);
|
|
const modelInstance = this.modelInstance;
|
|
const rel = modelInstance[this.definition.name];
|
|
if (rel && typeof rel[methodName] === 'function') {
|
|
return rel[methodName].apply(rel, args);
|
|
} else {
|
|
throw new Error(g.f('Unknown scope method: %s', 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);
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
const cache = this.modelInstance.__cachedRelations[this.definition.name];
|
|
const idName = this.definition.modelTo.definition.idName();
|
|
if (Array.isArray(cache)) {
|
|
for (let i = 0, n = cache.length; i < n; i++) {
|
|
if (idEquals(cache[i][idName], id)) {
|
|
return cache.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
HasMany.prototype.addToCache = function(inst) {
|
|
if (!inst) {
|
|
return;
|
|
}
|
|
let cache = this.modelInstance.__cachedRelations[this.definition.name];
|
|
if (cache === undefined) {
|
|
cache = this.modelInstance.__cachedRelations[this.definition.name] = [];
|
|
}
|
|
const idName = this.definition.modelTo.definition.idName();
|
|
if (Array.isArray(cache)) {
|
|
for (let i = 0, n = cache.length; i < n; i++) {
|
|
if (idEquals(cache[i][idName], inst[idName])) {
|
|
cache[i] = inst;
|
|
return;
|
|
}
|
|
}
|
|
cache.push(inst);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
|
|
/**
|
|
* EmbedsOne subclass
|
|
* @param {RelationDefinition|Object} definition
|
|
* @param {Object} modelInstance
|
|
* @returns {EmbedsOne}
|
|
* @constructor
|
|
* @class EmbedsOne
|
|
*/
|
|
function EmbedsOne(definition, modelInstance) {
|
|
if (!(this instanceof EmbedsOne)) {
|
|
return new EmbedsOne(definition, modelInstance);
|
|
}
|
|
assert(definition.type === RelationTypes.embedsOne);
|
|
Relation.apply(this, arguments);
|
|
}
|
|
|
|
util.inherits(EmbedsOne, Relation);
|
|
|
|
/**
|
|
* 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);
|
|
|
|
/**
|
|
* ReferencesMany subclass
|
|
* @param {RelationDefinition|Object} definition
|
|
* @param {Object} modelInstance
|
|
* @returns {ReferencesMany}
|
|
* @constructor
|
|
* @class ReferencesMany
|
|
*/
|
|
function ReferencesMany(definition, modelInstance) {
|
|
if (!(this instanceof ReferencesMany)) {
|
|
return new ReferencesMany(definition, modelInstance);
|
|
}
|
|
assert(definition.type === RelationTypes.referencesMany);
|
|
Relation.apply(this, arguments);
|
|
}
|
|
|
|
util.inherits(ReferencesMany, Relation);
|
|
|
|
/*!
|
|
* Find the relation by foreign key
|
|
* @param {*} foreignKey The foreign key
|
|
* @returns {Array} The array of matching relation objects
|
|
*/
|
|
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;
|
|
});
|
|
}
|
|
|
|
/*!
|
|
* Look up a model by name from the list of given models
|
|
* @param {Object} models Models keyed by name
|
|
* @param {String} modelName The model name
|
|
* @returns {*} The matching model class
|
|
*/
|
|
function lookupModel(models, modelName) {
|
|
if (models[modelName]) {
|
|
return models[modelName];
|
|
}
|
|
const lookupClassName = modelName.toLowerCase();
|
|
for (const name in models) {
|
|
if (name.toLowerCase() === lookupClassName) {
|
|
return models[name];
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* @param {Object} modelFrom Instance of the 'from' model
|
|
* @param {Object|String} modelToRef Reference to Model object to which you are
|
|
* creating the relation: model instance, model name, or name of relation to model.
|
|
* @param {Object} params The relation params
|
|
* @param {Boolean} singularize Whether the modelToRef should be singularized when
|
|
* looking-up modelTo
|
|
* @return {Object} modelTo Instance of the 'to' model
|
|
*/
|
|
function lookupModelTo(modelFrom, modelToRef, params, singularize) {
|
|
let modelTo;
|
|
|
|
if (typeof modelToRef !== 'string') {
|
|
// modelToRef might already be an instance of model
|
|
modelTo = modelToRef;
|
|
} else {
|
|
// lookup modelTo based on relation params and modelToRef
|
|
let modelToName;
|
|
modelTo = params.model || modelToRef; // modelToRef might be modelTo name
|
|
|
|
if (typeof modelTo === 'string') {
|
|
// lookup modelTo by name
|
|
modelToName = modelTo;
|
|
modelToName = (singularize ? i8n.singularize(modelToName) : modelToName).toLowerCase();
|
|
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
|
|
}
|
|
|
|
if (!modelTo) {
|
|
// lookup by modelTo name was not successful. Now looking-up by relationTo name
|
|
const relationToName = params.as || modelToRef; // modelToRef might be relationTo name
|
|
modelToName = (singularize ? i8n.singularize(relationToName) : relationToName).toLowerCase();
|
|
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
|
|
}
|
|
}
|
|
if (typeof modelTo !== 'function') {
|
|
throw new Error(g.f('Could not find relation %s for model %s', params.as, modelFrom.modelName));
|
|
}
|
|
return modelTo;
|
|
}
|
|
|
|
/*
|
|
* Normalize relation's parameter `as`
|
|
* @param {Object} params The relation params
|
|
* @param {String} relationName The relation name
|
|
* @returns {Object} The normalized parameters
|
|
* NOTE: normalizeRelationAs() mutates the params object
|
|
*/
|
|
function normalizeRelationAs(params, relationName) {
|
|
if (typeof relationName === 'string') {
|
|
params.as = params.as || relationName;
|
|
}
|
|
return params;
|
|
}
|
|
|
|
/*
|
|
* Normalize relation's polymorphic parameters
|
|
* @param {Object|String|Boolean} polymorphic Param `polymorphic` of the relation.
|
|
* @param {String} relationName The name of the relation we are currently setting up.
|
|
* @returns {Object} The normalized parameters
|
|
*/
|
|
function normalizePolymorphic(polymorphic, relationName) {
|
|
assert(polymorphic, 'polymorphic param can\'t be false, null or undefined');
|
|
assert(!Array.isArray(polymorphic, 'unexpected type for polymorphic param: \'Array\''));
|
|
|
|
let selector;
|
|
|
|
if (typeof polymorphic === 'string') {
|
|
// relation type is different from belongsTo (hasMany, hasManyThrough, hasAndBelongsToMany, ...)
|
|
// polymorphic is the name of the matching belongsTo relation from modelTo to modelFrom
|
|
selector = polymorphic;
|
|
}
|
|
|
|
if (polymorphic === true) {
|
|
// relation type is belongsTo: the relation name is used as the polymorphic selector
|
|
selector = relationName;
|
|
}
|
|
|
|
// NOTE: use of `polymorphic.as` keyword will be deprecated in LoopBack.next
|
|
// to avoid confusion with keyword `as` used at the root of the relation definition object
|
|
// It is replaced with the `polymorphic.selector` keyword
|
|
if (typeof polymorphic == 'object') {
|
|
selector = polymorphic.selector || polymorphic.as;
|
|
}
|
|
|
|
// relationName is eventually used as selector if provided and selector not already defined
|
|
// it ultimately defaults to 'reference'
|
|
selector = selector || relationName || 'reference';
|
|
|
|
// make sure polymorphic is an object
|
|
if (typeof polymorphic !== 'object') {
|
|
polymorphic = {};
|
|
}
|
|
|
|
polymorphic.selector = selector;
|
|
polymorphic.foreignKey = polymorphic.foreignKey || i8n.camelize(selector + '_id', true); // defaults to {{selector}}Id
|
|
polymorphic.discriminator = polymorphic.discriminator || i8n.camelize(selector + '_type', true); // defaults to {{selectorName}}Type
|
|
|
|
return polymorphic;
|
|
}
|
|
|
|
/**
|
|
* Define a "one to many" relationship by specifying the model name
|
|
*
|
|
* Examples:
|
|
* ```
|
|
* User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});
|
|
* ```
|
|
*
|
|
* ```
|
|
* Book.hasMany(Chapter);
|
|
* ```
|
|
* Or, equivalently:
|
|
* ```
|
|
* Book.hasMany('chapters', {model: Chapter});
|
|
* ```
|
|
* @param {Model} modelFrom Source model class
|
|
* @param {Object|String} modelToRef Reference to Model object to which you are
|
|
* creating the relation: model instance, model name, or name of relation to model.
|
|
* @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, modelToRef, params) {
|
|
const thisClassName = modelFrom.modelName;
|
|
params = params || {};
|
|
normalizeRelationAs(params, modelToRef);
|
|
const modelTo = lookupModelTo(modelFrom, modelToRef, params, true);
|
|
|
|
const relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
|
|
let fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true);
|
|
let keyThrough = params.keyThrough || i8n.camelize(modelTo.modelName + '_id', true);
|
|
|
|
const pkName = params.primaryKey || modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
|
|
let discriminator, polymorphic;
|
|
|
|
if (params.polymorphic) {
|
|
polymorphic = normalizePolymorphic(params.polymorphic, relationName);
|
|
if (params.invert) {
|
|
polymorphic.invert = true;
|
|
keyThrough = polymorphic.foreignKey;
|
|
}
|
|
discriminator = polymorphic.discriminator;
|
|
if (!params.invert) {
|
|
fk = polymorphic.foreignKey;
|
|
}
|
|
if (!params.through) {
|
|
modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, {type: 'string', index: true});
|
|
}
|
|
}
|
|
|
|
const definition = new RelationDefinition({
|
|
name: relationName,
|
|
type: RelationTypes.hasMany,
|
|
modelFrom: modelFrom,
|
|
keyFrom: pkName,
|
|
keyTo: fk,
|
|
modelTo: modelTo,
|
|
multiple: true,
|
|
properties: params.properties,
|
|
scope: params.scope,
|
|
options: params.options,
|
|
keyThrough: keyThrough,
|
|
polymorphic: polymorphic,
|
|
});
|
|
|
|
definition.modelThrough = params.through;
|
|
|
|
modelFrom.relations[relationName] = definition;
|
|
|
|
if (!params.through) {
|
|
// obviously, modelTo should have attribute called `fk`
|
|
// 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);
|
|
}
|
|
|
|
const scopeMethods = {
|
|
findById: scopeMethod(definition, 'findById'),
|
|
destroy: scopeMethod(definition, 'destroyById'),
|
|
updateById: scopeMethod(definition, 'updateById'),
|
|
exists: scopeMethod(definition, 'exists'),
|
|
};
|
|
|
|
const findByIdFunc = scopeMethods.findById;
|
|
modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
|
|
|
|
const destroyByIdFunc = scopeMethods.destroy;
|
|
modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
|
|
|
|
const updateByIdFunc = scopeMethods.updateById;
|
|
modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
|
|
|
|
const existsByIdFunc = scopeMethods.exists;
|
|
modelFrom.prototype['__exists__' + relationName] = existsByIdFunc;
|
|
|
|
if (definition.modelThrough) {
|
|
scopeMethods.create = scopeMethod(definition, 'create');
|
|
scopeMethods.add = scopeMethod(definition, 'add');
|
|
scopeMethods.remove = scopeMethod(definition, 'remove');
|
|
|
|
const addFunc = scopeMethods.add;
|
|
modelFrom.prototype['__link__' + relationName] = addFunc;
|
|
|
|
const removeFunc = scopeMethods.remove;
|
|
modelFrom.prototype['__unlink__' + relationName] = removeFunc;
|
|
} else {
|
|
scopeMethods.create = scopeMethod(definition, 'create');
|
|
scopeMethods.build = scopeMethod(definition, 'build');
|
|
}
|
|
|
|
const customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
|
|
|
|
for (let i = 0; i < customMethods.length; i++) {
|
|
const methodName = customMethods[i];
|
|
const method = scopeMethods[methodName];
|
|
if (typeof method === 'function' && method.shared === true) {
|
|
modelFrom.prototype['__' + methodName + '__' + relationName] = method;
|
|
}
|
|
}
|
|
|
|
// Mix the property and scoped methods into the prototype class
|
|
defineScope(modelFrom.prototype, params.through || modelTo, relationName, function() {
|
|
const filter = {};
|
|
filter.where = {};
|
|
filter.where[fk] = this[pkName];
|
|
|
|
definition.applyScope(this, filter);
|
|
|
|
if (definition.modelThrough) {
|
|
let throughRelationName;
|
|
|
|
// find corresponding belongsTo relations from through model as collect
|
|
for (const r in definition.modelThrough.relations) {
|
|
const 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.selector;
|
|
filter.include = filter.collect;
|
|
} else {
|
|
filter.collect = throughRelationName || i8n.camelize(modelTo.modelName, true);
|
|
filter.include = filter.collect;
|
|
}
|
|
}
|
|
|
|
return filter;
|
|
}, scopeMethods, definition.options);
|
|
|
|
return definition;
|
|
};
|
|
|
|
function scopeMethod(definition, methodName) {
|
|
let relationClass = RelationClasses[definition.type];
|
|
if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
|
|
relationClass = RelationClasses.hasManyThrough;
|
|
}
|
|
const method = function() {
|
|
const relation = new relationClass(definition, this);
|
|
return relation[methodName].apply(relation, arguments);
|
|
};
|
|
|
|
const relationMethod = relationClass.prototype[methodName];
|
|
if (relationMethod.shared) {
|
|
sharedMethod(definition, methodName, method, relationMethod);
|
|
}
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Find a related item by foreign key
|
|
* @param {*} fkId The foreign key
|
|
* @param {Object} [options] Options
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
HasMany.prototype.findById = function(fkId, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const modelTo = this.definition.modelTo;
|
|
const modelFrom = this.definition.modelFrom;
|
|
const fk = this.definition.keyTo;
|
|
const pk = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
const idName = this.definition.modelTo.definition.idName();
|
|
const filter = {};
|
|
filter.where = {};
|
|
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) {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
if (!inst) {
|
|
err = new Error(g.f('No instance with {{id}} %s found for %s', fkId, modelTo.modelName));
|
|
err.statusCode = 404;
|
|
return cb(err);
|
|
}
|
|
// Check if the foreign key matches the primary key
|
|
if (inst[fk] != null && idEquals(inst[fk], modelInstance[pk])) {
|
|
cb(null, inst);
|
|
} else {
|
|
err = new Error(g.f('Key mismatch: %s.%s: %s, %s.%s: %s',
|
|
modelFrom.modelName, pk, modelInstance[pk], modelTo.modelName, fk, inst[fk]));
|
|
err.statusCode = 400;
|
|
cb(err);
|
|
}
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
/**
|
|
* Find a related item by foreign key
|
|
* @param {*} fkId The foreign key
|
|
* @param {Object} [options] Options
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
HasMany.prototype.exists = function(fkId, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const fk = this.definition.keyTo;
|
|
const pk = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
this.findById(fkId, function(err, inst) {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
if (!inst) {
|
|
return cb(null, false);
|
|
}
|
|
// Check if the foreign key matches the primary key
|
|
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(null, false);
|
|
}
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
/**
|
|
* Update a related item by foreign key
|
|
* @param {*} fkId The foreign key
|
|
* @param {Object} Changes to the data
|
|
* @param {Object} [options] Options
|
|
* @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();
|
|
const fk = this.definition.keyTo;
|
|
|
|
this.findById(fkId, options, function(err, inst) {
|
|
if (err) {
|
|
return cb && cb(err);
|
|
}
|
|
// Ensure Foreign Key cannot be changed!
|
|
const fkErr = preventFkOverride(inst, data, fk);
|
|
if (fkErr) return cb(fkErr);
|
|
inst.updateAttributes(data, options, cb);
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
/**
|
|
* Delete a related item by foreign key
|
|
* @param {*} fkId The foreign key
|
|
* @param {Object} [options] Options
|
|
* @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();
|
|
const self = this;
|
|
this.findById(fkId, options, function(err, inst) {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
self.removeFromCache(fkId);
|
|
inst.destroy(options, cb);
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
const throughKeys = function(definition) {
|
|
const modelThrough = definition.modelThrough;
|
|
const pk2 = definition.modelTo.definition.idName();
|
|
|
|
let fk1, fk2;
|
|
if (typeof definition.polymorphic === 'object') { // polymorphic
|
|
fk1 = definition.keyTo;
|
|
if (definition.polymorphic.invert) {
|
|
fk2 = definition.polymorphic.foreignKey;
|
|
} else {
|
|
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;
|
|
});
|
|
} else {
|
|
fk1 = findBelongsTo(modelThrough, definition.modelFrom,
|
|
definition.keyFrom)[0];
|
|
fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2)[0];
|
|
}
|
|
return [fk1, fk2];
|
|
};
|
|
|
|
/**
|
|
* Find a related item by foreign key
|
|
* @param {*} fkId The foreign key value
|
|
* @param {Object} [options] Options
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
HasManyThrough.prototype.findById = function(fkId, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const self = this;
|
|
const modelTo = this.definition.modelTo;
|
|
const pk = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
const modelThrough = this.definition.modelThrough;
|
|
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
self.exists(fkId, options, function(err, exists) {
|
|
if (err || !exists) {
|
|
if (!err) {
|
|
err = new Error(g.f('No relation found in %s' +
|
|
' for (%s.%s,%s.%s)',
|
|
modelThrough.modelName, self.definition.modelFrom.modelName,
|
|
modelInstance[pk], modelTo.modelName, fkId));
|
|
err.statusCode = 404;
|
|
}
|
|
return cb(err);
|
|
}
|
|
modelTo.findById(fkId, options, function(err, inst) {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
if (!inst) {
|
|
err = new Error(g.f('No instance with id %s found for %s', fkId, modelTo.modelName));
|
|
err.statusCode = 404;
|
|
return cb(err);
|
|
}
|
|
cb(err, inst);
|
|
});
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
/**
|
|
* Delete a related item by foreign key
|
|
* @param {*} fkId The foreign key
|
|
* @param {Object} [options] Options
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
HasManyThrough.prototype.destroyById = function(fkId, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const self = this;
|
|
const modelTo = this.definition.modelTo;
|
|
const pk = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
const modelThrough = this.definition.modelThrough;
|
|
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
self.exists(fkId, options, function(err, exists) {
|
|
if (err || !exists) {
|
|
if (!err) {
|
|
err = new Error(g.f('No record found in %s for (%s.%s ,%s.%s)',
|
|
modelThrough.modelName, self.definition.modelFrom.modelName,
|
|
modelInstance[pk], modelTo.modelName, fkId));
|
|
err.statusCode = 404;
|
|
}
|
|
return cb(err);
|
|
}
|
|
self.remove(fkId, options, function(err) {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
modelTo.deleteById(fkId, options, cb);
|
|
});
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
// 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 = {};
|
|
}
|
|
const self = this;
|
|
const definition = this.definition;
|
|
const modelTo = definition.modelTo;
|
|
const modelThrough = definition.modelThrough;
|
|
|
|
if (typeof data === 'function' && !cb) {
|
|
cb = data;
|
|
data = {};
|
|
}
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
const modelInstance = this.modelInstance;
|
|
|
|
// First create the target model
|
|
modelTo.create(data, options, function(err, to) {
|
|
if (err) {
|
|
return cb(err, to);
|
|
}
|
|
// The primary key for the target model
|
|
const pk2 = definition.modelTo.definition.idName();
|
|
const keys = throughKeys(definition);
|
|
const fk1 = keys[0];
|
|
const fk2 = keys[1];
|
|
|
|
function createRelation(to, next) {
|
|
const 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);
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
HasManyThrough.prototype.add = function(acInst, data, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const self = this;
|
|
const definition = this.definition;
|
|
const modelThrough = definition.modelThrough;
|
|
const pk1 = definition.keyFrom;
|
|
|
|
if (typeof data === 'function') {
|
|
cb = data;
|
|
data = {};
|
|
}
|
|
const query = {};
|
|
|
|
data = data || {};
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
// The primary key for the target model
|
|
const pk2 = definition.modelTo.definition.idName();
|
|
|
|
const keys = throughKeys(definition);
|
|
const fk1 = keys[0];
|
|
const fk2 = keys[1];
|
|
|
|
query[fk1] = this.modelInstance[pk1];
|
|
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
|
|
|
|
const filter = {where: query};
|
|
|
|
definition.applyScope(this.modelInstance, filter);
|
|
|
|
data[fk1] = this.modelInstance[pk1];
|
|
data[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
|
|
|
|
definition.applyProperties(this.modelInstance, data);
|
|
|
|
// Create an instance of the through model
|
|
modelThrough.findOrCreate(filter, data, options, function(err, ac) {
|
|
if (!err) {
|
|
if (acInst instanceof definition.modelTo) {
|
|
self.addToCache(acInst);
|
|
}
|
|
}
|
|
cb(err, ac);
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
/**
|
|
* 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 = {};
|
|
}
|
|
const definition = this.definition;
|
|
const modelThrough = definition.modelThrough;
|
|
const pk1 = definition.keyFrom;
|
|
|
|
const query = {};
|
|
|
|
// The primary key for the target model
|
|
const pk2 = definition.modelTo.definition.idName();
|
|
|
|
const keys = throughKeys(definition);
|
|
const fk1 = keys[0];
|
|
const fk2 = keys[1];
|
|
|
|
query[fk1] = this.modelInstance[pk1];
|
|
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
|
|
|
|
const filter = {where: query};
|
|
|
|
definition.applyScope(this.modelInstance, filter);
|
|
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
modelThrough.count(filter.where, options, function(err, ac) {
|
|
cb(err, ac > 0);
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
/**
|
|
* 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 = {};
|
|
}
|
|
const self = this;
|
|
const definition = this.definition;
|
|
const modelThrough = definition.modelThrough;
|
|
const pk1 = definition.keyFrom;
|
|
|
|
const query = {};
|
|
|
|
// The primary key for the target model
|
|
const pk2 = definition.modelTo.definition.idName();
|
|
|
|
const keys = throughKeys(definition);
|
|
const fk1 = keys[0];
|
|
const fk2 = keys[1];
|
|
|
|
query[fk1] = this.modelInstance[pk1];
|
|
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
|
|
|
|
const filter = {where: query};
|
|
|
|
definition.applyScope(this.modelInstance, filter);
|
|
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
modelThrough.deleteAll(filter.where, options, function(err) {
|
|
if (!err) {
|
|
self.removeFromCache(query[fk2]);
|
|
}
|
|
cb(err);
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
/**
|
|
* Declare "belongsTo" relation that sets up a one-to-one connection with
|
|
* another model, such that each instance of the declaring model "belongs to"
|
|
* one instance of the other model.
|
|
*
|
|
* For example, if an application includes users and posts, and each post can
|
|
* be written by exactly one user. The following code specifies that `Post` has
|
|
* a reference called `author` to the `User` model via the `userId` property of
|
|
* `Post` as the foreign key.
|
|
* ```
|
|
* Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
|
|
* ```
|
|
*
|
|
* This optional parameter default value is false, so the related object will
|
|
* be loaded from cache if available.
|
|
*
|
|
* @param {Object|String} modelToRef Reference to Model object to which you are
|
|
* creating the relation: model instance, model name, or name of relation to model.
|
|
* @options {Object} params Configuration parameters; see below.
|
|
* @property {String} as Name of the property in the referring model that
|
|
* corresponds to the foreign key field in the related model.
|
|
* @property {String} foreignKey Name of foreign key property.
|
|
*
|
|
*/
|
|
RelationDefinition.belongsTo = function(modelFrom, modelToRef, params) {
|
|
let modelTo, discriminator, polymorphic;
|
|
params = params || {};
|
|
|
|
let pkName, relationName, fk;
|
|
if (params.polymorphic) {
|
|
relationName = params.as || (typeof modelToRef === 'string' ? modelToRef : null);
|
|
polymorphic = normalizePolymorphic(params.polymorphic, relationName);
|
|
|
|
modelTo = null; // will be looked-up dynamically
|
|
|
|
pkName = params.primaryKey || params.idName || 'id';
|
|
fk = polymorphic.foreignKey;
|
|
discriminator = polymorphic.discriminator;
|
|
|
|
if (polymorphic.idType) { // explicit key type
|
|
modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, {type: polymorphic.idType, index: true});
|
|
} else { // try to use the same foreign key type as modelFrom
|
|
modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelFrom.modelName, pkName);
|
|
}
|
|
|
|
modelFrom.dataSource.defineProperty(modelFrom.modelName, discriminator, {type: 'string', index: true});
|
|
} else {
|
|
// relation is not polymorphic
|
|
normalizeRelationAs(params, modelToRef);
|
|
modelTo = lookupModelTo(modelFrom, modelToRef, params);
|
|
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);
|
|
}
|
|
|
|
const definition = modelFrom.relations[relationName] = new RelationDefinition({
|
|
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,
|
|
});
|
|
|
|
// 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() {
|
|
const relation = new BelongsTo(definition, this);
|
|
const relationMethod = relation.related.bind(relation);
|
|
relationMethod.get = relation.get.bind(relation);
|
|
relationMethod.getAsync = function() {
|
|
deprecated(g.f('BelongsTo method "getAsync()" is deprecated, use "get()" instead.'));
|
|
return this.get.apply(this, arguments);
|
|
};
|
|
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;
|
|
}
|
|
bindRelationMethods(relation, relationMethod, definition);
|
|
return relationMethod;
|
|
},
|
|
});
|
|
|
|
// FIXME: [rfeng] Wrap the property into a function for remoting
|
|
// so that it can be accessed as /api/<model>/<id>/<belongsToRelationName>
|
|
// For example, /api/orders/1/customer
|
|
const fn = function() {
|
|
const f = this[relationName];
|
|
f.apply(this, arguments);
|
|
};
|
|
modelFrom.prototype['__get__' + relationName] = fn;
|
|
|
|
return definition;
|
|
};
|
|
|
|
BelongsTo.prototype.create = function(targetModelData, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const self = this;
|
|
const modelTo = this.definition.modelTo;
|
|
const fk = this.definition.keyFrom;
|
|
const pk = this.definition.keyTo;
|
|
const 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) {
|
|
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);
|
|
});
|
|
}
|
|
} else {
|
|
cb && cb(err);
|
|
}
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
BelongsTo.prototype.build = function(targetModelData) {
|
|
const modelTo = this.definition.modelTo;
|
|
this.definition.applyProperties(this.modelInstance, targetModelData || {});
|
|
return new modelTo(targetModelData);
|
|
};
|
|
|
|
BelongsTo.prototype.update = function(targetModelData, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
cb = cb || utils.createPromiseCallback();
|
|
const definition = this.definition;
|
|
const fk = definition.keyTo;
|
|
|
|
this.fetch(options, function(err, inst) {
|
|
if (inst instanceof ModelBaseClass) {
|
|
// Ensures Foreign Key cannot be changed!
|
|
const fkErr = preventFkOverride(inst, targetModelData, fk);
|
|
if (fkErr) return cb(fkErr);
|
|
inst.updateAttributes(targetModelData, options, cb);
|
|
} else {
|
|
cb(new Error(g.f('{{BelongsTo}} relation %s is empty', definition.name)));
|
|
}
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
BelongsTo.prototype.destroy = function(options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
|
|
const definition = this.definition;
|
|
const modelInstance = this.modelInstance;
|
|
const fk = 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 {
|
|
cb(new Error(g.f('{{BelongsTo}} relation %s is empty', definition.name)));
|
|
}
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
* - 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) {
|
|
const self = this;
|
|
const modelFrom = this.definition.modelFrom;
|
|
let modelTo = this.definition.modelTo;
|
|
const pk = this.definition.keyTo;
|
|
const fk = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
let discriminator;
|
|
let scopeQuery = null;
|
|
let 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) {
|
|
// order.customer(condOrRefresh, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
if (!newValue) {
|
|
scopeQuery = condOrRefresh;
|
|
}
|
|
|
|
if (typeof this.definition.polymorphic === 'object') {
|
|
discriminator = this.definition.polymorphic.discriminator;
|
|
}
|
|
|
|
let cachedValue;
|
|
if (!condOrRefresh) {
|
|
cachedValue = self.getCache();
|
|
}
|
|
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) {
|
|
let modelToName = modelInstance[discriminator];
|
|
if (typeof modelToName !== 'string') {
|
|
throw new Error(g.f('{{Polymorphic}} model not found: `%s` not set', discriminator));
|
|
}
|
|
modelToName = modelToName.toLowerCase();
|
|
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
|
|
if (!modelTo) {
|
|
throw new Error(g.f('{{Polymorphic}} model not found: `%s`', modelToName));
|
|
}
|
|
}
|
|
|
|
if (cachedValue === undefined || !(cachedValue instanceof ModelBaseClass)) {
|
|
const query = {where: {}};
|
|
query.where[pk] = modelInstance[fk];
|
|
|
|
if (query.where[pk] === undefined || query.where[pk] === null) {
|
|
// Foreign key is undefined
|
|
return process.nextTick(cb);
|
|
}
|
|
|
|
this.definition.applyScope(modelInstance, query);
|
|
|
|
if (scopeQuery) mergeQuery(query, scopeQuery);
|
|
|
|
if (Array.isArray(query.fields) && query.fields.indexOf(pk) === -1) {
|
|
query.fields.push(pk); // always include the pk
|
|
}
|
|
|
|
modelTo.findOne(query, options, function(err, inst) {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
if (!inst) {
|
|
return cb(null, null);
|
|
}
|
|
// Check if the foreign key matches the primary key
|
|
if (inst[pk] != null && modelInstance[fk] != null &&
|
|
inst[pk].toString() === modelInstance[fk].toString()) {
|
|
self.resetCache(inst);
|
|
cb(null, inst);
|
|
} else {
|
|
err = new Error(g.f('Key mismatch: %s.%s: %s, %s.%s: %s',
|
|
self.definition.modelFrom.modelName, fk, modelInstance[fk],
|
|
modelTo.modelName, pk, inst[pk]));
|
|
err.statusCode = 400;
|
|
cb(err);
|
|
}
|
|
});
|
|
return modelInstance[fk];
|
|
} else {
|
|
cb(null, cachedValue);
|
|
return cachedValue;
|
|
}
|
|
} else if (condOrRefresh === undefined) { // acts as sync getter
|
|
return cachedValue;
|
|
} else { // setter
|
|
modelInstance[fk] = newValue;
|
|
self.resetCache();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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.get = function(options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
cb = cb || utils.createPromiseCallback();
|
|
this.related(true, options, cb);
|
|
return cb.promise;
|
|
};
|
|
|
|
/**
|
|
* 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:
|
|
* ```
|
|
* User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
|
|
* ```
|
|
*
|
|
* @param {Object|String} modelToRef Reference to Model object to which you are
|
|
* creating the relation: model instance, model name, or name of relation to model.
|
|
* @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.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, modelToRef, params) {
|
|
params = params || {};
|
|
normalizeRelationAs(params, modelToRef);
|
|
const modelTo = lookupModelTo(modelFrom, modelToRef, params, true);
|
|
|
|
const models = modelFrom.dataSource.modelBuilder.models;
|
|
|
|
if (!params.through) {
|
|
if (params.polymorphic) throw new Error(g.f('{{Polymorphic}} relations need a through model'));
|
|
|
|
if (params.throughTable) {
|
|
params.through = modelFrom.dataSource.define(params.throughTable);
|
|
} else {
|
|
const name1 = modelFrom.modelName + modelTo.modelName;
|
|
const name2 = modelTo.modelName + modelFrom.modelName;
|
|
params.through = lookupModel(models, name1) || lookupModel(models, name2) ||
|
|
modelFrom.dataSource.define(name1);
|
|
}
|
|
}
|
|
|
|
const options = {as: params.as, through: params.through};
|
|
options.properties = params.properties;
|
|
options.scope = params.scope;
|
|
|
|
// Forward relation options like "disableInclude"
|
|
options.options = params.options;
|
|
|
|
if (params.polymorphic) {
|
|
const relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
|
|
const polymorphic = normalizePolymorphic(params.polymorphic, relationName);
|
|
options.polymorphic = polymorphic; // pass through
|
|
const accessor = params.through.prototype[polymorphic.selector];
|
|
if (typeof accessor !== 'function') { // declare once
|
|
// use the name of the polymorphic selector, not modelTo
|
|
params.through.belongsTo(polymorphic.selector, {polymorphic: true});
|
|
}
|
|
} else {
|
|
params.through.belongsTo(modelFrom);
|
|
}
|
|
|
|
params.through.belongsTo(modelTo);
|
|
|
|
return this.hasMany(modelFrom, modelTo, options);
|
|
};
|
|
|
|
/**
|
|
* 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 {Object|String} modelToRef Reference to Model object to which you are
|
|
* creating the relation: model instance, model name, or name of relation to model.
|
|
* @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.hasOne = function(modelFrom, modelToRef, params) {
|
|
params = params || {};
|
|
normalizeRelationAs(params, modelToRef);
|
|
const modelTo = lookupModelTo(modelFrom, modelToRef, params);
|
|
|
|
const pk = params.primaryKey || modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
|
|
const relationName = params.as || i8n.camelize(modelTo.modelName, true);
|
|
|
|
let fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true);
|
|
let discriminator, polymorphic;
|
|
|
|
if (params.polymorphic) {
|
|
polymorphic = normalizePolymorphic(params.polymorphic, relationName);
|
|
fk = polymorphic.foreignKey;
|
|
discriminator = polymorphic.discriminator;
|
|
if (!params.through) {
|
|
modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, {type: 'string', index: true});
|
|
}
|
|
}
|
|
|
|
const definition = modelFrom.relations[relationName] = new RelationDefinition({
|
|
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,
|
|
});
|
|
|
|
modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName, pk);
|
|
|
|
// 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() {
|
|
const relation = new HasOne(definition, this);
|
|
const relationMethod = relation.related.bind(relation);
|
|
relationMethod.get = relation.get.bind(relation);
|
|
relationMethod.getAsync = function() {
|
|
deprecated(g.f('HasOne method "getAsync()" is deprecated, use "get()" instead.'));
|
|
return this.get.apply(this, arguments);
|
|
};
|
|
relationMethod.create = relation.create.bind(relation);
|
|
relationMethod.build = relation.build.bind(relation);
|
|
relationMethod.update = relation.update.bind(relation);
|
|
relationMethod.destroy = relation.destroy.bind(relation);
|
|
relationMethod._targetClass = definition.modelTo.modelName;
|
|
bindRelationMethods(relation, relationMethod, definition);
|
|
return relationMethod;
|
|
},
|
|
});
|
|
|
|
// 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
|
|
modelFrom.prototype['__get__' + relationName] = function() {
|
|
const f = this[relationName];
|
|
f.apply(this, arguments);
|
|
};
|
|
|
|
modelFrom.prototype['__create__' + relationName] = function() {
|
|
const f = this[relationName].create;
|
|
f.apply(this, arguments);
|
|
};
|
|
|
|
modelFrom.prototype['__update__' + relationName] = function() {
|
|
const f = this[relationName].update;
|
|
f.apply(this, arguments);
|
|
};
|
|
|
|
modelFrom.prototype['__destroy__' + relationName] = function() {
|
|
const f = this[relationName].destroy;
|
|
f.apply(this, arguments);
|
|
};
|
|
|
|
return definition;
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
// customer.profile.create(options, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const self = this;
|
|
const modelTo = this.definition.modelTo;
|
|
const fk = this.definition.keyTo;
|
|
const pk = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
if (typeof targetModelData === 'function' && !cb) {
|
|
cb = targetModelData;
|
|
targetModelData = {};
|
|
}
|
|
targetModelData = targetModelData || {};
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
targetModelData[fk] = modelInstance[pk];
|
|
const query = {where: {}};
|
|
query.where[fk] = targetModelData[fk];
|
|
|
|
this.definition.applyScope(modelInstance, query);
|
|
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 {
|
|
cb && cb(new Error(g.f(
|
|
'{{HasOne}} relation cannot create more than one instance of %s',
|
|
modelTo.modelName
|
|
)));
|
|
}
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
HasOne.prototype.update = function(targetModelData, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.profile.update(data, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
cb = cb || utils.createPromiseCallback();
|
|
const definition = this.definition;
|
|
const fk = this.definition.keyTo;
|
|
this.fetch(function(err, targetModel) {
|
|
if (targetModel instanceof ModelBaseClass) {
|
|
// Ensures Foreign Key cannot be changed!
|
|
const fkErr = preventFkOverride(targetModel, targetModelData, fk);
|
|
if (fkErr) return cb(fkErr);
|
|
targetModel.updateAttributes(targetModelData, options, cb);
|
|
} else {
|
|
cb(new Error(g.f('{{HasOne}} relation %s is empty', definition.name)));
|
|
}
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
HasOne.prototype.destroy = function(options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.profile.destroy(cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
cb = cb || utils.createPromiseCallback();
|
|
const definition = this.definition;
|
|
this.fetch(function(err, targetModel) {
|
|
if (targetModel instanceof ModelBaseClass) {
|
|
targetModel.destroy(options, cb);
|
|
} else {
|
|
cb(new Error(g.f('{{HasOne}} relation %s is empty', definition.name)));
|
|
}
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
// customer.orders.create(data, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const self = this;
|
|
const modelTo = this.definition.modelTo;
|
|
const fk = this.definition.keyTo;
|
|
const pk = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
if (typeof targetModelData === 'function' && !cb) {
|
|
cb = targetModelData;
|
|
targetModelData = {};
|
|
}
|
|
targetModelData = targetModelData || {};
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
const fkAndProps = function(item) {
|
|
item[fk] = modelInstance[pk];
|
|
self.definition.applyProperties(modelInstance, item);
|
|
};
|
|
|
|
const 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));
|
|
cb && cb(err, targetModel);
|
|
} else {
|
|
cb && cb(err);
|
|
}
|
|
});
|
|
return cb.promise;
|
|
};
|
|
/**
|
|
* 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) {
|
|
const modelTo = this.definition.modelTo;
|
|
const pk = this.definition.keyFrom;
|
|
const fk = this.definition.keyTo;
|
|
|
|
targetModelData = targetModelData || {};
|
|
targetModelData[fk] = this.modelInstance[pk];
|
|
|
|
this.definition.applyProperties(this.modelInstance, targetModelData);
|
|
|
|
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
|
|
*
|
|
* @param {Boolean} refresh Reload from the data source
|
|
* @param {Object|Function} params Query parameters
|
|
* @returns {Object}
|
|
*/
|
|
HasOne.prototype.related = function(condOrRefresh, options, cb) {
|
|
const self = this;
|
|
const modelTo = this.definition.modelTo;
|
|
const fk = this.definition.keyTo;
|
|
const pk = this.definition.keyFrom;
|
|
const definition = this.definition;
|
|
const modelInstance = this.modelInstance;
|
|
let 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) {
|
|
// customer.profile(cb)
|
|
cb = condOrRefresh;
|
|
condOrRefresh = false;
|
|
} else if (typeof options === 'function' && cb === undefined) {
|
|
// customer.profile(condOrRefresh, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
|
|
let cachedValue;
|
|
if (!condOrRefresh) {
|
|
cachedValue = self.getCache();
|
|
}
|
|
if (newValue) { // acts as setter
|
|
newValue[fk] = modelInstance[pk];
|
|
self.resetCache(newValue);
|
|
} else if (typeof cb === 'function') { // acts as async getter
|
|
if (cachedValue === undefined) {
|
|
const query = {where: {}};
|
|
query.where[fk] = modelInstance[pk];
|
|
definition.applyScope(modelInstance, query);
|
|
modelTo.findOne(query, options, function(err, inst) {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
if (!inst) {
|
|
return cb(null, null);
|
|
}
|
|
// Check if the foreign key matches the primary key
|
|
if (inst[fk] != null && modelInstance[pk] != null &&
|
|
inst[fk].toString() === modelInstance[pk].toString()) {
|
|
self.resetCache(inst);
|
|
cb(null, inst);
|
|
} else {
|
|
err = new Error(g.f('Key mismatch: %s.%s: %s, %s.%s: %s',
|
|
self.definition.modelFrom.modelName, pk, modelInstance[pk],
|
|
modelTo.modelName, fk, inst[fk]));
|
|
err.statusCode = 400;
|
|
cb(err);
|
|
}
|
|
});
|
|
return modelInstance[pk];
|
|
} else {
|
|
cb(null, cachedValue);
|
|
return cachedValue;
|
|
}
|
|
} else if (condOrRefresh === undefined) { // acts as sync getter
|
|
return cachedValue;
|
|
} else { // setter
|
|
newValue[fk] = modelInstance[pk];
|
|
self.resetCache();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Define a Promise-based method for the hasOne 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 cb is omitted
|
|
*/
|
|
HasOne.prototype.get = function(options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
cb = cb || utils.createPromiseCallback();
|
|
this.related(true, cb);
|
|
return cb.promise;
|
|
};
|
|
|
|
RelationDefinition.embedsOne = function(modelFrom, modelToRef, params) {
|
|
params = params || {};
|
|
normalizeRelationAs(params, modelToRef);
|
|
const modelTo = lookupModelTo(modelFrom, modelToRef, params);
|
|
|
|
const thisClassName = modelFrom.modelName;
|
|
const relationName = params.as || (i8n.camelize(modelTo.modelName, true) + 'Item');
|
|
let propertyName = params.property || i8n.camelize(modelTo.modelName, true);
|
|
const idName = modelTo.dataSource.idName(modelTo.modelName) || 'id';
|
|
|
|
if (relationName === propertyName) {
|
|
propertyName = '_' + propertyName;
|
|
debug('EmbedsOne property cannot be equal to relation name: ' +
|
|
'forcing property %s for relation %s', propertyName, relationName);
|
|
}
|
|
|
|
const 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,
|
|
});
|
|
|
|
const opts = Object.assign(
|
|
params.options && params.options.property ? params.options.property : {},
|
|
{type: modelTo}
|
|
);
|
|
|
|
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);
|
|
};
|
|
}(params.default));
|
|
}
|
|
|
|
modelFrom.dataSource.defineProperty(modelFrom.modelName, propertyName, opts);
|
|
|
|
// validate the embedded instance
|
|
if (definition.options.validate !== false) {
|
|
modelFrom.validate(relationName, function(err) {
|
|
const inst = this[propertyName];
|
|
if (inst instanceof modelTo) {
|
|
if (!inst.isValid()) {
|
|
const first = Object.keys(inst.errors)[0];
|
|
const msg = 'is invalid: `' + first + '` ' + inst.errors[first];
|
|
this.errors.add(relationName, msg, 'invalid');
|
|
err(false);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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() {
|
|
const relation = new EmbedsOne(definition, this);
|
|
const relationMethod = relation.related.bind(relation);
|
|
relationMethod.create = relation.create.bind(relation);
|
|
relationMethod.build = relation.build.bind(relation);
|
|
relationMethod.update = relation.update.bind(relation);
|
|
relationMethod.destroy = relation.destroy.bind(relation);
|
|
relationMethod.value = relation.embeddedValue.bind(relation);
|
|
relationMethod._targetClass = definition.modelTo.modelName;
|
|
bindRelationMethods(relation, relationMethod, definition);
|
|
return relationMethod;
|
|
},
|
|
});
|
|
|
|
// 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() {
|
|
const f = this[relationName];
|
|
f.apply(this, arguments);
|
|
};
|
|
|
|
modelFrom.prototype['__create__' + relationName] = function() {
|
|
const f = this[relationName].create;
|
|
f.apply(this, arguments);
|
|
};
|
|
|
|
modelFrom.prototype['__update__' + relationName] = function() {
|
|
const f = this[relationName].update;
|
|
f.apply(this, arguments);
|
|
};
|
|
|
|
modelFrom.prototype['__destroy__' + relationName] = function() {
|
|
const f = this[relationName].destroy;
|
|
f.apply(this, arguments);
|
|
};
|
|
|
|
return definition;
|
|
};
|
|
|
|
EmbedsOne.prototype.related = function(condOrRefresh, options, cb) {
|
|
const modelTo = this.definition.modelTo;
|
|
const modelInstance = this.modelInstance;
|
|
const propertyName = this.definition.keyFrom;
|
|
let 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) {
|
|
// order.customer(condOrRefresh, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
|
|
if (newValue) { // acts as setter
|
|
if (newValue instanceof modelTo) {
|
|
this.definition.applyProperties(modelInstance, newValue);
|
|
modelInstance.setAttribute(propertyName, newValue);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const embeddedInstance = this.embeddedValue();
|
|
|
|
if (embeddedInstance) {
|
|
embeddedInstance.__persisted = true;
|
|
}
|
|
|
|
if (typeof cb === 'function') { // acts as async getter
|
|
process.nextTick(function() {
|
|
cb(null, embeddedInstance);
|
|
});
|
|
} else if (condOrRefresh === undefined) { // acts as sync getter
|
|
return embeddedInstance;
|
|
}
|
|
};
|
|
|
|
EmbedsOne.prototype.prepareEmbeddedInstance = function(inst) {
|
|
if (inst && inst.triggerParent !== 'function') {
|
|
const self = this;
|
|
const propertyName = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
if (this.definition.options.persistent) {
|
|
const pk = this.definition.keyTo;
|
|
inst.__persisted = !!inst[pk];
|
|
} else {
|
|
inst.__persisted = true;
|
|
}
|
|
inst.triggerParent = function(actionName, callback) {
|
|
if (actionName === 'save') {
|
|
const embeddedValue = self.embeddedValue();
|
|
modelInstance.updateAttribute(propertyName,
|
|
embeddedValue, function(err, modelInst) {
|
|
callback(err, err ? null : modelInst);
|
|
});
|
|
} else if (actionName === 'destroy') {
|
|
modelInstance.unsetAttribute(propertyName, true);
|
|
// cannot delete property completely the way save works. operator $unset needed like mongo
|
|
modelInstance.save(function(err, modelInst) {
|
|
callback(err, modelInst);
|
|
});
|
|
} else {
|
|
process.nextTick(callback);
|
|
}
|
|
};
|
|
const originalTrigger = inst.trigger;
|
|
inst.trigger = function(actionName, work, data, callback) {
|
|
if (typeof work === 'function') {
|
|
const 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);
|
|
};
|
|
}
|
|
};
|
|
|
|
EmbedsOne.prototype.embeddedValue = function(modelInstance) {
|
|
modelInstance = modelInstance || this.modelInstance;
|
|
const embeddedValue = modelInstance[this.definition.keyFrom];
|
|
this.prepareEmbeddedInstance(embeddedValue);
|
|
return embeddedValue;
|
|
};
|
|
|
|
EmbedsOne.prototype.create = function(targetModelData, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// order.customer.create(data, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const modelTo = this.definition.modelTo;
|
|
const propertyName = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
if (typeof targetModelData === 'function' && !cb) {
|
|
cb = targetModelData;
|
|
targetModelData = {};
|
|
}
|
|
|
|
targetModelData = targetModelData || {};
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
const inst = this.callScopeMethod('build', targetModelData);
|
|
|
|
const 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);
|
|
});
|
|
} else {
|
|
const context = {
|
|
Model: modelTo,
|
|
instance: inst,
|
|
options: options || {},
|
|
hookState: {},
|
|
};
|
|
modelTo.notifyObserversOf('before save', context, function(err) {
|
|
if (err) {
|
|
return process.nextTick(function() {
|
|
cb(err);
|
|
});
|
|
}
|
|
|
|
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);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
return cb.promise;
|
|
};
|
|
|
|
EmbedsOne.prototype.build = function(targetModelData) {
|
|
const modelTo = this.definition.modelTo;
|
|
const modelInstance = this.modelInstance;
|
|
const propertyName = this.definition.keyFrom;
|
|
const forceId = this.definition.options.forceId;
|
|
const persistent = this.definition.options.persistent;
|
|
const connector = modelTo.dataSource.connector;
|
|
|
|
targetModelData = targetModelData || {};
|
|
|
|
this.definition.applyProperties(modelInstance, targetModelData);
|
|
|
|
const pk = this.definition.keyTo;
|
|
const pkProp = modelTo.definition.properties[pk];
|
|
|
|
let assignId = (forceId || targetModelData[pk] === undefined);
|
|
assignId = assignId && !persistent && (pkProp && pkProp.generated);
|
|
|
|
if (assignId && typeof connector.generateId === 'function') {
|
|
const id = connector.generateId(modelTo.modelName, targetModelData, pk);
|
|
targetModelData[pk] = id;
|
|
}
|
|
|
|
const embeddedInstance = new modelTo(targetModelData);
|
|
modelInstance[propertyName] = embeddedInstance;
|
|
|
|
this.prepareEmbeddedInstance(embeddedInstance);
|
|
|
|
return embeddedInstance;
|
|
};
|
|
|
|
EmbedsOne.prototype.update = function(targetModelData, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// order.customer.update(data, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
|
|
const modelTo = this.definition.modelTo;
|
|
const modelInstance = this.modelInstance;
|
|
const propertyName = this.definition.keyFrom;
|
|
|
|
const isInst = targetModelData instanceof ModelBaseClass;
|
|
const data = isInst ? targetModelData.toObject() : targetModelData;
|
|
|
|
const embeddedInstance = this.embeddedValue();
|
|
if (embeddedInstance instanceof modelTo) {
|
|
cb = cb || utils.createPromiseCallback();
|
|
const hookState = {};
|
|
let 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);
|
|
});
|
|
});
|
|
});
|
|
} else if (!embeddedInstance && cb) {
|
|
return this.callScopeMethod('create', data, cb);
|
|
} else if (!embeddedInstance) {
|
|
return this.callScopeMethod('build', data);
|
|
}
|
|
return cb.promise;
|
|
};
|
|
|
|
EmbedsOne.prototype.destroy = function(options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// order.customer.destroy(cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
cb = cb || utils.createPromiseCallback();
|
|
const modelTo = this.definition.modelTo;
|
|
const modelInstance = this.modelInstance;
|
|
const propertyName = this.definition.keyFrom;
|
|
const embeddedInstance = modelInstance[propertyName];
|
|
|
|
if (!embeddedInstance) {
|
|
cb();
|
|
return cb.promise;
|
|
}
|
|
|
|
modelInstance.unsetAttribute(propertyName, true);
|
|
|
|
const 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;
|
|
};
|
|
|
|
RelationDefinition.embedsMany = function embedsMany(modelFrom, modelToRef, params) {
|
|
params = params || {};
|
|
normalizeRelationAs(params, modelToRef);
|
|
const modelTo = lookupModelTo(modelFrom, modelToRef, params, true);
|
|
|
|
const thisClassName = modelFrom.modelName;
|
|
const relationName = params.as || (i8n.camelize(modelTo.modelName, true) + 'List');
|
|
let propertyName = params.property || i8n.camelize(modelTo.pluralModelName, true);
|
|
const 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);
|
|
}
|
|
|
|
const definition = modelFrom.relations[relationName] = new RelationDefinition({
|
|
name: relationName,
|
|
type: RelationTypes.embedsMany,
|
|
modelFrom: modelFrom,
|
|
keyFrom: propertyName,
|
|
keyTo: idName,
|
|
modelTo: modelTo,
|
|
multiple: true,
|
|
properties: params.properties,
|
|
scope: params.scope,
|
|
options: params.options,
|
|
embed: true,
|
|
});
|
|
|
|
const opts = Object.assign(
|
|
params.options && params.options.property ? params.options.property : {},
|
|
params.options && params.options.omitDefaultEmbeddedItem ? {type: [modelTo]} :
|
|
{
|
|
type: [modelTo],
|
|
default: function() { return []; },
|
|
}
|
|
);
|
|
|
|
modelFrom.dataSource.defineProperty(modelFrom.modelName, propertyName, opts);
|
|
|
|
if (typeof modelTo.dataSource.connector.generateId !== 'function') {
|
|
modelFrom.validate(propertyName, function(err) {
|
|
const self = this;
|
|
const embeddedList = this[propertyName] || [];
|
|
let hasErrors = false;
|
|
embeddedList.forEach(function(item, idx) {
|
|
if (item instanceof modelTo && item[idName] == undefined) {
|
|
hasErrors = true;
|
|
let msg = 'contains invalid item at index `' + idx + '`:';
|
|
msg += ' `' + idName + '` is blank';
|
|
self.errors.add(propertyName, msg, 'invalid');
|
|
}
|
|
});
|
|
if (hasErrors) err(false);
|
|
});
|
|
}
|
|
|
|
if (!params.polymorphic) {
|
|
modelFrom.validate(propertyName, function(err) {
|
|
const embeddedList = this[propertyName] || [];
|
|
const ids = embeddedList.map(function(m) { return m[idName] && m[idName].toString(); }); // mongodb
|
|
if (idsHaveDuplicates(ids)) {
|
|
this.errors.add(propertyName, 'contains duplicate `' + idName + '`', 'uniqueness');
|
|
err(false);
|
|
}
|
|
}, {code: 'uniqueness'});
|
|
}
|
|
|
|
// validate all embedded items
|
|
if (definition.options.validate !== false) {
|
|
modelFrom.validate(propertyName, function(err) {
|
|
const self = this;
|
|
const embeddedList = this[propertyName] || [];
|
|
let hasErrors = false;
|
|
embeddedList.forEach(function(item, idx) {
|
|
if (item instanceof modelTo) {
|
|
if (!item.isValid()) {
|
|
hasErrors = true;
|
|
const id = item[idName];
|
|
const first = Object.keys(item.errors)[0];
|
|
let msg = id ?
|
|
'contains invalid item: `' + id + '`' :
|
|
'contains invalid item at index `' + idx + '`';
|
|
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);
|
|
});
|
|
}
|
|
|
|
const 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'),
|
|
};
|
|
|
|
const findByIdFunc = scopeMethods.findById;
|
|
modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
|
|
|
|
const destroyByIdFunc = scopeMethods.destroy;
|
|
modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
|
|
|
|
const updateByIdFunc = scopeMethods.updateById;
|
|
modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
|
|
|
|
const addFunc = scopeMethods.add;
|
|
modelFrom.prototype['__link__' + relationName] = addFunc;
|
|
|
|
const removeFunc = scopeMethods.remove;
|
|
modelFrom.prototype['__unlink__' + relationName] = removeFunc;
|
|
|
|
scopeMethods.create = scopeMethod(definition, 'create');
|
|
scopeMethods.build = scopeMethod(definition, 'build');
|
|
|
|
scopeMethods.related = scopeMethod(definition, 'related'); // bound to definition
|
|
|
|
if (!definition.options.persistent) {
|
|
scopeMethods.destroyAll = scopeMethod(definition, 'destroyAll');
|
|
}
|
|
|
|
const customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
|
|
|
|
for (let i = 0; i < customMethods.length; i++) {
|
|
const methodName = customMethods[i];
|
|
const method = scopeMethods[methodName];
|
|
if (typeof method === 'function' && method.shared === true) {
|
|
modelFrom.prototype['__' + methodName + '__' + relationName] = method;
|
|
}
|
|
}
|
|
|
|
// Mix the property and scoped methods into the prototype class
|
|
const scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function() {
|
|
return {};
|
|
}, scopeMethods, definition.options);
|
|
|
|
scopeDefinition.related = scopeMethods.related;
|
|
|
|
return definition;
|
|
};
|
|
|
|
EmbedsMany.prototype.prepareEmbeddedInstance = function(inst) {
|
|
if (inst && inst.triggerParent !== 'function') {
|
|
const self = this;
|
|
const propertyName = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
if (this.definition.options.persistent) {
|
|
const pk = this.definition.keyTo;
|
|
inst.__persisted = !!inst[pk];
|
|
} else {
|
|
inst.__persisted = true;
|
|
}
|
|
inst.triggerParent = function(actionName, callback) {
|
|
if (actionName === 'save' || actionName === 'destroy') {
|
|
const embeddedList = self.embeddedList();
|
|
if (actionName === 'destroy') {
|
|
const 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);
|
|
}
|
|
};
|
|
const originalTrigger = inst.trigger;
|
|
inst.trigger = function(actionName, work, data, callback) {
|
|
if (typeof work === 'function') {
|
|
const 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;
|
|
const embeddedList = modelInstance[this.definition.keyFrom] || [];
|
|
embeddedList.forEach(this.prepareEmbeddedInstance.bind(this));
|
|
return embeddedList;
|
|
};
|
|
|
|
EmbedsMany.prototype.related = function(receiver, scopeParams, condOrRefresh, options, cb) {
|
|
const modelTo = this.definition.modelTo;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
let actualCond = {};
|
|
|
|
if (typeof condOrRefresh === 'function' &&
|
|
options === undefined && cb === undefined) {
|
|
// customer.emails(receiver, scopeParams, cb)
|
|
cb = condOrRefresh;
|
|
condOrRefresh = false;
|
|
} else if (typeof options === 'function' && cb === undefined) {
|
|
// customer.emails(receiver, scopeParams, condOrRefresh, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
|
|
if (typeof condOrRefresh === 'object') {
|
|
actualCond = condOrRefresh;
|
|
}
|
|
|
|
let embeddedList = this.embeddedList(receiver);
|
|
|
|
this.definition.applyScope(receiver, actualCond);
|
|
|
|
const params = mergeQuery(actualCond, scopeParams);
|
|
|
|
if (params.where && Object.keys(params.where).length > 0) { // TODO [fabien] Support order/sorting
|
|
embeddedList = embeddedList ? embeddedList.filter(applyFilter(params)) : embeddedList;
|
|
}
|
|
|
|
const returnRelated = function(list) {
|
|
if (params.include) {
|
|
modelTo.include(list, params.include, options, cb);
|
|
} else {
|
|
process.nextTick(function() { cb(null, list); });
|
|
}
|
|
};
|
|
|
|
returnRelated(embeddedList);
|
|
};
|
|
|
|
EmbedsMany.prototype.findById = function(fkId, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// order.emails(fkId, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const pk = this.definition.keyTo;
|
|
const modelTo = this.definition.modelTo;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
const embeddedList = this.embeddedList();
|
|
|
|
const find = function(id) {
|
|
for (let i = 0; i < embeddedList.length; i++) {
|
|
const item = embeddedList[i];
|
|
if (idEquals(item[pk], id)) return item;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
let item = find(fkId.toString()); // in case of explicit id
|
|
item = (item instanceof modelTo) ? item : null;
|
|
|
|
if (typeof cb === 'function') {
|
|
process.nextTick(function() {
|
|
cb(null, item);
|
|
});
|
|
}
|
|
|
|
return item; // sync
|
|
};
|
|
|
|
EmbedsMany.prototype.exists = function(fkId, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.emails.exists(fkId, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const modelTo = this.definition.modelTo;
|
|
const inst = this.findById(fkId, options, function(err, inst) {
|
|
if (cb) cb(err, inst instanceof modelTo);
|
|
});
|
|
return inst instanceof modelTo; // sync
|
|
};
|
|
|
|
EmbedsMany.prototype.updateById = function(fkId, data, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.emails.updateById(fkId, data, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
if (typeof data === 'function') {
|
|
// customer.emails.updateById(fkId, cb)
|
|
cb = data;
|
|
data = {};
|
|
}
|
|
options = options || {};
|
|
|
|
const modelTo = this.definition.modelTo;
|
|
const propertyName = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
const embeddedList = this.embeddedList();
|
|
|
|
const inst = this.findById(fkId);
|
|
|
|
if (inst instanceof modelTo) {
|
|
const hookState = {};
|
|
let 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);
|
|
|
|
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);
|
|
});
|
|
}
|
|
});
|
|
} else if (typeof cb === 'function') {
|
|
process.nextTick(function() {
|
|
cb(null, null); // not found
|
|
});
|
|
}
|
|
return inst; // sync
|
|
};
|
|
|
|
EmbedsMany.prototype.destroyById = function(fkId, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.emails.destroyById(fkId, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const modelTo = this.definition.modelTo;
|
|
const propertyName = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
const embeddedList = this.embeddedList();
|
|
|
|
const inst = (fkId instanceof modelTo) ? fkId : this.findById(fkId);
|
|
|
|
if (inst instanceof modelTo) {
|
|
const context = {
|
|
Model: modelTo,
|
|
instance: inst,
|
|
options: options || {},
|
|
hookState: {},
|
|
};
|
|
modelTo.notifyObserversOf('before delete', context, function(err) {
|
|
if (err) return cb(err);
|
|
|
|
const index = embeddedList.indexOf(inst);
|
|
if (index > -1) embeddedList.splice(index, 1);
|
|
if (typeof cb !== 'function') return;
|
|
modelInstance.updateAttribute(propertyName,
|
|
embeddedList, context.options, function(err) {
|
|
if (err) return cb(err);
|
|
modelTo.notifyObserversOf('after delete', context, function(err) {
|
|
cb(err);
|
|
});
|
|
});
|
|
});
|
|
} else if (typeof cb === 'function') {
|
|
process.nextTick(cb); // not found
|
|
}
|
|
return inst; // sync
|
|
};
|
|
|
|
EmbedsMany.prototype.destroyAll = function(where, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.emails.destroyAll(where, cb);
|
|
cb = options;
|
|
options = {};
|
|
} else if (typeof where === 'function' &&
|
|
options === undefined && cb === undefined) {
|
|
// customer.emails.destroyAll(cb);
|
|
cb = where;
|
|
where = {};
|
|
}
|
|
const propertyName = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
let embeddedList = this.embeddedList();
|
|
|
|
if (where && Object.keys(where).length > 0) {
|
|
const filter = applyFilter({where: where});
|
|
const reject = function(v) { return !filter(v); };
|
|
embeddedList = embeddedList ? embeddedList.filter(reject) : embeddedList;
|
|
} else {
|
|
embeddedList = [];
|
|
}
|
|
|
|
if (typeof cb === 'function') {
|
|
modelInstance.updateAttribute(propertyName,
|
|
embeddedList, options || {}, function(err) {
|
|
cb(err);
|
|
});
|
|
} else {
|
|
modelInstance.setAttribute(propertyName, embeddedList, options || {});
|
|
}
|
|
};
|
|
|
|
EmbedsMany.prototype.get = EmbedsMany.prototype.findById;
|
|
EmbedsMany.prototype.set = EmbedsMany.prototype.updateById;
|
|
EmbedsMany.prototype.unset = EmbedsMany.prototype.destroyById;
|
|
|
|
EmbedsMany.prototype.at = function(index, cb) {
|
|
const modelTo = this.definition.modelTo;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
const embeddedList = this.embeddedList();
|
|
|
|
let 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) {
|
|
const pk = this.definition.keyTo;
|
|
const modelTo = this.definition.modelTo;
|
|
const propertyName = this.definition.keyFrom;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.emails.create(cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
|
|
if (typeof targetModelData === 'function' && !cb) {
|
|
cb = targetModelData;
|
|
targetModelData = {};
|
|
}
|
|
targetModelData = targetModelData || {};
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
const inst = this.callScopeMethod('build', targetModelData);
|
|
const embeddedList = this.embeddedList();
|
|
|
|
const 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);
|
|
});
|
|
} else {
|
|
const err = inst.isValid() ? null : new ValidationError(inst);
|
|
if (err) {
|
|
process.nextTick(function() {
|
|
cb(err);
|
|
});
|
|
} else {
|
|
const 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);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
return cb.promise;
|
|
};
|
|
|
|
EmbedsMany.prototype.build = function(targetModelData) {
|
|
const modelTo = this.definition.modelTo;
|
|
const modelInstance = this.modelInstance;
|
|
const forceId = this.definition.options.forceId;
|
|
const persistent = this.definition.options.persistent;
|
|
const propertyName = this.definition.keyFrom;
|
|
const connector = modelTo.dataSource.connector;
|
|
|
|
const pk = this.definition.keyTo;
|
|
const pkProp = modelTo.definition.properties[pk];
|
|
const pkType = pkProp && pkProp.type;
|
|
|
|
const embeddedList = this.embeddedList();
|
|
|
|
targetModelData = targetModelData || {};
|
|
|
|
let assignId = (forceId || targetModelData[pk] === undefined);
|
|
assignId = assignId && !persistent;
|
|
|
|
if (assignId && pkType === Number) {
|
|
const ids = embeddedList.map(function(m) {
|
|
return (typeof m[pk] === 'number' ? m[pk] : 0);
|
|
});
|
|
if (ids.length > 0) {
|
|
targetModelData[pk] = Math.max.apply(null, ids) + 1;
|
|
} else {
|
|
targetModelData[pk] = 1;
|
|
}
|
|
} else if (assignId && typeof connector.generateId === 'function') {
|
|
const id = connector.generateId(modelTo.modelName, targetModelData, pk);
|
|
targetModelData[pk] = id;
|
|
}
|
|
|
|
this.definition.applyProperties(modelInstance, targetModelData);
|
|
|
|
const inst = new modelTo(targetModelData);
|
|
|
|
if (this.definition.options.prepend) {
|
|
embeddedList.unshift(inst);
|
|
modelInstance[propertyName] = embeddedList;
|
|
} else {
|
|
embeddedList.push(inst);
|
|
modelInstance[propertyName] = embeddedList;
|
|
}
|
|
|
|
this.prepareEmbeddedInstance(inst);
|
|
|
|
return inst;
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
// customer.emails.add(acInst, data, cb)
|
|
cb = options;
|
|
options = {};
|
|
} else if (typeof data === 'function' &&
|
|
options === undefined && cb === undefined) {
|
|
// customer.emails.add(acInst, cb)
|
|
cb = data;
|
|
data = {};
|
|
}
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
const self = this;
|
|
const definition = this.definition;
|
|
const modelTo = this.definition.modelTo;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
const defOpts = definition.options;
|
|
const belongsTo = defOpts.belongsTo && modelTo.relations[defOpts.belongsTo];
|
|
|
|
if (!belongsTo) {
|
|
throw new Error('Invalid reference: ' + defOpts.belongsTo || '(none)');
|
|
}
|
|
|
|
const fk2 = belongsTo.keyTo;
|
|
const pk2 = belongsTo.modelTo.definition.idName() || 'id';
|
|
|
|
const query = {};
|
|
|
|
query[fk2] = (acInst instanceof belongsTo.modelTo) ? acInst[pk2] : acInst;
|
|
|
|
const filter = {where: query};
|
|
|
|
belongsTo.applyScope(modelInstance, filter);
|
|
|
|
belongsTo.modelTo.findOne(filter, options, function(err, ref) {
|
|
if (ref instanceof belongsTo.modelTo) {
|
|
const inst = self.build(data || {});
|
|
inst[defOpts.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) {
|
|
// customer.emails.remove(acInst, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const self = this;
|
|
const definition = this.definition;
|
|
const modelTo = this.definition.modelTo;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
const defOpts = definition.options;
|
|
const belongsTo = defOpts.belongsTo && modelTo.relations[defOpts.belongsTo];
|
|
|
|
if (!belongsTo) {
|
|
throw new Error('Invalid reference: ' + defOpts.belongsTo || '(none)');
|
|
}
|
|
|
|
const fk2 = belongsTo.keyTo;
|
|
const pk2 = belongsTo.modelTo.definition.idName() || 'id';
|
|
|
|
const query = {};
|
|
|
|
query[fk2] = (acInst instanceof belongsTo.modelTo) ? acInst[pk2] : acInst;
|
|
|
|
const 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) {
|
|
cb(err);
|
|
});
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
RelationDefinition.referencesMany = function referencesMany(modelFrom, modelToRef, params) {
|
|
params = params || {};
|
|
normalizeRelationAs(params, modelToRef);
|
|
const modelTo = lookupModelTo(modelFrom, modelToRef, params, true);
|
|
|
|
const thisClassName = modelFrom.modelName;
|
|
const relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
|
|
const fk = params.foreignKey || i8n.camelize(modelTo.modelName + '_ids', true);
|
|
const idName = modelTo.dataSource.idName(modelTo.modelName) || 'id';
|
|
const idType = modelTo.definition.properties[idName].type;
|
|
|
|
const 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,
|
|
});
|
|
|
|
modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, {
|
|
type: [idType], default: function() { return []; },
|
|
});
|
|
|
|
modelFrom.validate(relationName, function(err) {
|
|
const ids = this[fk] || [];
|
|
if (idsHaveDuplicates(ids)) {
|
|
const msg = 'contains duplicate `' + modelTo.modelName + '` instance';
|
|
this.errors.add(relationName, msg, 'uniqueness');
|
|
err(false);
|
|
}
|
|
}, {code: 'uniqueness'});
|
|
|
|
const 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'),
|
|
};
|
|
|
|
const findByIdFunc = scopeMethods.findById;
|
|
modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
|
|
|
|
const destroyByIdFunc = scopeMethods.destroy;
|
|
modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
|
|
|
|
const updateByIdFunc = scopeMethods.updateById;
|
|
modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
|
|
|
|
const addFunc = scopeMethods.add;
|
|
modelFrom.prototype['__link__' + relationName] = addFunc;
|
|
|
|
const removeFunc = scopeMethods.remove;
|
|
modelFrom.prototype['__unlink__' + relationName] = removeFunc;
|
|
|
|
scopeMethods.create = scopeMethod(definition, 'create');
|
|
scopeMethods.build = scopeMethod(definition, 'build');
|
|
|
|
scopeMethods.related = scopeMethod(definition, 'related');
|
|
|
|
const customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
|
|
|
|
for (let i = 0; i < customMethods.length; i++) {
|
|
const methodName = customMethods[i];
|
|
const method = scopeMethods[methodName];
|
|
if (typeof method === 'function' && method.shared === true) {
|
|
modelFrom.prototype['__' + methodName + '__' + relationName] = method;
|
|
}
|
|
}
|
|
|
|
// Mix the property and scoped methods into the prototype class
|
|
const scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function() {
|
|
return {};
|
|
}, scopeMethods, definition.options);
|
|
|
|
scopeDefinition.related = scopeMethods.related; // bound to definition
|
|
|
|
return definition;
|
|
};
|
|
|
|
ReferencesMany.prototype.related = function(receiver, scopeParams, condOrRefresh, options, cb) {
|
|
const fk = this.definition.keyFrom;
|
|
const modelTo = this.definition.modelTo;
|
|
const relationName = this.definition.name;
|
|
const modelInstance = this.modelInstance;
|
|
const self = receiver;
|
|
|
|
let actualCond = {};
|
|
let actualRefresh = false;
|
|
|
|
if (typeof condOrRefresh === 'function' &&
|
|
options === undefined && cb === undefined) {
|
|
// customer.orders(receiver, scopeParams, cb)
|
|
cb = condOrRefresh;
|
|
condOrRefresh = undefined;
|
|
} else if (typeof options === 'function' && cb === undefined) {
|
|
// customer.orders(receiver, scopeParams, condOrRefresh, cb)
|
|
cb = options;
|
|
options = {};
|
|
if (typeof condOrRefresh === 'boolean') {
|
|
actualRefresh = condOrRefresh;
|
|
condOrRefresh = {};
|
|
} else {
|
|
actualRefresh = true;
|
|
}
|
|
}
|
|
actualCond = condOrRefresh || {};
|
|
|
|
const ids = self[fk] || [];
|
|
|
|
this.definition.applyScope(modelInstance, actualCond);
|
|
|
|
const params = mergeQuery(actualCond, scopeParams);
|
|
return modelTo.findByIds(ids, params, options, cb);
|
|
};
|
|
|
|
ReferencesMany.prototype.findById = function(fkId, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.orders.findById(fkId, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const modelTo = this.definition.modelTo;
|
|
const modelFrom = this.definition.modelFrom;
|
|
const relationName = this.definition.name;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
const pk = this.definition.keyTo;
|
|
const fk = this.definition.keyFrom;
|
|
|
|
if (typeof fkId === 'object') {
|
|
fkId = fkId.toString(); // mongodb
|
|
}
|
|
|
|
const ids = modelInstance[fk] || [];
|
|
|
|
const filter = {};
|
|
|
|
this.definition.applyScope(modelInstance, filter);
|
|
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
modelTo.findByIds([fkId], filter, options, function(err, instances) {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
|
|
const inst = instances[0];
|
|
if (!inst) {
|
|
err = new Error(g.f('No instance with {{id}} %s found for %s', fkId, modelTo.modelName));
|
|
err.statusCode = 404;
|
|
return cb(err);
|
|
}
|
|
|
|
// Check if the foreign key is amongst the ids
|
|
if (utils.findIndexOf(ids, inst[pk], idEquals) > -1) {
|
|
cb(null, inst);
|
|
} else {
|
|
err = new Error(g.f('Key mismatch: %s.%s: %s, %s.%s: %s',
|
|
modelFrom.modelName, fk, modelInstance[fk],
|
|
modelTo.modelName, pk, inst[pk]));
|
|
err.statusCode = 400;
|
|
cb(err);
|
|
}
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
ReferencesMany.prototype.exists = function(fkId, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.orders.exists(fkId, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const fk = this.definition.keyFrom;
|
|
const ids = this.modelInstance[fk] || [];
|
|
|
|
cb = cb || utils.createPromiseCallback();
|
|
process.nextTick(function() { cb(null, utils.findIndexOf(ids, fkId, idEquals) > -1); });
|
|
return cb.promise;
|
|
};
|
|
|
|
ReferencesMany.prototype.updateById = function(fkId, data, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.orders.updateById(fkId, data, cb)
|
|
cb = options;
|
|
options = {};
|
|
} else if (typeof data === 'function' &&
|
|
options === undefined && cb === undefined) {
|
|
// customer.orders.updateById(fkId, cb)
|
|
cb = data;
|
|
data = {};
|
|
}
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
this.findById(fkId, options, function(err, inst) {
|
|
if (err) return cb(err);
|
|
inst.updateAttributes(data, options, cb);
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
ReferencesMany.prototype.destroyById = function(fkId, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.orders.destroyById(fkId, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const self = this;
|
|
cb = cb || utils.createPromiseCallback();
|
|
this.findById(fkId, function(err, inst) {
|
|
if (err) return cb(err);
|
|
self.remove(inst, function(err, ids) {
|
|
inst.destroy(cb);
|
|
});
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
ReferencesMany.prototype.at = function(index, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.orders.at(index, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const fk = this.definition.keyFrom;
|
|
const ids = this.modelInstance[fk] || [];
|
|
cb = cb || utils.createPromiseCallback();
|
|
this.findById(ids[index], options, cb);
|
|
return cb.promise;
|
|
};
|
|
|
|
ReferencesMany.prototype.create = function(targetModelData, options, cb) {
|
|
if (typeof options === 'function' && cb === undefined) {
|
|
// customer.orders.create(data, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const definition = this.definition;
|
|
const modelTo = this.definition.modelTo;
|
|
const relationName = this.definition.name;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
const pk = this.definition.keyTo;
|
|
const fk = this.definition.keyFrom;
|
|
|
|
if (typeof targetModelData === 'function' && !cb) {
|
|
cb = targetModelData;
|
|
targetModelData = {};
|
|
}
|
|
targetModelData = targetModelData || {};
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
const ids = modelInstance[fk] || [];
|
|
|
|
const inst = this.callScopeMethod('build', targetModelData);
|
|
|
|
inst.save(options, function(err, inst) {
|
|
if (err) return cb(err, inst);
|
|
|
|
let id = inst[pk];
|
|
|
|
if (typeof id === 'object') {
|
|
id = id.toString(); // mongodb
|
|
}
|
|
|
|
if (definition.options.prepend) {
|
|
ids.unshift(id);
|
|
} else {
|
|
ids.push(id);
|
|
}
|
|
|
|
modelInstance.updateAttribute(fk,
|
|
ids, options, function(err, modelInst) {
|
|
cb(err, inst);
|
|
});
|
|
});
|
|
return cb.promise;
|
|
};
|
|
|
|
ReferencesMany.prototype.build = function(targetModelData) {
|
|
const modelTo = this.definition.modelTo;
|
|
targetModelData = targetModelData || {};
|
|
|
|
this.definition.applyProperties(this.modelInstance, targetModelData);
|
|
|
|
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) {
|
|
// customer.orders.add(acInst, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const self = this;
|
|
const definition = this.definition;
|
|
const modelTo = this.definition.modelTo;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
const pk = this.definition.keyTo;
|
|
const fk = this.definition.keyFrom;
|
|
|
|
const insert = function(inst, done) {
|
|
let id = inst[pk];
|
|
|
|
if (typeof id === 'object') {
|
|
id = id.toString(); // mongodb
|
|
}
|
|
|
|
const ids = modelInstance[fk] || [];
|
|
|
|
if (definition.options.prepend) {
|
|
ids.unshift(id);
|
|
} else {
|
|
ids.push(id);
|
|
}
|
|
|
|
modelInstance.updateAttribute(fk, ids, options, function(err) {
|
|
done(err, err ? null : inst);
|
|
});
|
|
};
|
|
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
if (acInst instanceof modelTo) {
|
|
insert(acInst, cb);
|
|
} else {
|
|
const filter = {where: {}};
|
|
filter.where[pk] = acInst;
|
|
|
|
definition.applyScope(modelInstance, filter);
|
|
|
|
modelTo.findOne(filter, options, function(err, inst) {
|
|
if (err || !inst) return cb(err, null);
|
|
insert(inst, cb);
|
|
});
|
|
}
|
|
return cb.promise;
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
// customer.orders.remove(acInst, cb)
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
const definition = this.definition;
|
|
const modelInstance = this.modelInstance;
|
|
|
|
const pk = this.definition.keyTo;
|
|
const fk = this.definition.keyFrom;
|
|
|
|
const ids = modelInstance[fk] || [];
|
|
|
|
const id = (acInst instanceof definition.modelTo) ? acInst[pk] : acInst;
|
|
|
|
cb = cb || utils.createPromiseCallback();
|
|
|
|
const index = utils.findIndexOf(ids, id, idEquals);
|
|
if (index > -1) {
|
|
ids.splice(index, 1);
|
|
modelInstance.updateAttribute(fk, ids, options, function(err, inst) {
|
|
cb(err, inst[fk] || []);
|
|
});
|
|
} else {
|
|
process.nextTick(function() { cb(null, ids); });
|
|
}
|
|
return cb.promise;
|
|
};
|