UNPKG

110 kBJavaScriptView Raw
1// Copyright IBM Corp. 2014,2019. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6'use strict';
7
8/*!
9 * Dependencies
10 */
11const assert = require('assert');
12const util = require('util');
13const async = require('async');
14const utils = require('./utils');
15const i8n = require('inflection');
16const defineScope = require('./scope.js').defineScope;
17const g = require('strong-globalize')();
18const mergeQuery = utils.mergeQuery;
19const idEquals = utils.idEquals;
20const idsHaveDuplicates = utils.idsHaveDuplicates;
21const ModelBaseClass = require('./model.js');
22const applyFilter = require('./connectors/memory').applyFilter;
23const ValidationError = require('./validations.js').ValidationError;
24const deprecated = require('depd')('loopback-datasource-juggler');
25const debug = require('debug')('loopback:relations');
26
27const RelationTypes = {
28 belongsTo: 'belongsTo',
29 hasMany: 'hasMany',
30 hasOne: 'hasOne',
31 hasAndBelongsToMany: 'hasAndBelongsToMany',
32 referencesMany: 'referencesMany',
33 embedsOne: 'embedsOne',
34 embedsMany: 'embedsMany',
35};
36
37const RelationClasses = {
38 belongsTo: BelongsTo,
39 hasMany: HasMany,
40 hasManyThrough: HasManyThrough,
41 hasOne: HasOne,
42 hasAndBelongsToMany: HasAndBelongsToMany,
43 referencesMany: ReferencesMany,
44 embedsOne: EmbedsOne,
45 embedsMany: EmbedsMany,
46};
47
48exports.Relation = Relation;
49exports.RelationDefinition = RelationDefinition;
50
51exports.RelationTypes = RelationTypes;
52exports.RelationClasses = RelationClasses;
53
54exports.HasMany = HasMany;
55exports.HasManyThrough = HasManyThrough;
56exports.HasOne = HasOne;
57exports.HasAndBelongsToMany = HasAndBelongsToMany;
58exports.BelongsTo = BelongsTo;
59exports.ReferencesMany = ReferencesMany;
60exports.EmbedsOne = EmbedsOne;
61exports.EmbedsMany = EmbedsMany;
62
63function normalizeType(type) {
64 if (!type) {
65 return type;
66 }
67 const t1 = type.toLowerCase();
68 for (const t2 in RelationTypes) {
69 if (t2.toLowerCase() === t1) {
70 return t2;
71 }
72 }
73 return null;
74}
75
76function extendScopeMethods(definition, scopeMethods, ext) {
77 let customMethods = [];
78 let relationClass = RelationClasses[definition.type];
79 if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
80 relationClass = RelationClasses.hasManyThrough;
81 }
82 if (typeof ext === 'function') {
83 customMethods = ext.call(definition, scopeMethods, relationClass);
84 } else if (typeof ext === 'object') {
85 function createFunc(definition, relationMethod) {
86 return function() {
87 const relation = new relationClass(definition, this);
88 return relationMethod.apply(relation, arguments);
89 };
90 }
91 for (const key in ext) {
92 const relationMethod = ext[key];
93 const method = scopeMethods[key] = createFunc(definition, relationMethod);
94 if (relationMethod.shared) {
95 sharedMethod(definition, key, method, relationMethod);
96 }
97 customMethods.push(key);
98 }
99 }
100 return [].concat(customMethods || []);
101}
102
103function bindRelationMethods(relation, relationMethod, definition) {
104 const methods = definition.methods || {};
105 Object.keys(methods).forEach(function(m) {
106 if (typeof methods[m] !== 'function') return;
107 relationMethod[m] = methods[m].bind(relation);
108 });
109}
110
111function preventFkOverride(inst, data, fkProp) {
112 if (!fkProp) return undefined;
113 if (data[fkProp] !== undefined && !idEquals(data[fkProp], inst[fkProp])) {
114 return new Error(g.f(
115 'Cannot override foreign key %s from %s to %s',
116 fkProp,
117 inst[fkProp],
118 data[fkProp],
119 ));
120 }
121}
122
123/**
124 * Relation definition class. Use to define relationships between models.
125 * @param {Object} definition
126 * @class RelationDefinition
127 */
128function RelationDefinition(definition) {
129 if (!(this instanceof RelationDefinition)) {
130 return new RelationDefinition(definition);
131 }
132 definition = definition || {};
133 this.name = definition.name;
134 assert(this.name, 'Relation name is missing');
135 this.type = normalizeType(definition.type);
136 assert(this.type, 'Invalid relation type: ' + definition.type);
137 this.modelFrom = definition.modelFrom;
138 assert(this.modelFrom, 'Source model is required');
139 this.keyFrom = definition.keyFrom;
140 this.modelTo = definition.modelTo;
141 this.keyTo = definition.keyTo;
142 this.polymorphic = definition.polymorphic;
143 if (typeof this.polymorphic !== 'object') {
144 assert(this.modelTo, 'Target model is required');
145 }
146 this.modelThrough = definition.modelThrough;
147 this.keyThrough = definition.keyThrough;
148 this.multiple = definition.multiple;
149 this.properties = definition.properties || {};
150 this.options = definition.options || {};
151 this.scope = definition.scope;
152 this.embed = definition.embed === true;
153 this.methods = definition.methods || {};
154}
155
156RelationDefinition.prototype.toJSON = function() {
157 const polymorphic = typeof this.polymorphic === 'object';
158
159 let modelToName = this.modelTo && this.modelTo.modelName;
160 if (!modelToName && polymorphic && this.type === 'belongsTo') {
161 modelToName = '<polymorphic>';
162 }
163
164 const json = {
165 name: this.name,
166 type: this.type,
167 modelFrom: this.modelFrom.modelName,
168 keyFrom: this.keyFrom,
169 modelTo: modelToName,
170 keyTo: this.keyTo,
171 multiple: this.multiple,
172 };
173 if (this.modelThrough) {
174 json.modelThrough = this.modelThrough.modelName;
175 json.keyThrough = this.keyThrough;
176 }
177 if (polymorphic) {
178 json.polymorphic = this.polymorphic;
179 }
180 return json;
181};
182
183/**
184 * Define a relation scope method
185 * @param {String} name of the method
186 * @param {Function} function to define
187 */
188RelationDefinition.prototype.defineMethod = function(name, fn) {
189 const relationClass = RelationClasses[this.type];
190 const relationName = this.name;
191 const modelFrom = this.modelFrom;
192 const definition = this;
193 let method;
194 if (definition.multiple) {
195 const scope = this.modelFrom.scopes[this.name];
196 if (!scope) throw new Error(g.f('Unknown relation {{scope}}: %s', this.name));
197 method = scope.defineMethod(name, function() {
198 const relation = new relationClass(definition, this);
199 return fn.apply(relation, arguments);
200 });
201 } else {
202 definition.methods[name] = fn;
203 method = function() {
204 const rel = this[relationName];
205 return rel[name].apply(rel, arguments);
206 };
207 }
208 if (method && fn.shared) {
209 sharedMethod(definition, name, method, fn);
210 modelFrom.prototype['__' + name + '__' + relationName] = method;
211 }
212 return method;
213};
214
215/**
216 * Apply the configured scope to the filter/query object.
217 * @param {Object} modelInstance
218 * @param {Object} filter (where, order, limit, fields, ...)
219 */
220RelationDefinition.prototype.applyScope = function(modelInstance, filter) {
221 filter = filter || {};
222 filter.where = filter.where || {};
223 if ((this.type !== 'belongsTo' || this.type === 'hasOne') &&
224 typeof this.polymorphic === 'object') { // polymorphic
225 const discriminator = this.polymorphic.discriminator;
226 if (this.polymorphic.invert) {
227 filter.where[discriminator] = this.modelTo.modelName;
228 } else {
229 filter.where[discriminator] = this.modelFrom.modelName;
230 }
231 }
232 let scope;
233 if (typeof this.scope === 'function') {
234 scope = this.scope.call(this, modelInstance, filter);
235 } else {
236 scope = this.scope;
237 }
238 if (typeof scope === 'object') {
239 mergeQuery(filter, scope);
240 }
241};
242
243/**
244 * Apply the configured properties to the target object.
245 * @param {Object} modelInstance
246 * @param {Object} target
247 */
248RelationDefinition.prototype.applyProperties = function(modelInstance, obj) {
249 let source = modelInstance, target = obj;
250 if (this.options.invertProperties) {
251 source = obj;
252 target = modelInstance;
253 }
254 if (this.options.embedsProperties) {
255 target = target.__data[this.name] = {};
256 target[this.keyTo] = source[this.keyTo];
257 }
258 let k, key;
259 if (typeof this.properties === 'function') {
260 const data = this.properties.call(this, source, target);
261 for (k in data) {
262 target[k] = data[k];
263 }
264 } else if (Array.isArray(this.properties)) {
265 for (k = 0; k < this.properties.length; k++) {
266 key = this.properties[k];
267 target[key] = source[key];
268 }
269 } else if (typeof this.properties === 'object') {
270 for (k in this.properties) {
271 key = this.properties[k];
272 target[key] = source[k];
273 }
274 }
275 if ((this.type !== 'belongsTo' || this.type === 'hasOne') &&
276 typeof this.polymorphic === 'object') { // polymorphic
277 const discriminator = this.polymorphic.discriminator;
278 if (this.polymorphic.invert) {
279 target[discriminator] = this.modelTo.modelName;
280 } else {
281 target[discriminator] = this.modelFrom.modelName;
282 }
283 }
284};
285
286/**
287 * A relation attaching to a given model instance
288 * @param {RelationDefinition|Object} definition
289 * @param {Object} modelInstance
290 * @returns {Relation}
291 * @constructor
292 * @class Relation
293 */
294function Relation(definition, modelInstance) {
295 if (!(this instanceof Relation)) {
296 return new Relation(definition, modelInstance);
297 }
298 if (!(definition instanceof RelationDefinition)) {
299 definition = new RelationDefinition(definition);
300 }
301 this.definition = definition;
302 this.modelInstance = modelInstance;
303}
304
305Relation.prototype.resetCache = function(cache) {
306 cache = cache || undefined;
307 this.modelInstance.__cachedRelations[this.definition.name] = cache;
308};
309
310Relation.prototype.getCache = function() {
311 return this.modelInstance.__cachedRelations[this.definition.name];
312};
313
314Relation.prototype.callScopeMethod = function(methodName) {
315 const args = Array.prototype.slice.call(arguments, 1);
316 const modelInstance = this.modelInstance;
317 const rel = modelInstance[this.definition.name];
318 if (rel && typeof rel[methodName] === 'function') {
319 return rel[methodName].apply(rel, args);
320 } else {
321 throw new Error(g.f('Unknown scope method: %s', methodName));
322 }
323};
324
325/**
326 * Fetch the related model(s) - this is a helper method to unify access.
327 * @param (Boolean|Object} condOrRefresh refresh or conditions object
328 * @param {Object} [options] Options
329 * @param {Function} cb callback
330 */
331Relation.prototype.fetch = function(condOrRefresh, options, cb) {
332 this.modelInstance[this.definition.name].apply(this.modelInstance, arguments);
333};
334
335/**
336 * HasMany subclass
337 * @param {RelationDefinition|Object} definition
338 * @param {Object} modelInstance
339 * @returns {HasMany}
340 * @constructor
341 * @class HasMany
342 */
343function HasMany(definition, modelInstance) {
344 if (!(this instanceof HasMany)) {
345 return new HasMany(definition, modelInstance);
346 }
347 assert(definition.type === RelationTypes.hasMany);
348 Relation.apply(this, arguments);
349}
350
351util.inherits(HasMany, Relation);
352
353HasMany.prototype.removeFromCache = function(id) {
354 const cache = this.modelInstance.__cachedRelations[this.definition.name];
355 const idName = this.definition.modelTo.definition.idName();
356 if (Array.isArray(cache)) {
357 for (let i = 0, n = cache.length; i < n; i++) {
358 if (idEquals(cache[i][idName], id)) {
359 return cache.splice(i, 1);
360 }
361 }
362 }
363 return null;
364};
365
366HasMany.prototype.addToCache = function(inst) {
367 if (!inst) {
368 return;
369 }
370 let cache = this.modelInstance.__cachedRelations[this.definition.name];
371 if (cache === undefined) {
372 cache = this.modelInstance.__cachedRelations[this.definition.name] = [];
373 }
374 const idName = this.definition.modelTo.definition.idName();
375 if (Array.isArray(cache)) {
376 for (let i = 0, n = cache.length; i < n; i++) {
377 if (idEquals(cache[i][idName], inst[idName])) {
378 cache[i] = inst;
379 return;
380 }
381 }
382 cache.push(inst);
383 }
384};
385
386/**
387 * HasManyThrough subclass
388 * @param {RelationDefinition|Object} definition
389 * @param {Object} modelInstance
390 * @returns {HasManyThrough}
391 * @constructor
392 * @class HasManyThrough
393 */
394function HasManyThrough(definition, modelInstance) {
395 if (!(this instanceof HasManyThrough)) {
396 return new HasManyThrough(definition, modelInstance);
397 }
398 assert(definition.type === RelationTypes.hasMany);
399 assert(definition.modelThrough);
400 HasMany.apply(this, arguments);
401}
402
403util.inherits(HasManyThrough, HasMany);
404
405/**
406 * BelongsTo subclass
407 * @param {RelationDefinition|Object} definition
408 * @param {Object} modelInstance
409 * @returns {BelongsTo}
410 * @constructor
411 * @class BelongsTo
412 */
413function BelongsTo(definition, modelInstance) {
414 if (!(this instanceof BelongsTo)) {
415 return new BelongsTo(definition, modelInstance);
416 }
417 assert(definition.type === RelationTypes.belongsTo);
418 Relation.apply(this, arguments);
419}
420
421util.inherits(BelongsTo, Relation);
422
423/**
424 * HasAndBelongsToMany subclass
425 * @param {RelationDefinition|Object} definition
426 * @param {Object} modelInstance
427 * @returns {HasAndBelongsToMany}
428 * @constructor
429 * @class HasAndBelongsToMany
430 */
431function HasAndBelongsToMany(definition, modelInstance) {
432 if (!(this instanceof HasAndBelongsToMany)) {
433 return new HasAndBelongsToMany(definition, modelInstance);
434 }
435 assert(definition.type === RelationTypes.hasAndBelongsToMany);
436 Relation.apply(this, arguments);
437}
438
439util.inherits(HasAndBelongsToMany, Relation);
440
441/**
442 * HasOne subclass
443 * @param {RelationDefinition|Object} definition
444 * @param {Object} modelInstance
445 * @returns {HasOne}
446 * @constructor
447 * @class HasOne
448 */
449function HasOne(definition, modelInstance) {
450 if (!(this instanceof HasOne)) {
451 return new HasOne(definition, modelInstance);
452 }
453 assert(definition.type === RelationTypes.hasOne);
454 Relation.apply(this, arguments);
455}
456
457util.inherits(HasOne, Relation);
458
459/**
460 * EmbedsOne subclass
461 * @param {RelationDefinition|Object} definition
462 * @param {Object} modelInstance
463 * @returns {EmbedsOne}
464 * @constructor
465 * @class EmbedsOne
466 */
467function EmbedsOne(definition, modelInstance) {
468 if (!(this instanceof EmbedsOne)) {
469 return new EmbedsOne(definition, modelInstance);
470 }
471 assert(definition.type === RelationTypes.embedsOne);
472 Relation.apply(this, arguments);
473}
474
475util.inherits(EmbedsOne, Relation);
476
477/**
478 * EmbedsMany subclass
479 * @param {RelationDefinition|Object} definition
480 * @param {Object} modelInstance
481 * @returns {EmbedsMany}
482 * @constructor
483 * @class EmbedsMany
484 */
485function EmbedsMany(definition, modelInstance) {
486 if (!(this instanceof EmbedsMany)) {
487 return new EmbedsMany(definition, modelInstance);
488 }
489 assert(definition.type === RelationTypes.embedsMany);
490 Relation.apply(this, arguments);
491}
492
493util.inherits(EmbedsMany, Relation);
494
495/**
496 * ReferencesMany subclass
497 * @param {RelationDefinition|Object} definition
498 * @param {Object} modelInstance
499 * @returns {ReferencesMany}
500 * @constructor
501 * @class ReferencesMany
502 */
503function ReferencesMany(definition, modelInstance) {
504 if (!(this instanceof ReferencesMany)) {
505 return new ReferencesMany(definition, modelInstance);
506 }
507 assert(definition.type === RelationTypes.referencesMany);
508 Relation.apply(this, arguments);
509}
510
511util.inherits(ReferencesMany, Relation);
512
513/*!
514 * Find the relation by foreign key
515 * @param {*} foreignKey The foreign key
516 * @returns {Array} The array of matching relation objects
517 */
518function findBelongsTo(modelFrom, modelTo, keyTo) {
519 return Object.keys(modelFrom.relations)
520 .map(function(k) { return modelFrom.relations[k]; })
521 .filter(function(rel) {
522 return (rel.type === RelationTypes.belongsTo &&
523 rel.modelTo === modelTo &&
524 (keyTo === undefined || rel.keyTo === keyTo));
525 })
526 .map(function(rel) {
527 return rel.keyFrom;
528 });
529}
530
531/*!
532 * Look up a model by name from the list of given models
533 * @param {Object} models Models keyed by name
534 * @param {String} modelName The model name
535 * @returns {*} The matching model class
536 */
537function lookupModel(models, modelName) {
538 if (models[modelName]) {
539 return models[modelName];
540 }
541 const lookupClassName = modelName.toLowerCase();
542 for (const name in models) {
543 if (name.toLowerCase() === lookupClassName) {
544 return models[name];
545 }
546 }
547}
548
549/*
550 * @param {Object} modelFrom Instance of the 'from' model
551 * @param {Object|String} modelToRef Reference to Model object to which you are
552 * creating the relation: model instance, model name, or name of relation to model.
553 * @param {Object} params The relation params
554 * @param {Boolean} singularize Whether the modelToRef should be singularized when
555 * looking-up modelTo
556 * @return {Object} modelTo Instance of the 'to' model
557 */
558function lookupModelTo(modelFrom, modelToRef, params, singularize) {
559 let modelTo;
560
561 if (typeof modelToRef !== 'string') {
562 // modelToRef might already be an instance of model
563 modelTo = modelToRef;
564 } else {
565 // lookup modelTo based on relation params and modelToRef
566 let modelToName;
567 modelTo = params.model || modelToRef; // modelToRef might be modelTo name
568
569 if (typeof modelTo === 'string') {
570 // lookup modelTo by name
571 modelToName = modelTo;
572 modelToName = (singularize ? i8n.singularize(modelToName) : modelToName).toLowerCase();
573 modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
574 }
575
576 if (!modelTo) {
577 // lookup by modelTo name was not successful. Now looking-up by relationTo name
578 const relationToName = params.as || modelToRef; // modelToRef might be relationTo name
579 modelToName = (singularize ? i8n.singularize(relationToName) : relationToName).toLowerCase();
580 modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
581 }
582 }
583 if (typeof modelTo !== 'function') {
584 throw new Error(g.f('Could not find relation %s for model %s', params.as, modelFrom.modelName));
585 }
586 return modelTo;
587}
588
589/*
590 * Normalize relation's parameter `as`
591 * @param {Object} params The relation params
592 * @param {String} relationName The relation name
593 * @returns {Object} The normalized parameters
594 * NOTE: normalizeRelationAs() mutates the params object
595 */
596function normalizeRelationAs(params, relationName) {
597 if (typeof relationName === 'string') {
598 params.as = params.as || relationName;
599 }
600 return params;
601}
602
603/*
604 * Normalize relation's polymorphic parameters
605 * @param {Object|String|Boolean} polymorphic Param `polymorphic` of the relation.
606 * @param {String} relationName The name of the relation we are currently setting up.
607 * @returns {Object} The normalized parameters
608 */
609function normalizePolymorphic(polymorphic, relationName) {
610 assert(polymorphic, 'polymorphic param can\'t be false, null or undefined');
611 assert(!Array.isArray(polymorphic, 'unexpected type for polymorphic param: \'Array\''));
612
613 let selector;
614
615 if (typeof polymorphic === 'string') {
616 // relation type is different from belongsTo (hasMany, hasManyThrough, hasAndBelongsToMany, ...)
617 // polymorphic is the name of the matching belongsTo relation from modelTo to modelFrom
618 selector = polymorphic;
619 }
620
621 if (polymorphic === true) {
622 // relation type is belongsTo: the relation name is used as the polymorphic selector
623 selector = relationName;
624 }
625
626 // NOTE: use of `polymorphic.as` keyword will be deprecated in LoopBack.next
627 // to avoid confusion with keyword `as` used at the root of the relation definition object
628 // It is replaced with the `polymorphic.selector` keyword
629 if (typeof polymorphic == 'object') {
630 selector = polymorphic.selector || polymorphic.as;
631 }
632
633 // relationName is eventually used as selector if provided and selector not already defined
634 // it ultimately defaults to 'reference'
635 selector = selector || relationName || 'reference';
636
637 // make sure polymorphic is an object
638 if (typeof polymorphic !== 'object') {
639 polymorphic = {};
640 }
641
642 polymorphic.selector = selector;
643 polymorphic.foreignKey = polymorphic.foreignKey || i8n.camelize(selector + '_id', true); // defaults to {{selector}}Id
644 polymorphic.discriminator = polymorphic.discriminator || i8n.camelize(selector + '_type', true); // defaults to {{selectorName}}Type
645
646 return polymorphic;
647}
648
649/**
650 * Define a "one to many" relationship by specifying the model name
651 *
652 * Examples:
653 * ```
654 * User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});
655 * ```
656 *
657 * ```
658 * Book.hasMany(Chapter);
659 * ```
660 * Or, equivalently:
661 * ```
662 * Book.hasMany('chapters', {model: Chapter});
663 * ```
664 * @param {Model} modelFrom Source model class
665 * @param {Object|String} modelToRef Reference to Model object to which you are
666 * creating the relation: model instance, model name, or name of relation to model.
667 * @options {Object} params Configuration parameters; see below.
668 * @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
669 * @property {String} foreignKey Property name of foreign key field.
670 * @property {Object} model Model object
671 */
672RelationDefinition.hasMany = function hasMany(modelFrom, modelToRef, params) {
673 const thisClassName = modelFrom.modelName;
674 params = params || {};
675 normalizeRelationAs(params, modelToRef);
676 const modelTo = lookupModelTo(modelFrom, modelToRef, params, true);
677
678 const relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
679 let fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true);
680 let keyThrough = params.keyThrough || i8n.camelize(modelTo.modelName + '_id', true);
681
682 const pkName = params.primaryKey || modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
683 let discriminator, polymorphic;
684
685 if (params.polymorphic) {
686 polymorphic = normalizePolymorphic(params.polymorphic, relationName);
687 if (params.invert) {
688 polymorphic.invert = true;
689 keyThrough = polymorphic.foreignKey;
690 }
691 discriminator = polymorphic.discriminator;
692 if (!params.invert) {
693 fk = polymorphic.foreignKey;
694 }
695 if (!params.through) {
696 modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, {type: 'string', index: true});
697 }
698 }
699
700 const definition = new RelationDefinition({
701 name: relationName,
702 type: RelationTypes.hasMany,
703 modelFrom: modelFrom,
704 keyFrom: pkName,
705 keyTo: fk,
706 modelTo: modelTo,
707 multiple: true,
708 properties: params.properties,
709 scope: params.scope,
710 options: params.options,
711 keyThrough: keyThrough,
712 polymorphic: polymorphic,
713 });
714
715 definition.modelThrough = params.through;
716
717 modelFrom.relations[relationName] = definition;
718
719 if (!params.through) {
720 // obviously, modelTo should have attribute called `fk`
721 // for polymorphic relations, it is assumed to share the same fk type for all
722 // polymorphic models
723 modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName, pkName);
724 }
725
726 const scopeMethods = {
727 findById: scopeMethod(definition, 'findById'),
728 destroy: scopeMethod(definition, 'destroyById'),
729 updateById: scopeMethod(definition, 'updateById'),
730 exists: scopeMethod(definition, 'exists'),
731 };
732
733 const findByIdFunc = scopeMethods.findById;
734 modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
735
736 const destroyByIdFunc = scopeMethods.destroy;
737 modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
738
739 const updateByIdFunc = scopeMethods.updateById;
740 modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
741
742 const existsByIdFunc = scopeMethods.exists;
743 modelFrom.prototype['__exists__' + relationName] = existsByIdFunc;
744
745 if (definition.modelThrough) {
746 scopeMethods.create = scopeMethod(definition, 'create');
747 scopeMethods.add = scopeMethod(definition, 'add');
748 scopeMethods.remove = scopeMethod(definition, 'remove');
749
750 const addFunc = scopeMethods.add;
751 modelFrom.prototype['__link__' + relationName] = addFunc;
752
753 const removeFunc = scopeMethods.remove;
754 modelFrom.prototype['__unlink__' + relationName] = removeFunc;
755 } else {
756 scopeMethods.create = scopeMethod(definition, 'create');
757 scopeMethods.build = scopeMethod(definition, 'build');
758 }
759
760 const customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
761
762 for (let i = 0; i < customMethods.length; i++) {
763 const methodName = customMethods[i];
764 const method = scopeMethods[methodName];
765 if (typeof method === 'function' && method.shared === true) {
766 modelFrom.prototype['__' + methodName + '__' + relationName] = method;
767 }
768 }
769
770 // Mix the property and scoped methods into the prototype class
771 defineScope(modelFrom.prototype, params.through || modelTo, relationName, function() {
772 const filter = {};
773 filter.where = {};
774 filter.where[fk] = this[pkName];
775
776 definition.applyScope(this, filter);
777
778 if (definition.modelThrough) {
779 let throughRelationName;
780
781 // find corresponding belongsTo relations from through model as collect
782 for (const r in definition.modelThrough.relations) {
783 const relation = definition.modelThrough.relations[r];
784
785 // should be a belongsTo and match modelTo and keyThrough
786 // if relation is polymorphic then check keyThrough only
787 if (relation.type === RelationTypes.belongsTo &&
788 (relation.polymorphic && !relation.modelTo || relation.modelTo === definition.modelTo) &&
789 (relation.keyFrom === definition.keyThrough)
790 ) {
791 throughRelationName = relation.name;
792 break;
793 }
794 }
795
796 if (definition.polymorphic && definition.polymorphic.invert) {
797 filter.collect = definition.polymorphic.selector;
798 filter.include = filter.collect;
799 } else {
800 filter.collect = throughRelationName || i8n.camelize(modelTo.modelName, true);
801 filter.include = filter.collect;
802 }
803 }
804
805 return filter;
806 }, scopeMethods, definition.options);
807
808 return definition;
809};
810
811function scopeMethod(definition, methodName) {
812 let relationClass = RelationClasses[definition.type];
813 if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
814 relationClass = RelationClasses.hasManyThrough;
815 }
816 const method = function() {
817 const relation = new relationClass(definition, this);
818 return relation[methodName].apply(relation, arguments);
819 };
820
821 const relationMethod = relationClass.prototype[methodName];
822 if (relationMethod.shared) {
823 sharedMethod(definition, methodName, method, relationMethod);
824 }
825 return method;
826}
827
828function sharedMethod(definition, methodName, method, relationMethod) {
829 method.shared = true;
830 method.accepts = relationMethod.accepts;
831 method.returns = relationMethod.returns;
832 method.http = relationMethod.http;
833 method.description = relationMethod.description;
834}
835
836/**
837 * Find a related item by foreign key
838 * @param {*} fkId The foreign key
839 * @param {Object} [options] Options
840 * @param {Function} cb The callback function
841 */
842HasMany.prototype.findById = function(fkId, options, cb) {
843 if (typeof options === 'function' && cb === undefined) {
844 cb = options;
845 options = {};
846 }
847 const modelTo = this.definition.modelTo;
848 const modelFrom = this.definition.modelFrom;
849 const fk = this.definition.keyTo;
850 const pk = this.definition.keyFrom;
851 const modelInstance = this.modelInstance;
852
853 const idName = this.definition.modelTo.definition.idName();
854 const filter = {};
855 filter.where = {};
856 filter.where[idName] = fkId;
857 filter.where[fk] = modelInstance[pk];
858
859 cb = cb || utils.createPromiseCallback();
860
861 if (filter.where[fk] === undefined) {
862 // Foreign key is undefined
863 process.nextTick(cb);
864 return cb.promise;
865 }
866 this.definition.applyScope(modelInstance, filter);
867
868 modelTo.findOne(filter, options, function(err, inst) {
869 if (err) {
870 return cb(err);
871 }
872 if (!inst) {
873 err = new Error(g.f('No instance with {{id}} %s found for %s', fkId, modelTo.modelName));
874 err.statusCode = 404;
875 return cb(err);
876 }
877 // Check if the foreign key matches the primary key
878 if (inst[fk] != null && idEquals(inst[fk], modelInstance[pk])) {
879 cb(null, inst);
880 } else {
881 err = new Error(g.f('Key mismatch: %s.%s: %s, %s.%s: %s',
882 modelFrom.modelName, pk, modelInstance[pk], modelTo.modelName, fk, inst[fk]));
883 err.statusCode = 400;
884 cb(err);
885 }
886 });
887 return cb.promise;
888};
889
890/**
891 * Find a related item by foreign key
892 * @param {*} fkId The foreign key
893 * @param {Object} [options] Options
894 * @param {Function} cb The callback function
895 */
896HasMany.prototype.exists = function(fkId, options, cb) {
897 if (typeof options === 'function' && cb === undefined) {
898 cb = options;
899 options = {};
900 }
901 const fk = this.definition.keyTo;
902 const pk = this.definition.keyFrom;
903 const modelInstance = this.modelInstance;
904 cb = cb || utils.createPromiseCallback();
905
906 this.findById(fkId, function(err, inst) {
907 if (err) {
908 return cb(err);
909 }
910 if (!inst) {
911 return cb(null, false);
912 }
913 // Check if the foreign key matches the primary key
914 if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
915 cb(null, true);
916 } else {
917 cb(null, false);
918 }
919 });
920 return cb.promise;
921};
922
923/**
924 * Update a related item by foreign key
925 * @param {*} fkId The foreign key
926 * @param {Object} Changes to the data
927 * @param {Object} [options] Options
928 * @param {Function} cb The callback function
929 */
930HasMany.prototype.updateById = function(fkId, data, options, cb) {
931 if (typeof options === 'function' && cb === undefined) {
932 cb = options;
933 options = {};
934 }
935 cb = cb || utils.createPromiseCallback();
936 const fk = this.definition.keyTo;
937
938 this.findById(fkId, options, function(err, inst) {
939 if (err) {
940 return cb && cb(err);
941 }
942 // Ensure Foreign Key cannot be changed!
943 const fkErr = preventFkOverride(inst, data, fk);
944 if (fkErr) return cb(fkErr);
945 inst.updateAttributes(data, options, cb);
946 });
947 return cb.promise;
948};
949
950/**
951 * Delete a related item by foreign key
952 * @param {*} fkId The foreign key
953 * @param {Object} [options] Options
954 * @param {Function} cb The callback function
955 */
956HasMany.prototype.destroyById = function(fkId, options, cb) {
957 if (typeof options === 'function' && cb === undefined) {
958 cb = options;
959 options = {};
960 }
961 cb = cb || utils.createPromiseCallback();
962 const self = this;
963 this.findById(fkId, options, function(err, inst) {
964 if (err) {
965 return cb(err);
966 }
967 self.removeFromCache(fkId);
968 inst.destroy(options, cb);
969 });
970 return cb.promise;
971};
972
973const throughKeys = function(definition) {
974 const modelThrough = definition.modelThrough;
975 const pk2 = definition.modelTo.definition.idName();
976
977 let fk1, fk2;
978 if (typeof definition.polymorphic === 'object') { // polymorphic
979 fk1 = definition.keyTo;
980 if (definition.polymorphic.invert) {
981 fk2 = definition.polymorphic.foreignKey;
982 } else {
983 fk2 = definition.keyThrough;
984 }
985 } else if (definition.modelFrom === definition.modelTo) {
986 return findBelongsTo(modelThrough, definition.modelTo, pk2).
987 sort(function(fk1, fk2) {
988 // Fix for bug - https://github.com/strongloop/loopback-datasource-juggler/issues/571
989 // Make sure that first key is mapped to modelFrom
990 // & second key to modelTo. Order matters
991 return (definition.keyTo === fk1) ? -1 : 1;
992 });
993 } else {
994 fk1 = findBelongsTo(modelThrough, definition.modelFrom,
995 definition.keyFrom)[0];
996 fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2)[0];
997 }
998 return [fk1, fk2];
999};
1000
1001/**
1002 * Find a related item by foreign key
1003 * @param {*} fkId The foreign key value
1004 * @param {Object} [options] Options
1005 * @param {Function} cb The callback function
1006 */
1007HasManyThrough.prototype.findById = function(fkId, options, cb) {
1008 if (typeof options === 'function' && cb === undefined) {
1009 cb = options;
1010 options = {};
1011 }
1012 const self = this;
1013 const modelTo = this.definition.modelTo;
1014 const pk = this.definition.keyFrom;
1015 const modelInstance = this.modelInstance;
1016 const modelThrough = this.definition.modelThrough;
1017
1018 cb = cb || utils.createPromiseCallback();
1019
1020 self.exists(fkId, options, function(err, exists) {
1021 if (err || !exists) {
1022 if (!err) {
1023 err = new Error(g.f('No relation found in %s' +
1024 ' for (%s.%s,%s.%s)',
1025 modelThrough.modelName, self.definition.modelFrom.modelName,
1026 modelInstance[pk], modelTo.modelName, fkId));
1027 err.statusCode = 404;
1028 }
1029 return cb(err);
1030 }
1031 modelTo.findById(fkId, options, function(err, inst) {
1032 if (err) {
1033 return cb(err);
1034 }
1035 if (!inst) {
1036 err = new Error(g.f('No instance with id %s found for %s', fkId, modelTo.modelName));
1037 err.statusCode = 404;
1038 return cb(err);
1039 }
1040 cb(err, inst);
1041 });
1042 });
1043 return cb.promise;
1044};
1045
1046/**
1047 * Delete a related item by foreign key
1048 * @param {*} fkId The foreign key
1049 * @param {Object} [options] Options
1050 * @param {Function} cb The callback function
1051 */
1052HasManyThrough.prototype.destroyById = function(fkId, options, cb) {
1053 if (typeof options === 'function' && cb === undefined) {
1054 cb = options;
1055 options = {};
1056 }
1057 const self = this;
1058 const modelTo = this.definition.modelTo;
1059 const pk = this.definition.keyFrom;
1060 const modelInstance = this.modelInstance;
1061 const modelThrough = this.definition.modelThrough;
1062
1063 cb = cb || utils.createPromiseCallback();
1064
1065 self.exists(fkId, options, function(err, exists) {
1066 if (err || !exists) {
1067 if (!err) {
1068 err = new Error(g.f('No record found in %s for (%s.%s ,%s.%s)',
1069 modelThrough.modelName, self.definition.modelFrom.modelName,
1070 modelInstance[pk], modelTo.modelName, fkId));
1071 err.statusCode = 404;
1072 }
1073 return cb(err);
1074 }
1075 self.remove(fkId, options, function(err) {
1076 if (err) {
1077 return cb(err);
1078 }
1079 modelTo.deleteById(fkId, options, cb);
1080 });
1081 });
1082 return cb.promise;
1083};
1084
1085// Create an instance of the target model and connect it to the instance of
1086// the source model by creating an instance of the through model
1087HasManyThrough.prototype.create = function create(data, options, cb) {
1088 if (typeof options === 'function' && cb === undefined) {
1089 cb = options;
1090 options = {};
1091 }
1092 const self = this;
1093 const definition = this.definition;
1094 const modelTo = definition.modelTo;
1095 const modelThrough = definition.modelThrough;
1096
1097 if (typeof data === 'function' && !cb) {
1098 cb = data;
1099 data = {};
1100 }
1101 cb = cb || utils.createPromiseCallback();
1102
1103 const modelInstance = this.modelInstance;
1104
1105 // First create the target model
1106 modelTo.create(data, options, function(err, to) {
1107 if (err) {
1108 return cb(err, to);
1109 }
1110 // The primary key for the target model
1111 const pk2 = definition.modelTo.definition.idName();
1112 const keys = throughKeys(definition);
1113 const fk1 = keys[0];
1114 const fk2 = keys[1];
1115
1116 function createRelation(to, next) {
1117 const d = {}, q = {}, filter = {where: q};
1118 d[fk1] = q[fk1] = modelInstance[definition.keyFrom];
1119 d[fk2] = q[fk2] = to[pk2];
1120 definition.applyProperties(modelInstance, d);
1121 definition.applyScope(modelInstance, filter);
1122
1123 // Then create the through model
1124 modelThrough.findOrCreate(filter, d, options, function(e, through) {
1125 if (e) {
1126 // Undo creation of the target model
1127 to.destroy(options, function() {
1128 next(e);
1129 });
1130 } else {
1131 self.addToCache(to);
1132 next(err, to);
1133 }
1134 });
1135 }
1136
1137 // process array or single item
1138 if (!Array.isArray(to))
1139 createRelation(to, cb);
1140 else
1141 async.map(to, createRelation, cb);
1142 });
1143 return cb.promise;
1144};
1145
1146/**
1147 * Add the target model instance to the 'hasMany' relation
1148 * @param {Object|ID} acInst The actual instance or id value
1149 * @param {Object} [data] Optional data object for the through model to be created
1150 * @param {Object} [options] Options
1151 * @param {Function} [cb] Callback function
1152 */
1153HasManyThrough.prototype.add = function(acInst, data, options, cb) {
1154 if (typeof options === 'function' && cb === undefined) {
1155 cb = options;
1156 options = {};
1157 }
1158 const self = this;
1159 const definition = this.definition;
1160 const modelThrough = definition.modelThrough;
1161 const pk1 = definition.keyFrom;
1162
1163 if (typeof data === 'function') {
1164 cb = data;
1165 data = {};
1166 }
1167 const query = {};
1168
1169 data = data || {};
1170 cb = cb || utils.createPromiseCallback();
1171
1172 // The primary key for the target model
1173 const pk2 = definition.modelTo.definition.idName();
1174
1175 const keys = throughKeys(definition);
1176 const fk1 = keys[0];
1177 const fk2 = keys[1];
1178
1179 query[fk1] = this.modelInstance[pk1];
1180 query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
1181
1182 const filter = {where: query};
1183
1184 definition.applyScope(this.modelInstance, filter);
1185
1186 data[fk1] = this.modelInstance[pk1];
1187 data[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
1188
1189 definition.applyProperties(this.modelInstance, data);
1190
1191 // Create an instance of the through model
1192 modelThrough.findOrCreate(filter, data, options, function(err, ac) {
1193 if (!err) {
1194 if (acInst instanceof definition.modelTo) {
1195 self.addToCache(acInst);
1196 }
1197 }
1198 cb(err, ac);
1199 });
1200 return cb.promise;
1201};
1202
1203/**
1204 * Check if the target model instance is related to the 'hasMany' relation
1205 * @param {Object|ID} acInst The actual instance or id value
1206 */
1207HasManyThrough.prototype.exists = function(acInst, options, cb) {
1208 if (typeof options === 'function' && cb === undefined) {
1209 cb = options;
1210 options = {};
1211 }
1212 const definition = this.definition;
1213 const modelThrough = definition.modelThrough;
1214 const pk1 = definition.keyFrom;
1215
1216 const query = {};
1217
1218 // The primary key for the target model
1219 const pk2 = definition.modelTo.definition.idName();
1220
1221 const keys = throughKeys(definition);
1222 const fk1 = keys[0];
1223 const fk2 = keys[1];
1224
1225 query[fk1] = this.modelInstance[pk1];
1226 query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
1227
1228 const filter = {where: query};
1229
1230 definition.applyScope(this.modelInstance, filter);
1231
1232 cb = cb || utils.createPromiseCallback();
1233
1234 modelThrough.count(filter.where, options, function(err, ac) {
1235 cb(err, ac > 0);
1236 });
1237 return cb.promise;
1238};
1239
1240/**
1241 * Remove the target model instance from the 'hasMany' relation
1242 * @param {Object|ID) acInst The actual instance or id value
1243 */
1244HasManyThrough.prototype.remove = function(acInst, options, cb) {
1245 if (typeof options === 'function' && cb === undefined) {
1246 cb = options;
1247 options = {};
1248 }
1249 const self = this;
1250 const definition = this.definition;
1251 const modelThrough = definition.modelThrough;
1252 const pk1 = definition.keyFrom;
1253
1254 const query = {};
1255
1256 // The primary key for the target model
1257 const pk2 = definition.modelTo.definition.idName();
1258
1259 const keys = throughKeys(definition);
1260 const fk1 = keys[0];
1261 const fk2 = keys[1];
1262
1263 query[fk1] = this.modelInstance[pk1];
1264 query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
1265
1266 const filter = {where: query};
1267
1268 definition.applyScope(this.modelInstance, filter);
1269
1270 cb = cb || utils.createPromiseCallback();
1271
1272 modelThrough.deleteAll(filter.where, options, function(err) {
1273 if (!err) {
1274 self.removeFromCache(query[fk2]);
1275 }
1276 cb(err);
1277 });
1278 return cb.promise;
1279};
1280
1281/**
1282 * Declare "belongsTo" relation that sets up a one-to-one connection with
1283 * another model, such that each instance of the declaring model "belongs to"
1284 * one instance of the other model.
1285 *
1286 * For example, if an application includes users and posts, and each post can
1287 * be written by exactly one user. The following code specifies that `Post` has
1288 * a reference called `author` to the `User` model via the `userId` property of
1289 * `Post` as the foreign key.
1290 * ```
1291 * Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
1292 * ```
1293 *
1294 * This optional parameter default value is false, so the related object will
1295 * be loaded from cache if available.
1296 *
1297 * @param {Object|String} modelToRef Reference to Model object to which you are
1298 * creating the relation: model instance, model name, or name of relation to model.
1299 * @options {Object} params Configuration parameters; see below.
1300 * @property {String} as Name of the property in the referring model that
1301 * corresponds to the foreign key field in the related model.
1302 * @property {String} foreignKey Name of foreign key property.
1303 *
1304 */
1305RelationDefinition.belongsTo = function(modelFrom, modelToRef, params) {
1306 let modelTo, discriminator, polymorphic;
1307 params = params || {};
1308
1309 let pkName, relationName, fk;
1310 if (params.polymorphic) {
1311 relationName = params.as || (typeof modelToRef === 'string' ? modelToRef : null);
1312 polymorphic = normalizePolymorphic(params.polymorphic, relationName);
1313
1314 modelTo = null; // will be looked-up dynamically
1315
1316 pkName = params.primaryKey || params.idName || 'id';
1317 fk = polymorphic.foreignKey;
1318 discriminator = polymorphic.discriminator;
1319
1320 if (polymorphic.idType) { // explicit key type
1321 modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, {type: polymorphic.idType, index: true});
1322 } else { // try to use the same foreign key type as modelFrom
1323 modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelFrom.modelName, pkName);
1324 }
1325
1326 modelFrom.dataSource.defineProperty(modelFrom.modelName, discriminator, {type: 'string', index: true});
1327 } else {
1328 // relation is not polymorphic
1329 normalizeRelationAs(params, modelToRef);
1330 modelTo = lookupModelTo(modelFrom, modelToRef, params);
1331 pkName = params.primaryKey || modelTo.dataSource.idName(modelTo.modelName) || 'id';
1332 relationName = params.as || i8n.camelize(modelTo.modelName, true);
1333 fk = params.foreignKey || relationName + 'Id';
1334
1335 modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelTo.modelName, pkName);
1336 }
1337
1338 const definition = modelFrom.relations[relationName] = new RelationDefinition({
1339 name: relationName,
1340 type: RelationTypes.belongsTo,
1341 modelFrom: modelFrom,
1342 keyFrom: fk,
1343 keyTo: pkName,
1344 modelTo: modelTo,
1345 multiple: false,
1346 properties: params.properties,
1347 scope: params.scope,
1348 options: params.options,
1349 polymorphic: polymorphic,
1350 methods: params.methods,
1351 });
1352
1353 // Define a property for the scope so that we have 'this' for the scoped methods
1354 Object.defineProperty(modelFrom.prototype, relationName, {
1355 enumerable: true,
1356 configurable: true,
1357 get: function() {
1358 const relation = new BelongsTo(definition, this);
1359 const relationMethod = relation.related.bind(relation);
1360 relationMethod.get = relation.get.bind(relation);
1361 relationMethod.getAsync = function() {
1362 deprecated(g.f('BelongsTo method "getAsync()" is deprecated, use "get()" instead.'));
1363 return this.get.apply(this, arguments);
1364 };
1365 relationMethod.update = relation.update.bind(relation);
1366 relationMethod.destroy = relation.destroy.bind(relation);
1367 if (!polymorphic) {
1368 relationMethod.create = relation.create.bind(relation);
1369 relationMethod.build = relation.build.bind(relation);
1370 relationMethod._targetClass = definition.modelTo.modelName;
1371 }
1372 bindRelationMethods(relation, relationMethod, definition);
1373 return relationMethod;
1374 },
1375 });
1376
1377 // FIXME: [rfeng] Wrap the property into a function for remoting
1378 // so that it can be accessed as /api/<model>/<id>/<belongsToRelationName>
1379 // For example, /api/orders/1/customer
1380 const fn = function() {
1381 const f = this[relationName];
1382 f.apply(this, arguments);
1383 };
1384 modelFrom.prototype['__get__' + relationName] = fn;
1385
1386 return definition;
1387};
1388
1389BelongsTo.prototype.create = function(targetModelData, options, cb) {
1390 if (typeof options === 'function' && cb === undefined) {
1391 cb = options;
1392 options = {};
1393 }
1394 const self = this;
1395 const modelTo = this.definition.modelTo;
1396 const fk = this.definition.keyFrom;
1397 const pk = this.definition.keyTo;
1398 const modelInstance = this.modelInstance;
1399
1400 if (typeof targetModelData === 'function' && !cb) {
1401 cb = targetModelData;
1402 targetModelData = {};
1403 }
1404 cb = cb || utils.createPromiseCallback();
1405
1406 this.definition.applyProperties(modelInstance, targetModelData || {});
1407
1408 modelTo.create(targetModelData, options, function(err, targetModel) {
1409 if (!err) {
1410 modelInstance[fk] = targetModel[pk];
1411 if (modelInstance.isNewRecord()) {
1412 self.resetCache(targetModel);
1413 cb && cb(err, targetModel);
1414 } else {
1415 modelInstance.save(options, function(err, inst) {
1416 if (cb && err) return cb && cb(err);
1417 self.resetCache(targetModel);
1418 cb && cb(err, targetModel);
1419 });
1420 }
1421 } else {
1422 cb && cb(err);
1423 }
1424 });
1425 return cb.promise;
1426};
1427
1428BelongsTo.prototype.build = function(targetModelData) {
1429 const modelTo = this.definition.modelTo;
1430 this.definition.applyProperties(this.modelInstance, targetModelData || {});
1431 return new modelTo(targetModelData);
1432};
1433
1434BelongsTo.prototype.update = function(targetModelData, options, cb) {
1435 if (typeof options === 'function' && cb === undefined) {
1436 cb = options;
1437 options = {};
1438 }
1439 cb = cb || utils.createPromiseCallback();
1440 const definition = this.definition;
1441 const fk = definition.keyTo;
1442
1443 this.fetch(options, function(err, inst) {
1444 if (inst instanceof ModelBaseClass) {
1445 // Ensures Foreign Key cannot be changed!
1446 const fkErr = preventFkOverride(inst, targetModelData, fk);
1447 if (fkErr) return cb(fkErr);
1448 inst.updateAttributes(targetModelData, options, cb);
1449 } else {
1450 cb(new Error(g.f('{{BelongsTo}} relation %s is empty', definition.name)));
1451 }
1452 });
1453 return cb.promise;
1454};
1455
1456BelongsTo.prototype.destroy = function(options, cb) {
1457 if (typeof options === 'function' && cb === undefined) {
1458 cb = options;
1459 options = {};
1460 }
1461
1462 const definition = this.definition;
1463 const modelInstance = this.modelInstance;
1464 const fk = definition.keyFrom;
1465
1466 cb = cb || utils.createPromiseCallback();
1467
1468 this.fetch(options, function(err, targetModel) {
1469 if (targetModel instanceof ModelBaseClass) {
1470 modelInstance[fk] = null;
1471 modelInstance.save(options, function(err, targetModel) {
1472 if (cb && err) return cb && cb(err);
1473 cb && cb(err, targetModel);
1474 });
1475 } else {
1476 cb(new Error(g.f('{{BelongsTo}} relation %s is empty', definition.name)));
1477 }
1478 });
1479 return cb.promise;
1480};
1481
1482/**
1483 * Define the method for the belongsTo relation itself
1484 * It will support one of the following styles:
1485 * - order.customer(refresh, options, callback): Load the target model instance asynchronously
1486 * - order.customer(customer): Synchronous setter of the target model instance
1487 * - order.customer(): Synchronous getter of the target model instance
1488 *
1489 * @param refresh
1490 * @param params
1491 * @returns {*}
1492 */
1493BelongsTo.prototype.related = function(condOrRefresh, options, cb) {
1494 const self = this;
1495 const modelFrom = this.definition.modelFrom;
1496 let modelTo = this.definition.modelTo;
1497 const pk = this.definition.keyTo;
1498 const fk = this.definition.keyFrom;
1499 const modelInstance = this.modelInstance;
1500 let discriminator;
1501 let scopeQuery = null;
1502 let newValue;
1503
1504 if ((condOrRefresh instanceof ModelBaseClass) &&
1505 options === undefined && cb === undefined) {
1506 // order.customer(customer)
1507 newValue = condOrRefresh;
1508 condOrRefresh = false;
1509 } else if (typeof condOrRefresh === 'function' &&
1510 options === undefined && cb === undefined) {
1511 // order.customer(cb)
1512 cb = condOrRefresh;
1513 condOrRefresh = false;
1514 } else if (typeof options === 'function' && cb === undefined) {
1515 // order.customer(condOrRefresh, cb)
1516 cb = options;
1517 options = {};
1518 }
1519 if (!newValue) {
1520 scopeQuery = condOrRefresh;
1521 }
1522
1523 if (typeof this.definition.polymorphic === 'object') {
1524 discriminator = this.definition.polymorphic.discriminator;
1525 }
1526
1527 let cachedValue;
1528 if (!condOrRefresh) {
1529 cachedValue = self.getCache();
1530 }
1531 if (newValue) { // acts as setter
1532 modelInstance[fk] = newValue[pk];
1533
1534 if (discriminator) {
1535 modelInstance[discriminator] = newValue.constructor.modelName;
1536 }
1537
1538 this.definition.applyProperties(modelInstance, newValue);
1539
1540 self.resetCache(newValue);
1541 } else if (typeof cb === 'function') { // acts as async getter
1542 if (discriminator) {
1543 let modelToName = modelInstance[discriminator];
1544 if (typeof modelToName !== 'string') {
1545 throw new Error(g.f('{{Polymorphic}} model not found: `%s` not set', discriminator));
1546 }
1547 modelToName = modelToName.toLowerCase();
1548 modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
1549 if (!modelTo) {
1550 throw new Error(g.f('{{Polymorphic}} model not found: `%s`', modelToName));
1551 }
1552 }
1553
1554 if (cachedValue === undefined || !(cachedValue instanceof ModelBaseClass)) {
1555 const query = {where: {}};
1556 query.where[pk] = modelInstance[fk];
1557
1558 if (query.where[pk] === undefined || query.where[pk] === null) {
1559 // Foreign key is undefined
1560 return process.nextTick(cb);
1561 }
1562
1563 this.definition.applyScope(modelInstance, query);
1564
1565 if (scopeQuery) mergeQuery(query, scopeQuery);
1566
1567 if (Array.isArray(query.fields) && query.fields.indexOf(pk) === -1) {
1568 query.fields.push(pk); // always include the pk
1569 }
1570
1571 modelTo.findOne(query, options, function(err, inst) {
1572 if (err) {
1573 return cb(err);
1574 }
1575 if (!inst) {
1576 return cb(null, null);
1577 }
1578 // Check if the foreign key matches the primary key
1579 if (inst[pk] != null && modelInstance[fk] != null &&
1580 inst[pk].toString() === modelInstance[fk].toString()) {
1581 self.resetCache(inst);
1582 cb(null, inst);
1583 } else {
1584 err = new Error(g.f('Key mismatch: %s.%s: %s, %s.%s: %s',
1585 self.definition.modelFrom.modelName, fk, modelInstance[fk],
1586 modelTo.modelName, pk, inst[pk]));
1587 err.statusCode = 400;
1588 cb(err);
1589 }
1590 });
1591 return modelInstance[fk];
1592 } else {
1593 cb(null, cachedValue);
1594 return cachedValue;
1595 }
1596 } else if (condOrRefresh === undefined) { // acts as sync getter
1597 return cachedValue;
1598 } else { // setter
1599 modelInstance[fk] = newValue;
1600 self.resetCache();
1601 }
1602};
1603
1604/**
1605 * Define a Promise-based method for the belongsTo relation itself
1606 * - order.customer.get(cb): Load the target model instance asynchronously
1607 *
1608 * @param {Function} cb Callback of the form function (err, inst)
1609 * @returns {Promise | Undefined} returns promise if callback is omitted
1610 */
1611BelongsTo.prototype.get = function(options, cb) {
1612 if (typeof options === 'function' && cb === undefined) {
1613 cb = options;
1614 options = {};
1615 }
1616 cb = cb || utils.createPromiseCallback();
1617 this.related(true, options, cb);
1618 return cb.promise;
1619};
1620
1621/**
1622 * A hasAndBelongsToMany relation creates a direct many-to-many connection with
1623 * another model, with no intervening model. For example, if your application
1624 * includes users and groups, with each group having many users and each user
1625 * appearing in many groups, you could declare the models this way:
1626 * ```
1627 * User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
1628 * ```
1629 *
1630 * @param {Object|String} modelToRef Reference to Model object to which you are
1631 * creating the relation: model instance, model name, or name of relation to model.
1632 * @options {Object} params Configuration parameters; see below.
1633 * @property {String} as Name of the property in the referring model that
1634 * corresponds to the foreign key field in the related model.
1635 * @property {String} foreignKey Property name of foreign key field.
1636 * @property {Object} model Model object
1637 */
1638RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, modelToRef, params) {
1639 params = params || {};
1640 normalizeRelationAs(params, modelToRef);
1641 const modelTo = lookupModelTo(modelFrom, modelToRef, params, true);
1642
1643 const models = modelFrom.dataSource.modelBuilder.models;
1644
1645 if (!params.through) {
1646 if (params.polymorphic) throw new Error(g.f('{{Polymorphic}} relations need a through model'));
1647
1648 if (params.throughTable) {
1649 params.through = modelFrom.dataSource.define(params.throughTable);
1650 } else {
1651 const name1 = modelFrom.modelName + modelTo.modelName;
1652 const name2 = modelTo.modelName + modelFrom.modelName;
1653 params.through = lookupModel(models, name1) || lookupModel(models, name2) ||
1654 modelFrom.dataSource.define(name1);
1655 }
1656 }
1657
1658 const options = {as: params.as, through: params.through};
1659 options.properties = params.properties;
1660 options.scope = params.scope;
1661
1662 // Forward relation options like "disableInclude"
1663 options.options = params.options;
1664
1665 if (params.polymorphic) {
1666 const relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
1667 const polymorphic = normalizePolymorphic(params.polymorphic, relationName);
1668 options.polymorphic = polymorphic; // pass through
1669 const accessor = params.through.prototype[polymorphic.selector];
1670 if (typeof accessor !== 'function') { // declare once
1671 // use the name of the polymorphic selector, not modelTo
1672 params.through.belongsTo(polymorphic.selector, {polymorphic: true});
1673 }
1674 } else {
1675 params.through.belongsTo(modelFrom);
1676 }
1677
1678 params.through.belongsTo(modelTo);
1679
1680 return this.hasMany(modelFrom, modelTo, options);
1681};
1682
1683/**
1684 * A HasOne relation creates a one-to-one connection from modelFrom to modelTo.
1685 * This relation indicates that each instance of a model contains or possesses
1686 * one instance of another model. For example, each supplier in your application
1687 * has only one account.
1688 *
1689 * @param {Function} modelFrom The declaring model class
1690 * @param {Object|String} modelToRef Reference to Model object to which you are
1691 * creating the relation: model instance, model name, or name of relation to model.
1692 * @options {Object} params Configuration parameters; see below.
1693 * @property {String} as Name of the property in the referring model that
1694 * corresponds to the foreign key field in the related model.
1695 * @property {String} foreignKey Property name of foreign key field.
1696 * @property {Object} model Model object
1697 */
1698RelationDefinition.hasOne = function(modelFrom, modelToRef, params) {
1699 params = params || {};
1700 normalizeRelationAs(params, modelToRef);
1701 const modelTo = lookupModelTo(modelFrom, modelToRef, params);
1702
1703 const pk = params.primaryKey || modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
1704 const relationName = params.as || i8n.camelize(modelTo.modelName, true);
1705
1706 let fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true);
1707 let discriminator, polymorphic;
1708
1709 if (params.polymorphic) {
1710 polymorphic = normalizePolymorphic(params.polymorphic, relationName);
1711 fk = polymorphic.foreignKey;
1712 discriminator = polymorphic.discriminator;
1713 if (!params.through) {
1714 modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, {type: 'string', index: true});
1715 }
1716 }
1717
1718 const definition = modelFrom.relations[relationName] = new RelationDefinition({
1719 name: relationName,
1720 type: RelationTypes.hasOne,
1721 modelFrom: modelFrom,
1722 keyFrom: pk,
1723 keyTo: fk,
1724 modelTo: modelTo,
1725 multiple: false,
1726 properties: params.properties,
1727 scope: params.scope,
1728 options: params.options,
1729 polymorphic: polymorphic,
1730 methods: params.methods,
1731 });
1732
1733 modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName, pk);
1734
1735 // Define a property for the scope so that we have 'this' for the scoped methods
1736 Object.defineProperty(modelFrom.prototype, relationName, {
1737 enumerable: true,
1738 configurable: true,
1739 get: function() {
1740 const relation = new HasOne(definition, this);
1741 const relationMethod = relation.related.bind(relation);
1742 relationMethod.get = relation.get.bind(relation);
1743 relationMethod.getAsync = function() {
1744 deprecated(g.f('HasOne method "getAsync()" is deprecated, use "get()" instead.'));
1745 return this.get.apply(this, arguments);
1746 };
1747 relationMethod.create = relation.create.bind(relation);
1748 relationMethod.build = relation.build.bind(relation);
1749 relationMethod.update = relation.update.bind(relation);
1750 relationMethod.destroy = relation.destroy.bind(relation);
1751 relationMethod._targetClass = definition.modelTo.modelName;
1752 bindRelationMethods(relation, relationMethod, definition);
1753 return relationMethod;
1754 },
1755 });
1756
1757 // FIXME: [rfeng] Wrap the property into a function for remoting
1758 // so that it can be accessed as /api/<model>/<id>/<hasOneRelationName>
1759 // For example, /api/orders/1/customer
1760 modelFrom.prototype['__get__' + relationName] = function() {
1761 const f = this[relationName];
1762 f.apply(this, arguments);
1763 };
1764
1765 modelFrom.prototype['__create__' + relationName] = function() {
1766 const f = this[relationName].create;
1767 f.apply(this, arguments);
1768 };
1769
1770 modelFrom.prototype['__update__' + relationName] = function() {
1771 const f = this[relationName].update;
1772 f.apply(this, arguments);
1773 };
1774
1775 modelFrom.prototype['__destroy__' + relationName] = function() {
1776 const f = this[relationName].destroy;
1777 f.apply(this, arguments);
1778 };
1779
1780 return definition;
1781};
1782
1783/**
1784 * Create a target model instance
1785 * @param {Object} targetModelData The target model data
1786 * @callback {Function} [cb] Callback function
1787 * @param {String|Object} err Error string or object
1788 * @param {Object} The newly created target model instance
1789 */
1790HasOne.prototype.create = function(targetModelData, options, cb) {
1791 if (typeof options === 'function' && cb === undefined) {
1792 // customer.profile.create(options, cb)
1793 cb = options;
1794 options = {};
1795 }
1796 const self = this;
1797 const modelTo = this.definition.modelTo;
1798 const fk = this.definition.keyTo;
1799 const pk = this.definition.keyFrom;
1800 const modelInstance = this.modelInstance;
1801
1802 if (typeof targetModelData === 'function' && !cb) {
1803 cb = targetModelData;
1804 targetModelData = {};
1805 }
1806 targetModelData = targetModelData || {};
1807 cb = cb || utils.createPromiseCallback();
1808
1809 targetModelData[fk] = modelInstance[pk];
1810 const query = {where: {}};
1811 query.where[fk] = targetModelData[fk];
1812
1813 this.definition.applyScope(modelInstance, query);
1814 this.definition.applyProperties(modelInstance, targetModelData);
1815
1816 modelTo.findOrCreate(query, targetModelData, options,
1817 function(err, targetModel, created) {
1818 if (err) {
1819 return cb && cb(err);
1820 }
1821 if (created) {
1822 // Refresh the cache
1823 self.resetCache(targetModel);
1824 cb && cb(err, targetModel);
1825 } else {
1826 cb && cb(new Error(g.f(
1827 '{{HasOne}} relation cannot create more than one instance of %s',
1828 modelTo.modelName,
1829 )));
1830 }
1831 });
1832 return cb.promise;
1833};
1834
1835HasOne.prototype.update = function(targetModelData, options, cb) {
1836 if (typeof options === 'function' && cb === undefined) {
1837 // customer.profile.update(data, cb)
1838 cb = options;
1839 options = {};
1840 }
1841 cb = cb || utils.createPromiseCallback();
1842 const definition = this.definition;
1843 const fk = this.definition.keyTo;
1844 this.fetch(function(err, targetModel) {
1845 if (targetModel instanceof ModelBaseClass) {
1846 // Ensures Foreign Key cannot be changed!
1847 const fkErr = preventFkOverride(targetModel, targetModelData, fk);
1848 if (fkErr) return cb(fkErr);
1849 targetModel.updateAttributes(targetModelData, options, cb);
1850 } else {
1851 cb(new Error(g.f('{{HasOne}} relation %s is empty', definition.name)));
1852 }
1853 });
1854 return cb.promise;
1855};
1856
1857HasOne.prototype.destroy = function(options, cb) {
1858 if (typeof options === 'function' && cb === undefined) {
1859 // customer.profile.destroy(cb)
1860 cb = options;
1861 options = {};
1862 }
1863 cb = cb || utils.createPromiseCallback();
1864 const definition = this.definition;
1865 this.fetch(function(err, targetModel) {
1866 if (targetModel instanceof ModelBaseClass) {
1867 targetModel.destroy(options, cb);
1868 } else {
1869 cb(new Error(g.f('{{HasOne}} relation %s is empty', definition.name)));
1870 }
1871 });
1872 return cb.promise;
1873};
1874
1875/**
1876 * Create a target model instance
1877 * @param {Object} targetModelData The target model data
1878 * @callback {Function} [cb] Callback function
1879 * @param {String|Object} err Error string or object
1880 * @param {Object} The newly created target model instance
1881 */
1882HasMany.prototype.create = function(targetModelData, options, cb) {
1883 if (typeof options === 'function' && cb === undefined) {
1884 // customer.orders.create(data, cb)
1885 cb = options;
1886 options = {};
1887 }
1888 const self = this;
1889 const modelTo = this.definition.modelTo;
1890 const fk = this.definition.keyTo;
1891 const pk = this.definition.keyFrom;
1892 const modelInstance = this.modelInstance;
1893
1894 if (typeof targetModelData === 'function' && !cb) {
1895 cb = targetModelData;
1896 targetModelData = {};
1897 }
1898 targetModelData = targetModelData || {};
1899 cb = cb || utils.createPromiseCallback();
1900
1901 const fkAndProps = function(item) {
1902 item[fk] = modelInstance[pk];
1903 self.definition.applyProperties(modelInstance, item);
1904 };
1905
1906 const apply = function(data, fn) {
1907 if (Array.isArray(data)) {
1908 data.forEach(fn);
1909 } else {
1910 fn(data);
1911 }
1912 };
1913
1914 apply(targetModelData, fkAndProps);
1915
1916 modelTo.create(targetModelData, options, function(err, targetModel) {
1917 if (!err) {
1918 // Refresh the cache
1919 apply(targetModel, self.addToCache.bind(self));
1920 cb && cb(err, targetModel);
1921 } else {
1922 cb && cb(err);
1923 }
1924 });
1925 return cb.promise;
1926};
1927/**
1928 * Build a target model instance
1929 * @param {Object} targetModelData The target model data
1930 * @returns {Object} The newly built target model instance
1931 */
1932HasMany.prototype.build = HasOne.prototype.build = function(targetModelData) {
1933 const modelTo = this.definition.modelTo;
1934 const pk = this.definition.keyFrom;
1935 const fk = this.definition.keyTo;
1936
1937 targetModelData = targetModelData || {};
1938 targetModelData[fk] = this.modelInstance[pk];
1939
1940 this.definition.applyProperties(this.modelInstance, targetModelData);
1941
1942 return new modelTo(targetModelData);
1943};
1944
1945/**
1946 * Define the method for the hasOne relation itself
1947 * It will support one of the following styles:
1948 * - order.customer(refresh, callback): Load the target model instance asynchronously
1949 * - order.customer(customer): Synchronous setter of the target model instance
1950 * - order.customer(): Synchronous getter of the target model instance
1951 *
1952 * @param {Boolean} refresh Reload from the data source
1953 * @param {Object|Function} params Query parameters
1954 * @returns {Object}
1955 */
1956HasOne.prototype.related = function(condOrRefresh, options, cb) {
1957 const self = this;
1958 const modelTo = this.definition.modelTo;
1959 const fk = this.definition.keyTo;
1960 const pk = this.definition.keyFrom;
1961 const definition = this.definition;
1962 const modelInstance = this.modelInstance;
1963 let newValue;
1964
1965 if ((condOrRefresh instanceof ModelBaseClass) &&
1966 options === undefined && cb === undefined) {
1967 // order.customer(customer)
1968 newValue = condOrRefresh;
1969 condOrRefresh = false;
1970 } else if (typeof condOrRefresh === 'function' &&
1971 options === undefined && cb === undefined) {
1972 // customer.profile(cb)
1973 cb = condOrRefresh;
1974 condOrRefresh = false;
1975 } else if (typeof options === 'function' && cb === undefined) {
1976 // customer.profile(condOrRefresh, cb)
1977 cb = options;
1978 options = {};
1979 }
1980
1981 let cachedValue;
1982 if (!condOrRefresh) {
1983 cachedValue = self.getCache();
1984 }
1985 if (newValue) { // acts as setter
1986 newValue[fk] = modelInstance[pk];
1987 self.resetCache(newValue);
1988 } else if (typeof cb === 'function') { // acts as async getter
1989 if (cachedValue === undefined) {
1990 const query = {where: {}};
1991 query.where[fk] = modelInstance[pk];
1992 definition.applyScope(modelInstance, query);
1993 modelTo.findOne(query, options, function(err, inst) {
1994 if (err) {
1995 return cb(err);
1996 }
1997 if (!inst) {
1998 return cb(null, null);
1999 }
2000 // Check if the foreign key matches the primary key
2001 if (inst[fk] != null && modelInstance[pk] != null &&
2002 inst[fk].toString() === modelInstance[pk].toString()) {
2003 self.resetCache(inst);
2004 cb(null, inst);
2005 } else {
2006 err = new Error(g.f('Key mismatch: %s.%s: %s, %s.%s: %s',
2007 self.definition.modelFrom.modelName, pk, modelInstance[pk],
2008 modelTo.modelName, fk, inst[fk]));
2009 err.statusCode = 400;
2010 cb(err);
2011 }
2012 });
2013 return modelInstance[pk];
2014 } else {
2015 cb(null, cachedValue);
2016 return cachedValue;
2017 }
2018 } else if (condOrRefresh === undefined) { // acts as sync getter
2019 return cachedValue;
2020 } else { // setter
2021 newValue[fk] = modelInstance[pk];
2022 self.resetCache();
2023 }
2024};
2025
2026/**
2027 * Define a Promise-based method for the hasOne relation itself
2028 * - order.customer.get(cb): Load the target model instance asynchronously
2029 *
2030 * @param {Function} cb Callback of the form function (err, inst)
2031 * @returns {Promise | Undefined} Returns promise if cb is omitted
2032 */
2033HasOne.prototype.get = function(options, cb) {
2034 if (typeof options === 'function' && cb === undefined) {
2035 cb = options;
2036 options = {};
2037 }
2038 cb = cb || utils.createPromiseCallback();
2039 this.related(true, cb);
2040 return cb.promise;
2041};
2042
2043RelationDefinition.embedsOne = function(modelFrom, modelToRef, params) {
2044 params = params || {};
2045 normalizeRelationAs(params, modelToRef);
2046 const modelTo = lookupModelTo(modelFrom, modelToRef, params);
2047
2048 const thisClassName = modelFrom.modelName;
2049 const relationName = params.as || (i8n.camelize(modelTo.modelName, true) + 'Item');
2050 let propertyName = params.property || i8n.camelize(modelTo.modelName, true);
2051 const idName = modelTo.dataSource.idName(modelTo.modelName) || 'id';
2052
2053 if (relationName === propertyName) {
2054 propertyName = '_' + propertyName;
2055 debug('EmbedsOne property cannot be equal to relation name: ' +
2056 'forcing property %s for relation %s', propertyName, relationName);
2057 }
2058
2059 const definition = modelFrom.relations[relationName] = new RelationDefinition({
2060 name: relationName,
2061 type: RelationTypes.embedsOne,
2062 modelFrom: modelFrom,
2063 keyFrom: propertyName,
2064 keyTo: idName,
2065 modelTo: modelTo,
2066 multiple: false,
2067 properties: params.properties,
2068 scope: params.scope,
2069 options: params.options,
2070 embed: true,
2071 methods: params.methods,
2072 });
2073
2074 const opts = Object.assign(
2075 params.options && params.options.property ? params.options.property : {},
2076 {type: modelTo},
2077 );
2078
2079 if (params.default === true) {
2080 opts.default = function() { return new modelTo(); };
2081 } else if (typeof params.default === 'object') {
2082 opts.default = (function(def) {
2083 return function() {
2084 return new modelTo(def);
2085 };
2086 }(params.default));
2087 }
2088
2089 modelFrom.dataSource.defineProperty(modelFrom.modelName, propertyName, opts);
2090
2091 // validate the embedded instance
2092 if (definition.options.validate !== false) {
2093 modelFrom.validate(relationName, function(err) {
2094 const inst = this[propertyName];
2095 if (inst instanceof modelTo) {
2096 if (!inst.isValid()) {
2097 const first = Object.keys(inst.errors)[0];
2098 const msg = 'is invalid: `' + first + '` ' + inst.errors[first];
2099 this.errors.add(relationName, msg, 'invalid');
2100 err(false);
2101 }
2102 }
2103 });
2104 }
2105
2106 // Define a property for the scope so that we have 'this' for the scoped methods
2107 Object.defineProperty(modelFrom.prototype, relationName, {
2108 enumerable: true,
2109 configurable: true,
2110 get: function() {
2111 const relation = new EmbedsOne(definition, this);
2112 const relationMethod = relation.related.bind(relation);
2113 relationMethod.create = relation.create.bind(relation);
2114 relationMethod.build = relation.build.bind(relation);
2115 relationMethod.update = relation.update.bind(relation);
2116 relationMethod.destroy = relation.destroy.bind(relation);
2117 relationMethod.value = relation.embeddedValue.bind(relation);
2118 relationMethod._targetClass = definition.modelTo.modelName;
2119 bindRelationMethods(relation, relationMethod, definition);
2120 return relationMethod;
2121 },
2122 });
2123
2124 // FIXME: [rfeng] Wrap the property into a function for remoting
2125 // so that it can be accessed as /api/<model>/<id>/<embedsOneRelationName>
2126 // For example, /api/orders/1/customer
2127 modelFrom.prototype['__get__' + relationName] = function() {
2128 const f = this[relationName];
2129 f.apply(this, arguments);
2130 };
2131
2132 modelFrom.prototype['__create__' + relationName] = function() {
2133 const f = this[relationName].create;
2134 f.apply(this, arguments);
2135 };
2136
2137 modelFrom.prototype['__update__' + relationName] = function() {
2138 const f = this[relationName].update;
2139 f.apply(this, arguments);
2140 };
2141
2142 modelFrom.prototype['__destroy__' + relationName] = function() {
2143 const f = this[relationName].destroy;
2144 f.apply(this, arguments);
2145 };
2146
2147 return definition;
2148};
2149
2150EmbedsOne.prototype.related = function(condOrRefresh, options, cb) {
2151 const modelTo = this.definition.modelTo;
2152 const modelInstance = this.modelInstance;
2153 const propertyName = this.definition.keyFrom;
2154 let newValue;
2155
2156 if ((condOrRefresh instanceof ModelBaseClass) &&
2157 options === undefined && cb === undefined) {
2158 // order.customer(customer)
2159 newValue = condOrRefresh;
2160 condOrRefresh = false;
2161 } else if (typeof condOrRefresh === 'function' &&
2162 options === undefined && cb === undefined) {
2163 // order.customer(cb)
2164 cb = condOrRefresh;
2165 condOrRefresh = false;
2166 } else if (typeof options === 'function' && cb === undefined) {
2167 // order.customer(condOrRefresh, cb)
2168 cb = options;
2169 options = {};
2170 }
2171
2172 if (newValue) { // acts as setter
2173 if (newValue instanceof modelTo) {
2174 this.definition.applyProperties(modelInstance, newValue);
2175 modelInstance.setAttribute(propertyName, newValue);
2176 }
2177 return;
2178 }
2179
2180 const embeddedInstance = this.embeddedValue();
2181
2182 if (embeddedInstance) {
2183 embeddedInstance.__persisted = true;
2184 }
2185
2186 if (typeof cb === 'function') { // acts as async getter
2187 process.nextTick(function() {
2188 cb(null, embeddedInstance);
2189 });
2190 } else if (condOrRefresh === undefined) { // acts as sync getter
2191 return embeddedInstance;
2192 }
2193};
2194
2195EmbedsOne.prototype.prepareEmbeddedInstance = function(inst) {
2196 if (inst && inst.triggerParent !== 'function') {
2197 const self = this;
2198 const propertyName = this.definition.keyFrom;
2199 const modelInstance = this.modelInstance;
2200 if (this.definition.options.persistent) {
2201 const pk = this.definition.keyTo;
2202 inst.__persisted = !!inst[pk];
2203 } else {
2204 inst.__persisted = true;
2205 }
2206 inst.triggerParent = function(actionName, callback) {
2207 if (actionName === 'save') {
2208 const embeddedValue = self.embeddedValue();
2209 modelInstance.updateAttribute(propertyName,
2210 embeddedValue, function(err, modelInst) {
2211 callback(err, err ? null : modelInst);
2212 });
2213 } else if (actionName === 'destroy') {
2214 modelInstance.unsetAttribute(propertyName, true);
2215 // cannot delete property completely the way save works. operator $unset needed like mongo
2216 modelInstance.save(function(err, modelInst) {
2217 callback(err, modelInst);
2218 });
2219 } else {
2220 process.nextTick(callback);
2221 }
2222 };
2223 const originalTrigger = inst.trigger;
2224 inst.trigger = function(actionName, work, data, callback) {
2225 if (typeof work === 'function') {
2226 const originalWork = work;
2227 work = function(next) {
2228 originalWork.call(this, function(done) {
2229 inst.triggerParent(actionName, function(err, inst) {
2230 next(done); // TODO [fabien] - error handling?
2231 });
2232 });
2233 };
2234 }
2235 originalTrigger.call(this, actionName, work, data, callback);
2236 };
2237 }
2238};
2239
2240EmbedsOne.prototype.embeddedValue = function(modelInstance) {
2241 modelInstance = modelInstance || this.modelInstance;
2242 const embeddedValue = modelInstance[this.definition.keyFrom];
2243 this.prepareEmbeddedInstance(embeddedValue);
2244 return embeddedValue;
2245};
2246
2247EmbedsOne.prototype.create = function(targetModelData, options, cb) {
2248 if (typeof options === 'function' && cb === undefined) {
2249 // order.customer.create(data, cb)
2250 cb = options;
2251 options = {};
2252 }
2253 const modelTo = this.definition.modelTo;
2254 const propertyName = this.definition.keyFrom;
2255 const modelInstance = this.modelInstance;
2256
2257 if (typeof targetModelData === 'function' && !cb) {
2258 cb = targetModelData;
2259 targetModelData = {};
2260 }
2261
2262 targetModelData = targetModelData || {};
2263 cb = cb || utils.createPromiseCallback();
2264
2265 const inst = this.callScopeMethod('build', targetModelData);
2266
2267 const updateEmbedded = function(callback) {
2268 if (modelInstance.isNewRecord()) {
2269 modelInstance.setAttribute(propertyName, inst);
2270 modelInstance.save(options, function(err) {
2271 callback(err, err ? null : inst);
2272 });
2273 } else {
2274 modelInstance.updateAttribute(propertyName,
2275 inst, options, function(err) {
2276 callback(err, err ? null : inst);
2277 });
2278 }
2279 };
2280
2281 if (this.definition.options.persistent) {
2282 inst.save(options, function(err) { // will validate
2283 if (err) return cb(err, inst);
2284 updateEmbedded(cb);
2285 });
2286 } else {
2287 const context = {
2288 Model: modelTo,
2289 instance: inst,
2290 options: options || {},
2291 hookState: {},
2292 };
2293 modelTo.notifyObserversOf('before save', context, function(err) {
2294 if (err) {
2295 return process.nextTick(function() {
2296 cb(err);
2297 });
2298 }
2299
2300 err = inst.isValid() ? null : new ValidationError(inst);
2301 if (err) {
2302 process.nextTick(function() {
2303 cb(err);
2304 });
2305 } else {
2306 updateEmbedded(function(err, inst) {
2307 if (err) return cb(err);
2308 context.instance = inst;
2309 modelTo.notifyObserversOf('after save', context, function(err) {
2310 cb(err, err ? null : inst);
2311 });
2312 });
2313 }
2314 });
2315 }
2316 return cb.promise;
2317};
2318
2319EmbedsOne.prototype.build = function(targetModelData) {
2320 const modelTo = this.definition.modelTo;
2321 const modelInstance = this.modelInstance;
2322 const propertyName = this.definition.keyFrom;
2323 const forceId = this.definition.options.forceId;
2324 const persistent = this.definition.options.persistent;
2325 const connector = modelTo.dataSource.connector;
2326
2327 targetModelData = targetModelData || {};
2328
2329 this.definition.applyProperties(modelInstance, targetModelData);
2330
2331 const pk = this.definition.keyTo;
2332 const pkProp = modelTo.definition.properties[pk];
2333
2334 let assignId = (forceId || targetModelData[pk] === undefined);
2335 assignId = assignId && !persistent && (pkProp && pkProp.generated);
2336
2337 if (assignId && typeof connector.generateId === 'function') {
2338 const id = connector.generateId(modelTo.modelName, targetModelData, pk);
2339 targetModelData[pk] = id;
2340 }
2341
2342 const embeddedInstance = new modelTo(targetModelData);
2343 modelInstance[propertyName] = embeddedInstance;
2344
2345 this.prepareEmbeddedInstance(embeddedInstance);
2346
2347 return embeddedInstance;
2348};
2349
2350EmbedsOne.prototype.update = function(targetModelData, options, cb) {
2351 if (typeof options === 'function' && cb === undefined) {
2352 // order.customer.update(data, cb)
2353 cb = options;
2354 options = {};
2355 }
2356
2357 const modelTo = this.definition.modelTo;
2358 const modelInstance = this.modelInstance;
2359 const propertyName = this.definition.keyFrom;
2360
2361 const isInst = targetModelData instanceof ModelBaseClass;
2362 const data = isInst ? targetModelData.toObject() : targetModelData;
2363
2364 const embeddedInstance = this.embeddedValue();
2365 if (embeddedInstance instanceof modelTo) {
2366 cb = cb || utils.createPromiseCallback();
2367 const hookState = {};
2368 let context = {
2369 Model: modelTo,
2370 currentInstance: embeddedInstance,
2371 data: data,
2372 options: options || {},
2373 hookState: hookState,
2374 };
2375 modelTo.notifyObserversOf('before save', context, function(err) {
2376 if (err) return cb(err);
2377
2378 embeddedInstance.setAttributes(context.data);
2379
2380 // TODO support async validations
2381 if (!embeddedInstance.isValid()) {
2382 return cb(new ValidationError(embeddedInstance));
2383 }
2384
2385 modelInstance.save(function(err, inst) {
2386 if (err) return cb(err);
2387
2388 context = {
2389 Model: modelTo,
2390 instance: inst ? inst[propertyName] : embeddedInstance,
2391 options: options || {},
2392 hookState: hookState,
2393 };
2394 modelTo.notifyObserversOf('after save', context, function(err) {
2395 cb(err, context.instance);
2396 });
2397 });
2398 });
2399 } else if (!embeddedInstance && cb) {
2400 return this.callScopeMethod('create', data, cb);
2401 } else if (!embeddedInstance) {
2402 return this.callScopeMethod('build', data);
2403 }
2404 return cb.promise;
2405};
2406
2407EmbedsOne.prototype.destroy = function(options, cb) {
2408 if (typeof options === 'function' && cb === undefined) {
2409 // order.customer.destroy(cb)
2410 cb = options;
2411 options = {};
2412 }
2413 cb = cb || utils.createPromiseCallback();
2414 const modelTo = this.definition.modelTo;
2415 const modelInstance = this.modelInstance;
2416 const propertyName = this.definition.keyFrom;
2417 const embeddedInstance = modelInstance[propertyName];
2418
2419 if (!embeddedInstance) {
2420 cb();
2421 return cb.promise;
2422 }
2423
2424 modelInstance.unsetAttribute(propertyName, true);
2425
2426 const context = {
2427 Model: modelTo,
2428 instance: embeddedInstance,
2429 options: options || {},
2430 hookState: {},
2431 };
2432 modelTo.notifyObserversOf('before delete', context, function(err) {
2433 if (err) return cb(err);
2434 modelInstance.save(function(err, result) {
2435 if (err) return cb(err);
2436 modelTo.notifyObserversOf('after delete', context, cb);
2437 });
2438 });
2439
2440 return cb.promise;
2441};
2442
2443RelationDefinition.embedsMany = function embedsMany(modelFrom, modelToRef, params) {
2444 params = params || {};
2445 normalizeRelationAs(params, modelToRef);
2446 const modelTo = lookupModelTo(modelFrom, modelToRef, params, true);
2447
2448 const thisClassName = modelFrom.modelName;
2449 const relationName = params.as || (i8n.camelize(modelTo.modelName, true) + 'List');
2450 let propertyName = params.property || i8n.camelize(modelTo.pluralModelName, true);
2451 const idName = modelTo.dataSource.idName(modelTo.modelName) || 'id';
2452
2453 if (relationName === propertyName) {
2454 propertyName = '_' + propertyName;
2455 debug('EmbedsMany property cannot be equal to relation name: ' +
2456 'forcing property %s for relation %s', propertyName, relationName);
2457 }
2458
2459 const definition = modelFrom.relations[relationName] = new RelationDefinition({
2460 name: relationName,
2461 type: RelationTypes.embedsMany,
2462 modelFrom: modelFrom,
2463 keyFrom: propertyName,
2464 keyTo: idName,
2465 modelTo: modelTo,
2466 multiple: true,
2467 properties: params.properties,
2468 scope: params.scope,
2469 options: params.options,
2470 embed: true,
2471 });
2472
2473 const opts = Object.assign(
2474 params.options && params.options.property ? params.options.property : {},
2475 params.options && params.options.omitDefaultEmbeddedItem ? {type: [modelTo]} :
2476 {
2477 type: [modelTo],
2478 default: function() { return []; },
2479 },
2480 );
2481
2482 modelFrom.dataSource.defineProperty(modelFrom.modelName, propertyName, opts);
2483
2484 if (typeof modelTo.dataSource.connector.generateId !== 'function') {
2485 modelFrom.validate(propertyName, function(err) {
2486 const self = this;
2487 const embeddedList = this[propertyName] || [];
2488 let hasErrors = false;
2489 embeddedList.forEach(function(item, idx) {
2490 if (item instanceof modelTo && item[idName] == undefined) {
2491 hasErrors = true;
2492 let msg = 'contains invalid item at index `' + idx + '`:';
2493 msg += ' `' + idName + '` is blank';
2494 self.errors.add(propertyName, msg, 'invalid');
2495 }
2496 });
2497 if (hasErrors) err(false);
2498 });
2499 }
2500
2501 if (!params.polymorphic) {
2502 modelFrom.validate(propertyName, function(err) {
2503 const embeddedList = this[propertyName] || [];
2504 const ids = embeddedList.map(function(m) { return m[idName] && m[idName].toString(); }); // mongodb
2505 if (idsHaveDuplicates(ids)) {
2506 this.errors.add(propertyName, 'contains duplicate `' + idName + '`', 'uniqueness');
2507 err(false);
2508 }
2509 }, {code: 'uniqueness'});
2510 }
2511
2512 // validate all embedded items
2513 if (definition.options.validate !== false) {
2514 modelFrom.validate(propertyName, function(err) {
2515 const self = this;
2516 const embeddedList = this[propertyName] || [];
2517 let hasErrors = false;
2518 embeddedList.forEach(function(item, idx) {
2519 if (item instanceof modelTo) {
2520 if (!item.isValid()) {
2521 hasErrors = true;
2522 const id = item[idName];
2523 const first = Object.keys(item.errors)[0];
2524 let msg = id ?
2525 'contains invalid item: `' + id + '`' :
2526 'contains invalid item at index `' + idx + '`';
2527 msg += ' (`' + first + '` ' + item.errors[first] + ')';
2528 self.errors.add(propertyName, msg, 'invalid');
2529 }
2530 } else {
2531 hasErrors = true;
2532 self.errors.add(propertyName, 'contains invalid item', 'invalid');
2533 }
2534 });
2535 if (hasErrors) err(false);
2536 });
2537 }
2538
2539 const scopeMethods = {
2540 findById: scopeMethod(definition, 'findById'),
2541 destroy: scopeMethod(definition, 'destroyById'),
2542 updateById: scopeMethod(definition, 'updateById'),
2543 exists: scopeMethod(definition, 'exists'),
2544 add: scopeMethod(definition, 'add'),
2545 remove: scopeMethod(definition, 'remove'),
2546 get: scopeMethod(definition, 'get'),
2547 set: scopeMethod(definition, 'set'),
2548 unset: scopeMethod(definition, 'unset'),
2549 at: scopeMethod(definition, 'at'),
2550 value: scopeMethod(definition, 'embeddedValue'),
2551 };
2552
2553 const findByIdFunc = scopeMethods.findById;
2554 modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
2555
2556 const destroyByIdFunc = scopeMethods.destroy;
2557 modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
2558
2559 const updateByIdFunc = scopeMethods.updateById;
2560 modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
2561
2562 const addFunc = scopeMethods.add;
2563 modelFrom.prototype['__link__' + relationName] = addFunc;
2564
2565 const removeFunc = scopeMethods.remove;
2566 modelFrom.prototype['__unlink__' + relationName] = removeFunc;
2567
2568 scopeMethods.create = scopeMethod(definition, 'create');
2569 scopeMethods.build = scopeMethod(definition, 'build');
2570
2571 scopeMethods.related = scopeMethod(definition, 'related'); // bound to definition
2572
2573 if (!definition.options.persistent) {
2574 scopeMethods.destroyAll = scopeMethod(definition, 'destroyAll');
2575 }
2576
2577 const customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
2578
2579 for (let i = 0; i < customMethods.length; i++) {
2580 const methodName = customMethods[i];
2581 const method = scopeMethods[methodName];
2582 if (typeof method === 'function' && method.shared === true) {
2583 modelFrom.prototype['__' + methodName + '__' + relationName] = method;
2584 }
2585 }
2586
2587 // Mix the property and scoped methods into the prototype class
2588 const scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function() {
2589 return {};
2590 }, scopeMethods, definition.options);
2591
2592 scopeDefinition.related = scopeMethods.related;
2593
2594 return definition;
2595};
2596
2597EmbedsMany.prototype.prepareEmbeddedInstance = function(inst) {
2598 if (inst && inst.triggerParent !== 'function') {
2599 const self = this;
2600 const propertyName = this.definition.keyFrom;
2601 const modelInstance = this.modelInstance;
2602 if (this.definition.options.persistent) {
2603 const pk = this.definition.keyTo;
2604 inst.__persisted = !!inst[pk];
2605 } else {
2606 inst.__persisted = true;
2607 }
2608 inst.triggerParent = function(actionName, callback) {
2609 if (actionName === 'save' || actionName === 'destroy') {
2610 const embeddedList = self.embeddedList();
2611 if (actionName === 'destroy') {
2612 const index = embeddedList.indexOf(inst);
2613 if (index > -1) embeddedList.splice(index, 1);
2614 }
2615 modelInstance.updateAttribute(propertyName,
2616 embeddedList, function(err, modelInst) {
2617 callback(err, err ? null : modelInst);
2618 });
2619 } else {
2620 process.nextTick(callback);
2621 }
2622 };
2623 const originalTrigger = inst.trigger;
2624 inst.trigger = function(actionName, work, data, callback) {
2625 if (typeof work === 'function') {
2626 const originalWork = work;
2627 work = function(next) {
2628 originalWork.call(this, function(done) {
2629 inst.triggerParent(actionName, function(err, inst) {
2630 next(done); // TODO [fabien] - error handling?
2631 });
2632 });
2633 };
2634 }
2635 originalTrigger.call(this, actionName, work, data, callback);
2636 };
2637 }
2638};
2639
2640EmbedsMany.prototype.embeddedList =
2641EmbedsMany.prototype.embeddedValue = function(modelInstance) {
2642 modelInstance = modelInstance || this.modelInstance;
2643 const embeddedList = modelInstance[this.definition.keyFrom] || [];
2644 embeddedList.forEach(this.prepareEmbeddedInstance.bind(this));
2645 return embeddedList;
2646};
2647
2648EmbedsMany.prototype.related = function(receiver, scopeParams, condOrRefresh, options, cb) {
2649 const modelTo = this.definition.modelTo;
2650 const modelInstance = this.modelInstance;
2651
2652 let actualCond = {};
2653
2654 if (typeof condOrRefresh === 'function' &&
2655 options === undefined && cb === undefined) {
2656 // customer.emails(receiver, scopeParams, cb)
2657 cb = condOrRefresh;
2658 condOrRefresh = false;
2659 } else if (typeof options === 'function' && cb === undefined) {
2660 // customer.emails(receiver, scopeParams, condOrRefresh, cb)
2661 cb = options;
2662 options = {};
2663 }
2664
2665 if (typeof condOrRefresh === 'object') {
2666 actualCond = condOrRefresh;
2667 }
2668
2669 let embeddedList = this.embeddedList(receiver);
2670
2671 this.definition.applyScope(receiver, actualCond);
2672
2673 const params = mergeQuery(actualCond, scopeParams);
2674
2675 if (params.where && Object.keys(params.where).length > 0) { // TODO [fabien] Support order/sorting
2676 embeddedList = embeddedList ? embeddedList.filter(applyFilter(params)) : embeddedList;
2677 }
2678
2679 const returnRelated = function(list) {
2680 if (params.include) {
2681 modelTo.include(list, params.include, options, cb);
2682 } else {
2683 process.nextTick(function() { cb(null, list); });
2684 }
2685 };
2686
2687 returnRelated(embeddedList);
2688};
2689
2690EmbedsMany.prototype.findById = function(fkId, options, cb) {
2691 if (typeof options === 'function' && cb === undefined) {
2692 // order.emails(fkId, cb)
2693 cb = options;
2694 options = {};
2695 }
2696 const pk = this.definition.keyTo;
2697 const modelTo = this.definition.modelTo;
2698 const modelInstance = this.modelInstance;
2699
2700 const embeddedList = this.embeddedList();
2701
2702 const find = function(id) {
2703 for (let i = 0; i < embeddedList.length; i++) {
2704 const item = embeddedList[i];
2705 if (idEquals(item[pk], id)) return item;
2706 }
2707 return null;
2708 };
2709
2710 let item = find(fkId.toString()); // in case of explicit id
2711 item = (item instanceof modelTo) ? item : null;
2712
2713 if (typeof cb === 'function') {
2714 process.nextTick(function() {
2715 cb(null, item);
2716 });
2717 }
2718
2719 return item; // sync
2720};
2721
2722EmbedsMany.prototype.exists = function(fkId, options, cb) {
2723 if (typeof options === 'function' && cb === undefined) {
2724 // customer.emails.exists(fkId, cb)
2725 cb = options;
2726 options = {};
2727 }
2728 const modelTo = this.definition.modelTo;
2729 const inst = this.findById(fkId, options, function(err, inst) {
2730 if (cb) cb(err, inst instanceof modelTo);
2731 });
2732 return inst instanceof modelTo; // sync
2733};
2734
2735EmbedsMany.prototype.updateById = function(fkId, data, options, cb) {
2736 if (typeof options === 'function' && cb === undefined) {
2737 // customer.emails.updateById(fkId, data, cb)
2738 cb = options;
2739 options = {};
2740 }
2741 if (typeof data === 'function') {
2742 // customer.emails.updateById(fkId, cb)
2743 cb = data;
2744 data = {};
2745 }
2746 options = options || {};
2747
2748 const modelTo = this.definition.modelTo;
2749 const propertyName = this.definition.keyFrom;
2750 const modelInstance = this.modelInstance;
2751
2752 const embeddedList = this.embeddedList();
2753
2754 const inst = this.findById(fkId);
2755
2756 if (inst instanceof modelTo) {
2757 const hookState = {};
2758 let context = {
2759 Model: modelTo,
2760 currentInstance: inst,
2761 data: data,
2762 options: options,
2763 hookState: hookState,
2764 };
2765 modelTo.notifyObserversOf('before save', context, function(err) {
2766 if (err) return cb && cb(err);
2767
2768 inst.setAttributes(data);
2769
2770 err = inst.isValid() ? null : new ValidationError(inst);
2771 if (err && typeof cb === 'function') {
2772 return process.nextTick(function() {
2773 cb(err, inst);
2774 });
2775 }
2776
2777 context = {
2778 Model: modelTo,
2779 instance: inst,
2780 options: options,
2781 hookState: hookState,
2782 };
2783
2784 if (typeof cb === 'function') {
2785 modelInstance.updateAttribute(propertyName, embeddedList, options,
2786 function(err) {
2787 if (err) return cb(err, inst);
2788 modelTo.notifyObserversOf('after save', context, function(err) {
2789 cb(err, inst);
2790 });
2791 });
2792 } else {
2793 modelTo.notifyObserversOf('after save', context, function(err) {
2794 if (!err) return;
2795 debug('Unhandled error in "after save" hooks: %s', err.stack || err);
2796 });
2797 }
2798 });
2799 } else if (typeof cb === 'function') {
2800 process.nextTick(function() {
2801 cb(null, null); // not found
2802 });
2803 }
2804 return inst; // sync
2805};
2806
2807EmbedsMany.prototype.destroyById = function(fkId, options, cb) {
2808 if (typeof options === 'function' && cb === undefined) {
2809 // customer.emails.destroyById(fkId, cb)
2810 cb = options;
2811 options = {};
2812 }
2813 const modelTo = this.definition.modelTo;
2814 const propertyName = this.definition.keyFrom;
2815 const modelInstance = this.modelInstance;
2816
2817 const embeddedList = this.embeddedList();
2818
2819 const inst = (fkId instanceof modelTo) ? fkId : this.findById(fkId);
2820
2821 if (inst instanceof modelTo) {
2822 const context = {
2823 Model: modelTo,
2824 instance: inst,
2825 options: options || {},
2826 hookState: {},
2827 };
2828 modelTo.notifyObserversOf('before delete', context, function(err) {
2829 if (err) return cb(err);
2830
2831 const index = embeddedList.indexOf(inst);
2832 if (index > -1) embeddedList.splice(index, 1);
2833 if (typeof cb !== 'function') return;
2834 modelInstance.updateAttribute(propertyName,
2835 embeddedList, context.options, function(err) {
2836 if (err) return cb(err);
2837 modelTo.notifyObserversOf('after delete', context, function(err) {
2838 cb(err);
2839 });
2840 });
2841 });
2842 } else if (typeof cb === 'function') {
2843 process.nextTick(cb); // not found
2844 }
2845 return inst; // sync
2846};
2847
2848EmbedsMany.prototype.destroyAll = function(where, options, cb) {
2849 if (typeof options === 'function' && cb === undefined) {
2850 // customer.emails.destroyAll(where, cb);
2851 cb = options;
2852 options = {};
2853 } else if (typeof where === 'function' &&
2854 options === undefined && cb === undefined) {
2855 // customer.emails.destroyAll(cb);
2856 cb = where;
2857 where = {};
2858 }
2859 const propertyName = this.definition.keyFrom;
2860 const modelInstance = this.modelInstance;
2861
2862 let embeddedList = this.embeddedList();
2863
2864 if (where && Object.keys(where).length > 0) {
2865 const filter = applyFilter({where: where});
2866 const reject = function(v) { return !filter(v); };
2867 embeddedList = embeddedList ? embeddedList.filter(reject) : embeddedList;
2868 } else {
2869 embeddedList = [];
2870 }
2871
2872 if (typeof cb === 'function') {
2873 modelInstance.updateAttribute(propertyName,
2874 embeddedList, options || {}, function(err) {
2875 cb(err);
2876 });
2877 } else {
2878 modelInstance.setAttribute(propertyName, embeddedList, options || {});
2879 }
2880};
2881
2882EmbedsMany.prototype.get = EmbedsMany.prototype.findById;
2883EmbedsMany.prototype.set = EmbedsMany.prototype.updateById;
2884EmbedsMany.prototype.unset = EmbedsMany.prototype.destroyById;
2885
2886EmbedsMany.prototype.at = function(index, cb) {
2887 const modelTo = this.definition.modelTo;
2888 const modelInstance = this.modelInstance;
2889
2890 const embeddedList = this.embeddedList();
2891
2892 let item = embeddedList[parseInt(index)];
2893 item = (item instanceof modelTo) ? item : null;
2894
2895 if (typeof cb === 'function') {
2896 process.nextTick(function() {
2897 cb(null, item);
2898 });
2899 }
2900
2901 return item; // sync
2902};
2903
2904EmbedsMany.prototype.create = function(targetModelData, options, cb) {
2905 const pk = this.definition.keyTo;
2906 const modelTo = this.definition.modelTo;
2907 const propertyName = this.definition.keyFrom;
2908 const modelInstance = this.modelInstance;
2909
2910 if (typeof options === 'function' && cb === undefined) {
2911 // customer.emails.create(cb)
2912 cb = options;
2913 options = {};
2914 }
2915
2916 if (typeof targetModelData === 'function' && !cb) {
2917 cb = targetModelData;
2918 targetModelData = {};
2919 }
2920 targetModelData = targetModelData || {};
2921 cb = cb || utils.createPromiseCallback();
2922
2923 const inst = this.callScopeMethod('build', targetModelData);
2924 const embeddedList = this.embeddedList();
2925
2926 const updateEmbedded = function(callback) {
2927 if (modelInstance.isNewRecord()) {
2928 modelInstance.setAttribute(propertyName, embeddedList);
2929 modelInstance.save(options, function(err) {
2930 callback(err, err ? null : inst);
2931 });
2932 } else {
2933 modelInstance.updateAttribute(propertyName,
2934 embeddedList, options, function(err) {
2935 callback(err, err ? null : inst);
2936 });
2937 }
2938 };
2939
2940 if (this.definition.options.persistent) {
2941 inst.save(function(err) { // will validate
2942 if (err) return cb(err, inst);
2943 updateEmbedded(cb);
2944 });
2945 } else {
2946 const err = inst.isValid() ? null : new ValidationError(inst);
2947 if (err) {
2948 process.nextTick(function() {
2949 cb(err);
2950 });
2951 } else {
2952 const context = {
2953 Model: modelTo,
2954 instance: inst,
2955 options: options || {},
2956 hookState: {},
2957 };
2958 modelTo.notifyObserversOf('before save', context, function(err) {
2959 if (err) return cb(err);
2960 updateEmbedded(function(err, inst) {
2961 if (err) return cb(err, null);
2962 modelTo.notifyObserversOf('after save', context, function(err) {
2963 cb(err, err ? null : inst);
2964 });
2965 });
2966 });
2967 }
2968 }
2969 return cb.promise;
2970};
2971
2972EmbedsMany.prototype.build = function(targetModelData) {
2973 const modelTo = this.definition.modelTo;
2974 const modelInstance = this.modelInstance;
2975 const forceId = this.definition.options.forceId;
2976 const persistent = this.definition.options.persistent;
2977 const propertyName = this.definition.keyFrom;
2978 const connector = modelTo.dataSource.connector;
2979
2980 const pk = this.definition.keyTo;
2981 const pkProp = modelTo.definition.properties[pk];
2982 const pkType = pkProp && pkProp.type;
2983
2984 const embeddedList = this.embeddedList();
2985
2986 targetModelData = targetModelData || {};
2987
2988 let assignId = (forceId || targetModelData[pk] === undefined);
2989 assignId = assignId && !persistent;
2990
2991 if (assignId && pkType === Number) {
2992 const ids = embeddedList.map(function(m) {
2993 return (typeof m[pk] === 'number' ? m[pk] : 0);
2994 });
2995 if (ids.length > 0) {
2996 targetModelData[pk] = Math.max.apply(null, ids) + 1;
2997 } else {
2998 targetModelData[pk] = 1;
2999 }
3000 } else if (assignId && typeof connector.generateId === 'function') {
3001 const id = connector.generateId(modelTo.modelName, targetModelData, pk);
3002 targetModelData[pk] = id;
3003 }
3004
3005 this.definition.applyProperties(modelInstance, targetModelData);
3006
3007 const inst = new modelTo(targetModelData);
3008
3009 if (this.definition.options.prepend) {
3010 embeddedList.unshift(inst);
3011 modelInstance[propertyName] = embeddedList;
3012 } else {
3013 embeddedList.push(inst);
3014 modelInstance[propertyName] = embeddedList;
3015 }
3016
3017 this.prepareEmbeddedInstance(inst);
3018
3019 return inst;
3020};
3021
3022/**
3023 * Add the target model instance to the 'embedsMany' relation
3024 * @param {Object|ID} acInst The actual instance or id value
3025 */
3026EmbedsMany.prototype.add = function(acInst, data, options, cb) {
3027 if (typeof options === 'function' && cb === undefined) {
3028 // customer.emails.add(acInst, data, cb)
3029 cb = options;
3030 options = {};
3031 } else if (typeof data === 'function' &&
3032 options === undefined && cb === undefined) {
3033 // customer.emails.add(acInst, cb)
3034 cb = data;
3035 data = {};
3036 }
3037 cb = cb || utils.createPromiseCallback();
3038
3039 const self = this;
3040 const definition = this.definition;
3041 const modelTo = this.definition.modelTo;
3042 const modelInstance = this.modelInstance;
3043
3044 const defOpts = definition.options;
3045 const belongsTo = defOpts.belongsTo && modelTo.relations[defOpts.belongsTo];
3046
3047 if (!belongsTo) {
3048 throw new Error('Invalid reference: ' + defOpts.belongsTo || '(none)');
3049 }
3050
3051 const fk2 = belongsTo.keyTo;
3052 const pk2 = belongsTo.modelTo.definition.idName() || 'id';
3053
3054 const query = {};
3055
3056 query[fk2] = (acInst instanceof belongsTo.modelTo) ? acInst[pk2] : acInst;
3057
3058 const filter = {where: query};
3059
3060 belongsTo.applyScope(modelInstance, filter);
3061
3062 belongsTo.modelTo.findOne(filter, options, function(err, ref) {
3063 if (ref instanceof belongsTo.modelTo) {
3064 const inst = self.build(data || {});
3065 inst[defOpts.belongsTo](ref);
3066 modelInstance.save(function(err) {
3067 cb(err, err ? null : inst);
3068 });
3069 } else {
3070 cb(null, null);
3071 }
3072 });
3073 return cb.promise;
3074};
3075
3076/**
3077 * Remove the target model instance from the 'embedsMany' relation
3078 * @param {Object|ID) acInst The actual instance or id value
3079 */
3080EmbedsMany.prototype.remove = function(acInst, options, cb) {
3081 if (typeof options === 'function' && cb === undefined) {
3082 // customer.emails.remove(acInst, cb)
3083 cb = options;
3084 options = {};
3085 }
3086 const self = this;
3087 const definition = this.definition;
3088 const modelTo = this.definition.modelTo;
3089 const modelInstance = this.modelInstance;
3090
3091 const defOpts = definition.options;
3092 const belongsTo = defOpts.belongsTo && modelTo.relations[defOpts.belongsTo];
3093
3094 if (!belongsTo) {
3095 throw new Error('Invalid reference: ' + defOpts.belongsTo || '(none)');
3096 }
3097
3098 const fk2 = belongsTo.keyTo;
3099 const pk2 = belongsTo.modelTo.definition.idName() || 'id';
3100
3101 const query = {};
3102
3103 query[fk2] = (acInst instanceof belongsTo.modelTo) ? acInst[pk2] : acInst;
3104
3105 const filter = {where: query};
3106
3107 belongsTo.applyScope(modelInstance, filter);
3108
3109 cb = cb || utils.createPromiseCallback();
3110
3111 modelInstance[definition.name](filter, options, function(err, items) {
3112 if (err) return cb(err);
3113
3114 items.forEach(function(item) {
3115 self.unset(item);
3116 });
3117
3118 modelInstance.save(options, function(err) {
3119 cb(err);
3120 });
3121 });
3122 return cb.promise;
3123};
3124
3125RelationDefinition.referencesMany = function referencesMany(modelFrom, modelToRef, params) {
3126 params = params || {};
3127 normalizeRelationAs(params, modelToRef);
3128 const modelTo = lookupModelTo(modelFrom, modelToRef, params, true);
3129
3130 const thisClassName = modelFrom.modelName;
3131 const relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
3132 const fk = params.foreignKey || i8n.camelize(modelTo.modelName + '_ids', true);
3133 const idName = modelTo.dataSource.idName(modelTo.modelName) || 'id';
3134 const idType = modelTo.definition.properties[idName].type;
3135
3136 const definition = modelFrom.relations[relationName] = new RelationDefinition({
3137 name: relationName,
3138 type: RelationTypes.referencesMany,
3139 modelFrom: modelFrom,
3140 keyFrom: fk,
3141 keyTo: idName,
3142 modelTo: modelTo,
3143 multiple: true,
3144 properties: params.properties,
3145 scope: params.scope,
3146 options: params.options,
3147 });
3148
3149 modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, {
3150 type: [idType], default: function() { return []; },
3151 });
3152
3153 modelFrom.validate(relationName, function(err) {
3154 const ids = this[fk] || [];
3155 if (idsHaveDuplicates(ids)) {
3156 const msg = 'contains duplicate `' + modelTo.modelName + '` instance';
3157 this.errors.add(relationName, msg, 'uniqueness');
3158 err(false);
3159 }
3160 }, {code: 'uniqueness'});
3161
3162 const scopeMethods = {
3163 findById: scopeMethod(definition, 'findById'),
3164 destroy: scopeMethod(definition, 'destroyById'),
3165 updateById: scopeMethod(definition, 'updateById'),
3166 exists: scopeMethod(definition, 'exists'),
3167 add: scopeMethod(definition, 'add'),
3168 remove: scopeMethod(definition, 'remove'),
3169 at: scopeMethod(definition, 'at'),
3170 };
3171
3172 const findByIdFunc = scopeMethods.findById;
3173 modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
3174
3175 const destroyByIdFunc = scopeMethods.destroy;
3176 modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
3177
3178 const updateByIdFunc = scopeMethods.updateById;
3179 modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
3180
3181 const addFunc = scopeMethods.add;
3182 modelFrom.prototype['__link__' + relationName] = addFunc;
3183
3184 const removeFunc = scopeMethods.remove;
3185 modelFrom.prototype['__unlink__' + relationName] = removeFunc;
3186
3187 scopeMethods.create = scopeMethod(definition, 'create');
3188 scopeMethods.build = scopeMethod(definition, 'build');
3189
3190 scopeMethods.related = scopeMethod(definition, 'related');
3191
3192 const customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
3193
3194 for (let i = 0; i < customMethods.length; i++) {
3195 const methodName = customMethods[i];
3196 const method = scopeMethods[methodName];
3197 if (typeof method === 'function' && method.shared === true) {
3198 modelFrom.prototype['__' + methodName + '__' + relationName] = method;
3199 }
3200 }
3201
3202 // Mix the property and scoped methods into the prototype class
3203 const scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function() {
3204 return {};
3205 }, scopeMethods, definition.options);
3206
3207 scopeDefinition.related = scopeMethods.related; // bound to definition
3208
3209 return definition;
3210};
3211
3212ReferencesMany.prototype.related = function(receiver, scopeParams, condOrRefresh, options, cb) {
3213 const fk = this.definition.keyFrom;
3214 const modelTo = this.definition.modelTo;
3215 const relationName = this.definition.name;
3216 const modelInstance = this.modelInstance;
3217 const self = receiver;
3218
3219 let actualCond = {};
3220 let actualRefresh = false;
3221
3222 if (typeof condOrRefresh === 'function' &&
3223 options === undefined && cb === undefined) {
3224 // customer.orders(receiver, scopeParams, cb)
3225 cb = condOrRefresh;
3226 condOrRefresh = undefined;
3227 } else if (typeof options === 'function' && cb === undefined) {
3228 // customer.orders(receiver, scopeParams, condOrRefresh, cb)
3229 cb = options;
3230 options = {};
3231 if (typeof condOrRefresh === 'boolean') {
3232 actualRefresh = condOrRefresh;
3233 condOrRefresh = {};
3234 } else {
3235 actualRefresh = true;
3236 }
3237 }
3238 actualCond = condOrRefresh || {};
3239
3240 const ids = self[fk] || [];
3241
3242 this.definition.applyScope(modelInstance, actualCond);
3243
3244 const params = mergeQuery(actualCond, scopeParams);
3245 return modelTo.findByIds(ids, params, options, cb);
3246};
3247
3248ReferencesMany.prototype.findById = function(fkId, options, cb) {
3249 if (typeof options === 'function' && cb === undefined) {
3250 // customer.orders.findById(fkId, cb)
3251 cb = options;
3252 options = {};
3253 }
3254 const modelTo = this.definition.modelTo;
3255 const modelFrom = this.definition.modelFrom;
3256 const relationName = this.definition.name;
3257 const modelInstance = this.modelInstance;
3258
3259 const pk = this.definition.keyTo;
3260 const fk = this.definition.keyFrom;
3261
3262 if (typeof fkId === 'object') {
3263 fkId = fkId.toString(); // mongodb
3264 }
3265
3266 const ids = modelInstance[fk] || [];
3267
3268 const filter = {};
3269
3270 this.definition.applyScope(modelInstance, filter);
3271
3272 cb = cb || utils.createPromiseCallback();
3273
3274 modelTo.findByIds([fkId], filter, options, function(err, instances) {
3275 if (err) {
3276 return cb(err);
3277 }
3278
3279 const inst = instances[0];
3280 if (!inst) {
3281 err = new Error(g.f('No instance with {{id}} %s found for %s', fkId, modelTo.modelName));
3282 err.statusCode = 404;
3283 return cb(err);
3284 }
3285
3286 // Check if the foreign key is amongst the ids
3287 if (utils.findIndexOf(ids, inst[pk], idEquals) > -1) {
3288 cb(null, inst);
3289 } else {
3290 err = new Error(g.f('Key mismatch: %s.%s: %s, %s.%s: %s',
3291 modelFrom.modelName, fk, modelInstance[fk],
3292 modelTo.modelName, pk, inst[pk]));
3293 err.statusCode = 400;
3294 cb(err);
3295 }
3296 });
3297 return cb.promise;
3298};
3299
3300ReferencesMany.prototype.exists = function(fkId, options, cb) {
3301 if (typeof options === 'function' && cb === undefined) {
3302 // customer.orders.exists(fkId, cb)
3303 cb = options;
3304 options = {};
3305 }
3306 const fk = this.definition.keyFrom;
3307 const ids = this.modelInstance[fk] || [];
3308
3309 cb = cb || utils.createPromiseCallback();
3310 process.nextTick(function() { cb(null, utils.findIndexOf(ids, fkId, idEquals) > -1); });
3311 return cb.promise;
3312};
3313
3314ReferencesMany.prototype.updateById = function(fkId, data, options, cb) {
3315 if (typeof options === 'function' && cb === undefined) {
3316 // customer.orders.updateById(fkId, data, cb)
3317 cb = options;
3318 options = {};
3319 } else if (typeof data === 'function' &&
3320 options === undefined && cb === undefined) {
3321 // customer.orders.updateById(fkId, cb)
3322 cb = data;
3323 data = {};
3324 }
3325 cb = cb || utils.createPromiseCallback();
3326
3327 this.findById(fkId, options, function(err, inst) {
3328 if (err) return cb(err);
3329 inst.updateAttributes(data, options, cb);
3330 });
3331 return cb.promise;
3332};
3333
3334ReferencesMany.prototype.destroyById = function(fkId, options, cb) {
3335 if (typeof options === 'function' && cb === undefined) {
3336 // customer.orders.destroyById(fkId, cb)
3337 cb = options;
3338 options = {};
3339 }
3340 const self = this;
3341 cb = cb || utils.createPromiseCallback();
3342 this.findById(fkId, function(err, inst) {
3343 if (err) return cb(err);
3344 self.remove(inst, function(err, ids) {
3345 inst.destroy(cb);
3346 });
3347 });
3348 return cb.promise;
3349};
3350
3351ReferencesMany.prototype.at = function(index, options, cb) {
3352 if (typeof options === 'function' && cb === undefined) {
3353 // customer.orders.at(index, cb)
3354 cb = options;
3355 options = {};
3356 }
3357 const fk = this.definition.keyFrom;
3358 const ids = this.modelInstance[fk] || [];
3359 cb = cb || utils.createPromiseCallback();
3360 this.findById(ids[index], options, cb);
3361 return cb.promise;
3362};
3363
3364ReferencesMany.prototype.create = function(targetModelData, options, cb) {
3365 if (typeof options === 'function' && cb === undefined) {
3366 // customer.orders.create(data, cb)
3367 cb = options;
3368 options = {};
3369 }
3370 const definition = this.definition;
3371 const modelTo = this.definition.modelTo;
3372 const relationName = this.definition.name;
3373 const modelInstance = this.modelInstance;
3374
3375 const pk = this.definition.keyTo;
3376 const fk = this.definition.keyFrom;
3377
3378 if (typeof targetModelData === 'function' && !cb) {
3379 cb = targetModelData;
3380 targetModelData = {};
3381 }
3382 targetModelData = targetModelData || {};
3383 cb = cb || utils.createPromiseCallback();
3384
3385 const ids = modelInstance[fk] || [];
3386
3387 const inst = this.callScopeMethod('build', targetModelData);
3388
3389 inst.save(options, function(err, inst) {
3390 if (err) return cb(err, inst);
3391
3392 let id = inst[pk];
3393
3394 if (typeof id === 'object') {
3395 id = id.toString(); // mongodb
3396 }
3397
3398 if (definition.options.prepend) {
3399 ids.unshift(id);
3400 } else {
3401 ids.push(id);
3402 }
3403
3404 modelInstance.updateAttribute(fk,
3405 ids, options, function(err, modelInst) {
3406 cb(err, inst);
3407 });
3408 });
3409 return cb.promise;
3410};
3411
3412ReferencesMany.prototype.build = function(targetModelData) {
3413 const modelTo = this.definition.modelTo;
3414 targetModelData = targetModelData || {};
3415
3416 this.definition.applyProperties(this.modelInstance, targetModelData);
3417
3418 return new modelTo(targetModelData);
3419};
3420
3421/**
3422 * Add the target model instance to the 'embedsMany' relation
3423 * @param {Object|ID} acInst The actual instance or id value
3424 */
3425ReferencesMany.prototype.add = function(acInst, options, cb) {
3426 if (typeof options === 'function' && cb === undefined) {
3427 // customer.orders.add(acInst, cb)
3428 cb = options;
3429 options = {};
3430 }
3431 const self = this;
3432 const definition = this.definition;
3433 const modelTo = this.definition.modelTo;
3434 const modelInstance = this.modelInstance;
3435
3436 const pk = this.definition.keyTo;
3437 const fk = this.definition.keyFrom;
3438
3439 const insert = function(inst, done) {
3440 let id = inst[pk];
3441
3442 if (typeof id === 'object') {
3443 id = id.toString(); // mongodb
3444 }
3445
3446 const ids = modelInstance[fk] || [];
3447
3448 if (definition.options.prepend) {
3449 ids.unshift(id);
3450 } else {
3451 ids.push(id);
3452 }
3453
3454 modelInstance.updateAttribute(fk, ids, options, function(err) {
3455 done(err, err ? null : inst);
3456 });
3457 };
3458
3459 cb = cb || utils.createPromiseCallback();
3460
3461 if (acInst instanceof modelTo) {
3462 insert(acInst, cb);
3463 } else {
3464 const filter = {where: {}};
3465 filter.where[pk] = acInst;
3466
3467 definition.applyScope(modelInstance, filter);
3468
3469 modelTo.findOne(filter, options, function(err, inst) {
3470 if (err || !inst) return cb(err, null);
3471 insert(inst, cb);
3472 });
3473 }
3474 return cb.promise;
3475};
3476
3477/**
3478 * Remove the target model instance from the 'embedsMany' relation
3479 * @param {Object|ID) acInst The actual instance or id value
3480 */
3481ReferencesMany.prototype.remove = function(acInst, options, cb) {
3482 if (typeof options === 'function' && cb === undefined) {
3483 // customer.orders.remove(acInst, cb)
3484 cb = options;
3485 options = {};
3486 }
3487 const definition = this.definition;
3488 const modelInstance = this.modelInstance;
3489
3490 const pk = this.definition.keyTo;
3491 const fk = this.definition.keyFrom;
3492
3493 const ids = modelInstance[fk] || [];
3494
3495 const id = (acInst instanceof definition.modelTo) ? acInst[pk] : acInst;
3496
3497 cb = cb || utils.createPromiseCallback();
3498
3499 const index = utils.findIndexOf(ids, id, idEquals);
3500 if (index > -1) {
3501 ids.splice(index, 1);
3502 modelInstance.updateAttribute(fk, ids, options, function(err, inst) {
3503 cb(err, inst[fk] || []);
3504 });
3505 } else {
3506 process.nextTick(function() { cb(null, ids); });
3507 }
3508 return cb.promise;
3509};